Added ChapterImageModelLoader
This commit is contained in:
parent
9497a97289
commit
312cb84598
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue