Added ChapterImageModelLoader

This commit is contained in:
ByteHamster 2020-02-10 12:27:09 +01:00
parent 9497a97289
commit 312cb84598
7 changed files with 220 additions and 38 deletions

View File

@ -6,6 +6,7 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ListView; import android.widget.ListView;
import de.danoeh.antennapod.core.util.ChapterUtils;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
@ -104,18 +105,10 @@ public class ChaptersFragment extends ListFragment {
} }
private int getCurrentChapter(Playable media) { private int getCurrentChapter(Playable media) {
if (media == null || media.getChapters() == null || media.getChapters().size() == 0 || controller == null) { if (controller == null) {
return -1; return -1;
} }
int currentPosition = controller.getPosition(); return ChapterUtils.getCurrentChapterIndex(media, controller.getPosition());
List<Chapter> chapters = media.getChapters();
for (int i = 0; i < chapters.size(); i++) {
if (chapters.get(i).getStart() > currentPosition) {
return i - 1;
}
}
return chapters.size() - 1;
} }
private void loadMediaInfo() { private void loadMediaInfo() {

View File

@ -1,6 +1,7 @@
package de.danoeh.antennapod.fragment; package de.danoeh.antennapod.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import android.util.Log; import android.util.Log;
@ -14,14 +15,20 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; 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.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.PlaybackController;
import io.reactivex.Maybe; import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; 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. * Displays the cover and the title of a FeedItem.
@ -36,6 +43,8 @@ public class CoverFragment extends Fragment {
private ImageView imgvCover; private ImageView imgvCover;
private PlaybackController controller; private PlaybackController controller;
private Disposable disposable; private Disposable disposable;
private int displayedChapterIndex = -1;
private Playable media;
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
@ -53,18 +62,20 @@ public class CoverFragment extends Fragment {
if (disposable != null) { if (disposable != null) {
disposable.dispose(); disposable.dispose();
} }
disposable = Maybe.create(emitter -> { disposable = Maybe.<Playable>create(emitter -> {
Playable media = controller.getMedia(); Playable media = controller.getMedia();
if (media != null) { if (media != null) {
emitter.onSuccess(media); emitter.onSuccess(media);
} else { } else {
emitter.onComplete(); emitter.onComplete();
} }
}) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> displayMediaInfo((Playable) media), .subscribe(media -> {
error -> Log.e(TAG, Log.getStackTraceString(error))); this.media = media;
displayMediaInfo(media);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
} }
private void displayMediaInfo(@NonNull Playable media) { private void displayMediaInfo(@NonNull Playable media) {
@ -99,6 +110,7 @@ public class CoverFragment extends Fragment {
}; };
controller.init(); controller.init();
loadMediaInfo(); loadMediaInfo();
EventBus.getDefault().register(this);
} }
@Override @Override
@ -106,6 +118,30 @@ public class CoverFragment extends Fragment {
super.onStop(); super.onStop();
controller.release(); controller.release();
controller = null; 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 @Override

View File

@ -11,10 +11,12 @@ import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.module.AppGlideModule;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import java.io.InputStream; import java.io.InputStream;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import java.nio.ByteBuffer;
/** /**
* {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule} * {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule}
@ -32,5 +34,6 @@ public class ApGlideModule extends AppGlideModule {
@Override @Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
} }
} }

View File

@ -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<EmbeddedChapterImage, ByteBuffer> {
public static class Factory implements ModelLoaderFactory<EmbeddedChapterImage, ByteBuffer> {
@Override
public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(MultiModelLoaderFactory unused) {
return new ChapterImageModelLoader();
}
@Override
public void teardown() {
// Do nothing.
}
}
@Nullable
@Override
public LoadData<ByteBuffer> 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<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));
} 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<ByteBuffer> getDataClass() {
return ByteBuffer.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.LOCAL;
}
}
}

View File

@ -36,24 +36,17 @@ public class ChapterUtils {
private ChapterUtils() { private ChapterUtils() {
} }
@Nullable public static int getCurrentChapterIndex(Playable media, int position) {
public static Chapter getCurrentChapter(Playable media) { if (media == null || media.getChapters() == null || media.getChapters().size() == 0) {
if (media.getChapters() == null) { return -1;
return null;
} }
List<Chapter> chapters = media.getChapters(); List<Chapter> chapters = media.getChapters();
if (chapters == null) { for (int i = 0; i < chapters.size(); i++) {
return null; if (chapters.get(i).getStart() > position) {
} return i - 1;
Chapter current = chapters.get(0);
for (Chapter sc : chapters) {
if (sc.getStart() > media.getPosition()) {
break;
} else {
current = sc;
} }
} }
return current; return chapters.size() - 1;
} }
public static void loadChaptersFromStreamUrl(Playable media) { public static void loadChaptersFromStreamUrl(Playable media) {

View File

@ -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;
}
}
}

View File

@ -4,6 +4,7 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.ID3Chapter; 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.FrameHeader;
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
@ -104,8 +105,8 @@ public class ChapterReader extends ID3Reader {
// Data contains the picture // Data contains the picture
int length = header.getSize() - read; int length = header.getSize() - read;
if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
currentChapter.setImageUrl("embedded-image://" + mime.toString() currentChapter.setImageUrl(
+ "@" + input.getByteCount() + ":" + length); EmbeddedChapterImage.makeUrl(mime.toString(), input.getCount(), length));
} }
skipBytes(input, length); skipBytes(input, length);
} }