diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index 386b760b1..9940ccbdd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.View; import android.widget.ListView; +import de.danoeh.antennapod.core.util.ChapterUtils; import java.util.List; import java.util.ListIterator; @@ -104,18 +105,10 @@ public class ChaptersFragment extends ListFragment { } private int getCurrentChapter(Playable media) { - if (media == null || media.getChapters() == null || media.getChapters().size() == 0 || controller == null) { + if (controller == null) { return -1; } - int currentPosition = controller.getPosition(); - - List chapters = media.getChapters(); - for (int i = 0; i < chapters.size(); i++) { - if (chapters.get(i).getStart() > currentPosition) { - return i - 1; - } - } - return chapters.size() - 1; + return ChapterUtils.getCurrentChapterIndex(media, controller.getPosition()); } private void loadMediaInfo() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 5467d71a8..b9263f21e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import android.util.Log; @@ -14,14 +15,20 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.util.EmbeddedChapterImage; +import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Displays the cover and the title of a FeedItem. @@ -36,6 +43,8 @@ public class CoverFragment extends Fragment { private ImageView imgvCover; private PlaybackController controller; private Disposable disposable; + private int displayedChapterIndex = -1; + private Playable media; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -53,18 +62,20 @@ public class CoverFragment extends Fragment { if (disposable != null) { disposable.dispose(); } - disposable = Maybe.create(emitter -> { - Playable media = controller.getMedia(); - if (media != null) { - emitter.onSuccess(media); - } else { - emitter.onComplete(); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> displayMediaInfo((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error))); + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> { + this.media = media; + displayMediaInfo(media); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); } private void displayMediaInfo(@NonNull Playable media) { @@ -99,6 +110,7 @@ public class CoverFragment extends Fragment { }; controller.init(); loadMediaInfo(); + EventBus.getDefault().register(this); } @Override @@ -106,6 +118,30 @@ public class CoverFragment extends Fragment { super.onStop(); controller.release(); controller = null; + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (controller == null) { + return; + } + int chapter = ChapterUtils.getCurrentChapterIndex(media, event.getPosition()); + if (chapter != displayedChapterIndex) { + displayedChapterIndex = chapter; + + if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) { + displayMediaInfo(media); + } else { + Glide.with(this) + .load(EmbeddedChapterImage.getModelFor(media, chapter)) + .apply(new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .fitCenter()) + .into(imgvCover); + } + } } @Override 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 b2c809e90..50511526f 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 @@ -11,10 +11,12 @@ import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; import com.bumptech.glide.module.AppGlideModule; +import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import java.io.InputStream; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.core.preferences.UserPreferences; +import java.nio.ByteBuffer; /** * {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule} @@ -32,5 +34,6 @@ 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 ApOkHttpUrlLoader.Factory()); + registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java new file mode 100644 index 000000000..6548e9c5e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java @@ -0,0 +1,98 @@ +package de.danoeh.antennapod.core.glide; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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.feed.Chapter; +import de.danoeh.antennapod.core.util.EmbeddedChapterImage; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.ByteBuffer; +import org.apache.commons.io.IOUtils; + +public final class ChapterImageModelLoader implements ModelLoader { + + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(MultiModelLoaderFactory unused) { + return new ChapterImageModelLoader(); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + @Nullable + @Override + public LoadData buildLoadData(EmbeddedChapterImage model, int width, int height, Options options) { + return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model)); + } + + @Override + public boolean handles(EmbeddedChapterImage model) { + return true; + } + + class EmbeddedImageFetcher implements DataFetcher { + private final EmbeddedChapterImage image; + + public EmbeddedImageFetcher(EmbeddedChapterImage image) { + this.image = image; + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { + + BufferedInputStream stream = null; + try { + if (image.getMedia().localFileAvailable()) { + File localFile = new File(image.getMedia().getLocalMediaUrl()); + stream = new BufferedInputStream(new FileInputStream(localFile)); + } else { + URL url = new URL(image.getMedia().getStreamUrl()); + stream = new BufferedInputStream(url.openStream()); + } + byte[] imageContent = new byte[image.getLength()]; + stream.skip(image.getPosition()); + stream.read(imageContent, 0, image.getLength()); + callback.onDataReady(ByteBuffer.wrap(imageContent)); + } catch (IOException e) { + callback.onLoadFailed(new IOException("Loading embedded cover did not work")); + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(stream); + } + } + + @Override public void cleanup() { + // nothing to clean up + } + @Override public void cancel() { + // cannot cancel + } + + @NonNull + @Override + public Class getDataClass() { + return ByteBuffer.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.LOCAL; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java index e69bb2863..b75887154 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -36,24 +36,17 @@ public class ChapterUtils { private ChapterUtils() { } - @Nullable - public static Chapter getCurrentChapter(Playable media) { - if (media.getChapters() == null) { - return null; + public static int getCurrentChapterIndex(Playable media, int position) { + if (media == null || media.getChapters() == null || media.getChapters().size() == 0) { + return -1; } List chapters = media.getChapters(); - if (chapters == null) { - return null; - } - Chapter current = chapters.get(0); - for (Chapter sc : chapters) { - if (sc.getStart() > media.getPosition()) { - break; - } else { - current = sc; + for (int i = 0; i < chapters.size(); i++) { + if (chapters.get(i).getStart() > position) { + return i - 1; } } - return current; + return chapters.size() - 1; } public static void loadChaptersFromStreamUrl(Playable media) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/EmbeddedChapterImage.java b/core/src/main/java/de/danoeh/antennapod/core/util/EmbeddedChapterImage.java new file mode 100644 index 000000000..5cb62e6c2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/EmbeddedChapterImage.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.util.playback.Playable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmbeddedChapterImage { + private static final Pattern EMBEDDED_IMAGE_MATCHER = Pattern.compile("embedded-image://(.*)@(\\d+):(\\d+)"); + final String mime; + final int position; + final int length; + final Playable media; + + public EmbeddedChapterImage(Playable media, String imageUrl) { + this.media = media; + Matcher m = EMBEDDED_IMAGE_MATCHER.matcher(imageUrl); + if (m.find()) { + this.mime = m.group(1); + this.position = Integer.parseInt(m.group(2)); + this.length = Integer.parseInt(m.group(3)); + } else { + throw new IllegalArgumentException("Not an embedded chapter"); + } + } + + public static String makeUrl(String mime, int position, int length) { + return "embedded-image://" + mime + "@" + position + ":" + length; + } + + public String getMime() { + return mime; + } + + public int getPosition() { + return position; + } + + public int getLength() { + return length; + } + + public Playable getMedia() { + return media; + } + + private static boolean isEmbeddedChapterImage(String imageUrl) { + return EMBEDDED_IMAGE_MATCHER.matcher(imageUrl).matches(); + } + + public static Object getModelFor(Playable media, int chapter) { + String imageUrl = media.getChapters().get(chapter).getImageUrl(); + if (isEmbeddedChapterImage(imageUrl)) { + return new EmbeddedChapterImage(media, imageUrl); + } else { + return imageUrl; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java index b007967c1..934e0b00c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -4,6 +4,7 @@ import android.text.TextUtils; import android.util.Log; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.ID3Chapter; +import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; @@ -104,8 +105,8 @@ public class ChapterReader extends ID3Reader { // Data contains the picture int length = header.getSize() - read; if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { - currentChapter.setImageUrl("embedded-image://" + mime.toString() - + "@" + input.getByteCount() + ":" + length); + currentChapter.setImageUrl( + EmbeddedChapterImage.makeUrl(mime.toString(), input.getCount(), length)); } skipBytes(input, length); }