summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java32
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java36
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java29
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java5
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java51
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java14
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java40
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java12
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java3
-rw-r--r--app/src/main/res/layout/addfeed.xml14
-rw-r--r--core/build.gradle1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java151
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java28
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java57
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java193
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java98
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java46
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java4
-rw-r--r--core/src/main/res/raw/local_feed_default_icon.pngbin0 -> 1240 bytes
-rw-r--r--core/src/main/res/values/colors.xml1
-rw-r--r--core/src/main/res/values/strings.xml4
37 files changed, 701 insertions, 206 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java
index 595a9794f..84b8d0e09 100644
--- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java
@@ -68,7 +68,7 @@ public class DBTasksTest {
for (int i = 0; i < NUM_ITEMS; i++) {
feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(), FeedItem.UNPLAYED, feed));
}
- Feed newFeed = DBTasks.updateFeed(context, feed)[0];
+ Feed newFeed = DBTasks.updateFeed(context, feed, false);
assertSame(feed, newFeed);
assertTrue(feed.getId() != 0);
@@ -88,8 +88,8 @@ public class DBTasksTest {
feed1.setItems(new ArrayList<>());
feed2.setItems(new ArrayList<>());
- Feed savedFeed1 = DBTasks.updateFeed(context, feed1)[0];
- Feed savedFeed2 = DBTasks.updateFeed(context, feed2)[0];
+ Feed savedFeed1 = DBTasks.updateFeed(context, feed1, false);
+ Feed savedFeed2 = DBTasks.updateFeed(context, feed2, false);
assertTrue(savedFeed1.getId() != savedFeed2.getId());
}
@@ -124,7 +124,7 @@ public class DBTasksTest {
feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.UNPLAYED, feed));
}
- final Feed newFeed = DBTasks.updateFeed(context, feed)[0];
+ final Feed newFeed = DBTasks.updateFeed(context, feed, false);
assertNotSame(newFeed, feed);
updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW);
@@ -156,7 +156,7 @@ public class DBTasksTest {
list.add(item);
feed.setItems(list);
- final Feed newFeed = DBTasks.updateFeed(context, feed)[0];
+ final Feed newFeed = DBTasks.updateFeed(context, feed, false);
assertNotSame(newFeed, feed);
final Feed feedFromDB = DBReader.getFeed(newFeed.getId());
@@ -164,6 +164,28 @@ public class DBTasksTest {
assertTrue("state: " + feedItemFromDB.getState(), feedItemFromDB.isNew());
}
+ @Test
+ public void testUpdateFeedRemoveUnlistedItems() {
+ final Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < 10; i++) {
+ feed.getItems().add(
+ new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed));
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // delete some items
+ feed.getItems().subList(0, 2).clear();
+ Feed newFeed = DBTasks.updateFeed(context, feed, true);
+ assertEquals(8, newFeed.getItems().size()); // 10 - 2 = 8 items
+
+ Feed feedFromDB = DBReader.getFeed(newFeed.getId());
+ assertEquals(8, feedFromDB.getItems().size()); // 10 - 2 = 8 items
+ }
+
private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) {
assertEquals(feedID, newFeed.getId());
assertEquals(NUM_ITEMS_NEW + NUM_ITEMS_OLD, newFeed.getItems().size());
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
index 9fbc5b956..f7be8a371 100644
--- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
@@ -428,6 +428,42 @@ public class DBWriterTest {
adapter.close();
}
+ @Test
+ public void testDeleteFeedItems() throws Exception {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ feed.setImageUrl("url");
+
+ // create items
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ List<FeedItem> itemsToDelete = feed.getItems().subList(0, 2);
+ DBWriter.deleteFeedItems(InstrumentationRegistry.getInstrumentation()
+ .getTargetContext(), itemsToDelete).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (int i = 0; i < feed.getItems().size(); i++) {
+ FeedItem feedItem = feed.getItems().get(i);
+ Cursor c = adapter.getFeedItemCursor(String.valueOf(feedItem.getId()));
+ if (i < 2) {
+ assertEquals(0, c.getCount());
+ } else {
+ assertEquals(1, c.getCount());
+ }
+ c.close();
+ }
+ adapter.close();
+ }
+
private FeedMedia playbackHistorySetup(Date playbackCompletionDate) {
Feed feed = new Feed("url", null, "title");
feed.setItems(new ArrayList<>());
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
index 5acc25bee..d782d4ed5 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java
@@ -15,6 +15,8 @@ import com.bumptech.glide.request.target.CustomViewTarget;
import java.lang.ref.WeakReference;
import com.bumptech.glide.request.transition.Transition;
+
+import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
@@ -23,6 +25,7 @@ public class CoverLoader {
private String fallbackUri;
private TextView txtvPlaceholder;
private ImageView imgvCover;
+ private boolean textAndImageCombined;
private MainActivity activity;
public CoverLoader(MainActivity activity) {
@@ -49,6 +52,19 @@ public class CoverLoader {
return this;
}
+ /**
+ * Set cover text and if it should be shown even if there is a cover image.
+ *
+ * @param placeholderView Cover text.
+ * @param textAndImageCombined Show cover text even if there is a cover image?
+ */
+ @NonNull
+ public CoverLoader withPlaceholderView(@NonNull TextView placeholderView, boolean textAndImageCombined) {
+ this.txtvPlaceholder = placeholderView;
+ this.textAndImageCombined = textAndImageCombined;
+ return this;
+ }
+
public void load() {
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
@@ -65,20 +81,22 @@ public class CoverLoader {
.apply(options));
}
- builder.into(new CoverTarget(txtvPlaceholder, imgvCover));
+ builder.into(new CoverTarget(txtvPlaceholder, imgvCover, textAndImageCombined));
}
static class CoverTarget extends CustomViewTarget<ImageView, Drawable> {
private final WeakReference<TextView> placeholder;
private final WeakReference<ImageView> cover;
+ private boolean textAndImageCombined;
- public CoverTarget(TextView txtvPlaceholder, ImageView imgvCover) {
+ public CoverTarget(TextView txtvPlaceholder, ImageView imgvCover, boolean textAndImageCombined) {
super(imgvCover);
if (txtvPlaceholder != null) {
txtvPlaceholder.setVisibility(View.VISIBLE);
}
placeholder = new WeakReference<>(txtvPlaceholder);
cover = new WeakReference<>(imgvCover);
+ this.textAndImageCombined = textAndImageCombined;
}
@Override
@@ -90,7 +108,12 @@ public class CoverLoader {
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
TextView txtvPlaceholder = placeholder.get();
if (txtvPlaceholder != null) {
- txtvPlaceholder.setVisibility(View.INVISIBLE);
+ if (textAndImageCombined) {
+ int bgColor = txtvPlaceholder.getContext().getResources().getColor(R.color.feed_text_bg);
+ txtvPlaceholder.setBackgroundColor(bgColor);
+ } else {
+ txtvPlaceholder.setVisibility(View.INVISIBLE);
+ }
}
ImageView ivCover = cover.get();
ivCover.setImageDrawable(resource);
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
index fdda526ff..8c294a9c9 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SubscriptionsAdapter.java
@@ -22,6 +22,7 @@ import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import jp.shts.android.library.TriangleLabelView;
@@ -107,9 +108,11 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
holder.count.setVisibility(View.GONE);
}
+ boolean textAndImageCombined = feed.isLocalFeed()
+ && LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageLocation())
- .withPlaceholderView(holder.feedTitle)
+ .withPlaceholderView(holder.feedTitle, textAndImageCombined)
.withCoverView(holder.imageView)
.load();
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
index 3e210c822..0f7c2bdd0 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java
@@ -40,8 +40,7 @@ public class DownloadActionButton extends ItemActionButton {
@Override
public int getVisibility() {
- return (item.getMedia() != null && DownloadRequester.getInstance().isDownloadingFile(item.getMedia()))
- ? View.INVISIBLE : View.VISIBLE;
+ return item.getFeed().isLocalFeed() ? View.INVISIBLE : View.VISIBLE;
}
@Override
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
index 527ac3ec1..5d95d3775 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java
@@ -42,6 +42,8 @@ public abstract class ItemActionButton {
final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media);
if (media.isCurrentlyPlaying()) {
return new PauseActionButton(item);
+ } else if (item.getFeed().isLocalFeed()) {
+ return new PlayLocalActionButton(item);
} else if (media.isDownloaded()) {
return new PlayActionButton(item);
} else if (isDownloadingMedia) {
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java
new file mode 100644
index 000000000..31dfe15da
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java
@@ -0,0 +1,51 @@
+package de.danoeh.antennapod.adapter.actionbutton;
+
+import android.content.Context;
+import androidx.annotation.AttrRes;
+import androidx.annotation.StringRes;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.UsageStatistics;
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
+import de.danoeh.antennapod.dialog.StreamingConfirmationDialog;
+
+public class PlayLocalActionButton extends ItemActionButton {
+
+ public PlayLocalActionButton(FeedItem item) {
+ super(item);
+ }
+
+ @Override
+ @StringRes
+ public int getLabel() {
+ return R.string.play_label;
+ }
+
+ @Override
+ @AttrRes
+ public int getDrawable() {
+ return R.attr.av_play;
+ }
+
+ @Override
+ public void onClick(Context context) {
+ final FeedMedia media = item.getMedia();
+ if (media == null) {
+ return;
+ }
+
+ new PlaybackServiceStarter(context, media)
+ .callEvenIfRunning(true)
+ .startWhenPrepared(true)
+ .shouldStream(true)
+ .start();
+
+ if (media.getMediaType() == MediaType.VIDEO) {
+ context.startActivity(PlaybackService.getPlayerActivityIntent(context, media));
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
index c3177668a..f2524c40c 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
@@ -51,7 +51,7 @@ public class EpisodesApplyActionFragment extends Fragment {
private static final int ACTION_MARK_UNPLAYED = 8;
public static final int ACTION_DOWNLOAD = 16;
public static final int ACTION_DELETE = 32;
- private static final int ACTION_ALL = ACTION_ADD_TO_QUEUE | ACTION_REMOVE_FROM_QUEUE
+ public static final int ACTION_ALL = ACTION_ADD_TO_QUEUE | ACTION_REMOVE_FROM_QUEUE
| ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED | ACTION_DOWNLOAD | ACTION_DELETE;
private Toolbar toolbar;
@@ -103,10 +103,6 @@ public class EpisodesApplyActionFragment extends Fragment {
);
}
- public static EpisodesApplyActionFragment newInstance(List<FeedItem> items) {
- return newInstance(items, ACTION_ALL);
- }
-
public static EpisodesApplyActionFragment newInstance(List<FeedItem> items, int actions) {
EpisodesApplyActionFragment f = new EpisodesApplyActionFragment();
f.episodes.addAll(items);
@@ -449,7 +445,7 @@ public class EpisodesApplyActionFragment extends Fragment {
// download the check episodes in the same order as they are currently displayed
List<FeedItem> toDownload = new ArrayList<>(checkedIds.size());
for (FeedItem episode : episodes) {
- if (checkedIds.contains(episode.getId()) && episode.hasMedia()) {
+ if (checkedIds.contains(episode.getId()) && episode.hasMedia() && !episode.getFeed().isLocalFeed()) {
toDownload.add(episode);
}
}
@@ -473,10 +469,8 @@ public class EpisodesApplyActionFragment extends Fragment {
}
private void close(@PluralsRes int msgId, int numItems) {
- if (numItems > 0) {
- ((MainActivity) getActivity()).showSnackbarAbovePlayer(
- getResources().getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
- }
+ ((MainActivity) getActivity()).showSnackbarAbovePlayer(
+ getResources().getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
getActivity().getSupportFragmentManager().popBackStack();
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
index 167daa08b..290bf1845 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java
@@ -17,17 +17,24 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.Fragment;
+import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.activity.OpmlImportActivity;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.util.SortOrder;
import de.danoeh.antennapod.discovery.CombinedSearcher;
import de.danoeh.antennapod.discovery.FyydPodcastSearcher;
import de.danoeh.antennapod.discovery.ItunesPodcastSearcher;
import de.danoeh.antennapod.fragment.gpodnet.GpodnetMainFragment;
+import java.util.Collections;
+
/**
* Provides actions for adding new podcast subscriptions.
*/
@@ -35,6 +42,7 @@ public class AddFeedFragment extends Fragment {
public static final String TAG = "AddFeedFragment";
private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1;
+ private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2;
private EditText combinedFeedSearchBox;
private MainActivity activity;
@@ -64,8 +72,7 @@ public class AddFeedFragment extends Fragment {
root.findViewById(R.id.btn_add_via_url).setOnClickListener(v
-> showAddViaUrlDialog());
- View butOpmlImport = root.findViewById(R.id.btn_opml_import);
- butOpmlImport.setOnClickListener(v -> {
+ root.findViewById(R.id.btn_opml_import).setOnClickListener(v -> {
try {
Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT);
intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE);
@@ -75,6 +82,15 @@ public class AddFeedFragment extends Fragment {
Log.e(TAG, "No activity found. Should never happen...");
}
});
+ root.findViewById(R.id.btn_add_local_folder).setOnClickListener(v -> {
+ try {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ startActivityForResult(intent, REQUEST_CODE_ADD_LOCAL_FOLDER);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "No activity found. Should never happen...");
+ }
+ });
root.findViewById(R.id.search_icon).setOnClickListener(view -> performSearch());
return root;
}
@@ -134,6 +150,26 @@ public class AddFeedFragment extends Fragment {
Intent intent = new Intent(getContext(), OpmlImportActivity.class);
intent.setData(uri);
startActivity(intent);
+ } else if (requestCode == REQUEST_CODE_ADD_LOCAL_FOLDER) {
+ try {
+ getActivity().getContentResolver()
+ .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), uri);
+ if (documentFile == null) {
+ throw new IllegalArgumentException("Unable to retrieve document tree");
+ }
+ Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, documentFile.getName());
+ dirFeed.setDescription(getString(R.string.local_feed_description));
+ dirFeed.setItems(Collections.emptyList());
+ dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z);
+ DBTasks.forceRefreshFeed(getContext(), dirFeed, true);
+ ((MainActivity) getActivity())
+ .showSnackbarAbovePlayer(R.string.add_local_folder_success, Snackbar.LENGTH_SHORT);
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ ((MainActivity) getActivity())
+ .showSnackbarAbovePlayer(e.getLocalizedMessage(), Snackbar.LENGTH_LONG);
+ }
}
}
}
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 6911687dd..6f95d71da 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -122,7 +122,7 @@ public class ChaptersFragment extends Fragment {
disposable = Maybe.create(emitter -> {
Playable media = controller.getMedia();
if (media != null) {
- media.loadChapterMarks();
+ media.loadChapterMarks(getContext());
emitter.onSuccess(media);
} else {
emitter.onComplete();
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
index 965cfdc86..7b66a189f 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
@@ -271,8 +271,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (!FeedMenuHandler.onOptionsItemClicked(getActivity(), item, feed)) {
switch (item.getItemId()) {
case R.id.episode_actions:
+ int actions = EpisodesApplyActionFragment.ACTION_ALL;
+ if (feed.isLocalFeed()) {
+ // turn off download and delete actions for local feed
+ actions ^= EpisodesApplyActionFragment.ACTION_DOWNLOAD;
+ actions ^= EpisodesApplyActionFragment.ACTION_DELETE;
+ }
EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment
- .newInstance(feed.getItems());
+ .newInstance(feed.getItems(), actions);
((MainActivity)getActivity()).loadChildFragment(fragment);
return true;
case R.id.rename_item:
@@ -287,9 +293,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
}
};
+ int messageId = feed.isLocalFeed() ? R.string.feed_delete_confirmation_local_msg
+ : R.string.feed_delete_confirmation_msg;
ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(),
R.string.remove_feed_label,
- getString(R.string.feed_delete_confirmation_msg, feed.getTitle())) {
+ getString(messageId, feed.getTitle())) {
@Override
public void onConfirmButtonPressed(
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
index 337c789fe..669dbdac2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -39,6 +39,7 @@ import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton;
import de.danoeh.antennapod.adapter.actionbutton.MarkAsPlayedActionButton;
import de.danoeh.antennapod.adapter.actionbutton.PauseActionButton;
import de.danoeh.antennapod.adapter.actionbutton.PlayActionButton;
+import de.danoeh.antennapod.adapter.actionbutton.PlayLocalActionButton;
import de.danoeh.antennapod.adapter.actionbutton.StreamActionButton;
import de.danoeh.antennapod.adapter.actionbutton.VisitWebsiteActionButton;
import de.danoeh.antennapod.core.event.DownloadEvent;
@@ -326,6 +327,8 @@ public class ItemFragment extends Fragment {
}
if (media.isCurrentlyPlaying()) {
actionButton1 = new PauseActionButton(item);
+ } else if (item.getFeed().isLocalFeed()) {
+ actionButton1 = new PlayLocalActionButton(item);
} else if (media.isDownloaded()) {
actionButton1 = new PlayActionButton(item);
} else {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
index 8b7d2b886..8746793be 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java
@@ -200,9 +200,11 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
}
}
};
+ int messageId = feed.isLocalFeed() ? R.string.feed_delete_confirmation_local_msg
+ : R.string.feed_delete_confirmation_msg;
ConfirmationDialog conDialog = new ConfirmationDialog(getContext(),
R.string.remove_feed_label,
- getString(R.string.feed_delete_confirmation_msg, feed.getTitle())) {
+ getString(messageId, feed.getTitle())) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
dialog.dismiss();
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
index 33e4cb764..7d6a3dc40 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java
@@ -293,7 +293,9 @@ public class SubscriptionFragment extends Fragment {
}
};
- String message = getString(R.string.feed_delete_confirmation_msg, feed.getTitle());
+ int messageId = feed.isLocalFeed() ? R.string.feed_delete_confirmation_local_msg
+ : R.string.feed_delete_confirmation_msg;
+ String message = getString(messageId, feed.getTitle());
ConfirmationDialog dialog = new ConfirmationDialog(getContext(), R.string.remove_feed_label, message) {
@Override
public void onConfirmButtonPressed(DialogInterface clickedDialog) {
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
index bddafb75e..06b1c55bc 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -64,6 +64,9 @@ public class FeedItemMenuHandler {
if (!ShareUtils.hasLinkToShare(selectedItem)) {
setItemVisibility(menu, R.id.visit_website_item, false);
}
+ if (selectedItem.getFeed().isLocalFeed()) {
+ setItemVisibility(menu, R.id.visit_website_item, false);
+ }
boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists();
diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml
index d44552bab..297c5d812 100644
--- a/app/src/main/res/layout/addfeed.xml
+++ b/app/src/main/res/layout/addfeed.xml
@@ -101,6 +101,20 @@
android:text="@string/add_podcast_by_url"/>
<TextView
+ android:id="@+id/btn_add_local_folder"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="8dp"
+ app:drawableStartCompat="?attr/ic_folder"
+ app:drawableLeftCompat="?attr/ic_folder"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:textColor="?android:attr/textColorPrimary"
+ android:clickable="true"
+ android:text="@string/add_local_folder"/>
+
+ <TextView
android:id="@+id/btn_search_itunes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/core/build.gradle b/core/build.gradle
index 4c7ef5a0a..1f9079e81 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -57,6 +57,7 @@ android {
dependencies {
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.media:media:$mediaVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.work:work-runtime:$workManagerVersion"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
index 0889e5182..9c1c55d76 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
@@ -24,6 +24,7 @@ public class Feed extends FeedFile implements ImageResource {
public static final int FEEDFILETYPE_FEED = 0;
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_ATOM1 = "atom";
+ public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";
/* title as defined by the feed */
private String feedTitle;
@@ -551,4 +552,7 @@ public class Feed extends FeedFile implements ImageResource {
this.lastUpdateFailed = lastUpdateFailed;
}
+ public boolean isLocalFeed() {
+ return download_url.startsWith(PREFIX_LOCAL_FOLDER);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
index b681c21d1..131cbe563 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
@@ -381,7 +381,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
if (imageUrl != null) {
return imageUrl;
} else if (media != null && media.hasEmbeddedPicture()) {
- return media.getLocalMediaUrl();
+ return FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER + media.getLocalMediaUrl();
} else if (feed != null) {
return feed.getImageLocation();
} else {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index d1c90cfa7..92e45376a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -32,6 +32,7 @@ public class FeedMedia extends FeedFile implements Playable {
public static final int FEEDFILETYPE_FEEDMEDIA = 2;
public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
+ public static final String FILENAME_PREFIX_EMBEDDED_COVER = "metadata-retriever:";
public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
private static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
@@ -375,7 +376,7 @@ public class FeedMedia extends FeedFile implements Playable {
}
@Override
- public void loadChapterMarks() {
+ public void loadChapterMarks(Context context) {
if (item == null && itemID != 0) {
item = DBReader.getFeedItem(itemID);
}
@@ -386,10 +387,10 @@ public class FeedMedia extends FeedFile implements Playable {
if (item.hasChapters()) {
DBReader.loadChaptersOfFeedItem(item);
} else {
- if(localFileAvailable()) {
+ if (localFileAvailable()) {
ChapterUtils.loadChaptersFromFileUrl(this);
} else {
- ChapterUtils.loadChaptersFromStreamUrl(this);
+ ChapterUtils.loadChaptersFromStreamUrl(this, context);
}
if (item.getChapters() != null) {
DBWriter.setFeedItem(item);
@@ -557,7 +558,7 @@ public class FeedMedia extends FeedFile implements Playable {
if (item != null) {
return item.getImageLocation();
} else if (hasEmbeddedPicture()) {
- return getLocalMediaUrl();
+ return FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl();
} else {
return null;
}
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
new file mode 100644
index 000000000..706aed2c3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -0,0 +1,151 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.documentfile.provider.DocumentFile;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.DownloadError;
+
+public class LocalFeedUpdater {
+
+ public static void updateFeed(Feed feed, Context context) {
+ String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, "");
+ DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
+ if (documentFolder == null) {
+ reportError(feed, "Unable to retrieve document tree");
+ return;
+ }
+ if (!documentFolder.exists() || !documentFolder.canRead()) {
+ reportError(feed, "Cannot read local directory");
+ return;
+ }
+
+ if (feed.getItems() == null) {
+ feed.setItems(new ArrayList<>());
+ }
+ //make sure it is the latest 'version' of this feed from the db (all items etc)
+ feed = DBTasks.updateFeed(context, feed, false);
+
+ // list files in feed folder
+ List<DocumentFile> mediaFiles = new ArrayList<>();
+ Set<String> mediaFileNames = new HashSet<>();
+ for (DocumentFile file : documentFolder.listFiles()) {
+ String mime = file.getType();
+ if (mime != null && (mime.startsWith("audio/") || mime.startsWith("video/"))) {
+ mediaFiles.add(file);
+ mediaFileNames.add(file.getName());
+ }
+ }
+
+ // add new files to feed and update item data
+ List<FeedItem> newItems = feed.getItems();
+ for (DocumentFile f : mediaFiles) {
+ FeedItem oldItem = feedContainsFile(feed, f.getName());
+ FeedItem newItem = createFeedItem(feed, f, context);
+ if (oldItem == null) {
+ newItems.add(newItem);
+ } else {
+ oldItem.updateFromOther(newItem);
+ }
+ }
+
+ // remove feed items without corresponding file
+ Iterator<FeedItem> it = newItems.iterator();
+ while (it.hasNext()) {
+ FeedItem feedItem = it.next();
+ if (!mediaFileNames.contains(feedItem.getLink())) {
+ it.remove();
+ }
+ }
+
+ List<String> iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png");
+ for (String iconLocation : iconLocations) {
+ DocumentFile image = documentFolder.findFile(iconLocation);
+ if (image != null) {
+ feed.setImageUrl(image.getUri().toString());
+ break;
+ }
+ }
+ if (StringUtils.isBlank(feed.getImageUrl())) {
+ // set default feed image
+ feed.setImageUrl(getDefaultIconUrl(context));
+ }
+
+ // update items, delete items without existing file;
+ // only delete items if the folder contains at least one element to avoid accidentally
+ // deleting played state or position in case the folder is temporarily unavailable.
+ boolean removeUnlistedItems = (newItems.size() >= 1);
+ DBTasks.updateFeed(context, feed, removeUnlistedItems);
+ }
+
+ /**
+ * 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;
+ }
+
+ private static FeedItem feedContainsFile(Feed feed, String filename) {
+ List<FeedItem> items = feed.getItems();
+ for (FeedItem i : items) {
+ if (i.getMedia() != null && i.getLink().equals(filename)) {
+ return i;
+ }
+ }
+ return null;
+ }
+
+ private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) {
+ String uuid = UUID.randomUUID().toString();
+ FeedItem item = new FeedItem(0, file.getName(), uuid, file.getName(), new Date(),
+ FeedItem.UNPLAYED, feed);
+ item.setAutoDownload(false);
+
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ mediaMetadataRetriever.setDataSource(context, file.getUri());
+ String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (!TextUtils.isEmpty(title)) {
+ item.setTitle(title);
+ }
+
+ //add the media to the item
+ long duration = Long.parseLong(durationStr);
+ long size = file.length();
+ FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(),
+ file.getUri().toString(), file.getUri().toString(), false, null, 0, 0);
+ media.setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
+ item.setMedia(media);
+
+ return item;
+ }
+
+ private static void reportError(Feed feed, String reasonDetailed) {
+ DownloadStatus status = new DownloadStatus(feed, feed.getTitle(),
+ DownloadError.ERROR_IO_ERROR, false, reasonDetailed, true);
+ DBWriter.addDownloadStatus(status);
+ DBWriter.setFeedLastUpdateFailed(feed.getId(), true);
+ }
+}
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 50511526f..ab4247cef 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
@@ -9,6 +9,8 @@ import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
+import com.bumptech.glide.load.model.StringLoader;
+import com.bumptech.glide.load.model.UriLoader;
import com.bumptech.glide.module.AppGlideModule;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
@@ -33,7 +35,10 @@ 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.replace(String.class, InputStream.class, new MetadataRetrieverLoader.Factory(context));
+ registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
+ registry.append(String.class, InputStream.class, new StringLoader.StreamFactory());
+
registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
index 071b1d0c9..8c80e9151 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.glide;
+import android.content.ContentResolver;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -22,7 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
/**
- * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
+ * {@see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader}.
*/
class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@@ -52,14 +53,7 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
* Constructor for a new Factory that runs requests using a static singleton client.
*/
Factory() {
- this(getInternalClient());
- }
-
- /**
- * Constructor for a new Factory that runs requests using given client.
- */
- Factory(OkHttpClient client) {
- this.client = client;
+ this.client = getInternalClient();
}
@NonNull
@@ -83,19 +77,15 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@Nullable
@Override
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
- if (TextUtils.isEmpty(model)) {
- return null;
- } else if (model.startsWith("/")) {
- return new LoadData<>(new ObjectKey(model), new AudioCoverFetcher(model));
- } else {
- GlideUrl url = new GlideUrl(model);
- return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, url));
- }
+ return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, new GlideUrl(model)));
}
@Override
- public boolean handles(@NonNull String s) {
- return true;
+ public boolean handles(@NonNull String model) {
+ // Leave content URIs to Glide's default loaders
+ return !TextUtils.isEmpty(model)
+ && !model.startsWith(ContentResolver.SCHEME_CONTENT)
+ && !model.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE);
}
private static class NetworkAllowanceInterceptor implements Interceptor {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
index 6a237573b..b6b607904 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
@@ -1,7 +1,10 @@
package de.danoeh.antennapod.core.glide;
+import android.content.ContentResolver;
+import android.content.Context;
import android.media.MediaMetadataRetriever;
+import android.net.Uri;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
@@ -17,16 +20,22 @@ class AudioCoverFetcher implements DataFetcher<InputStream> {
private static final String TAG = "AudioCoverFetcher";
private final String path;
+ private final Context context;
- public AudioCoverFetcher(String path) {
+ public AudioCoverFetcher(String path, Context context) {
this.path = path;
+ this.context = context;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
- retriever.setDataSource(path);
+ if (path.startsWith(ContentResolver.SCHEME_CONTENT)) {
+ retriever.setDataSource(context, Uri.parse(path));
+ } else {
+ retriever.setDataSource(path);
+ }
byte[] picture = retriever.getEmbeddedPicture();
if (picture != null) {
callback.onDataReady(new ByteArrayInputStream(picture));
@@ -41,6 +50,7 @@ class AudioCoverFetcher implements DataFetcher<InputStream> {
@Override public void cleanup() {
// nothing to clean up
}
+
@Override public void cancel() {
// cannot cancel
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java
new file mode 100644
index 000000000..baa06e722
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.load.Options;
+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.FeedMedia;
+
+import java.io.InputStream;
+
+class MetadataRetrieverLoader implements ModelLoader<String, InputStream> {
+
+ /**
+ * The default factory for {@link MetadataRetrieverLoader}s.
+ */
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+ private final Context context;
+
+ Factory(Context context) {
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new MetadataRetrieverLoader(context);
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+ }
+
+ private final Context context;
+
+ private MetadataRetrieverLoader(Context context) {
+ this.context = context;
+ }
+
+ @Nullable
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model,
+ int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model),
+ new AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
index 8be3d2980..483a2aa56 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
@@ -30,8 +30,7 @@ public class FeedSyncTask {
return false;
}
- Feed[] savedFeeds = DBTasks.updateFeed(context, result.feed);
- Feed savedFeed = savedFeeds[0];
+ Feed savedFeed = DBTasks.updateFeed(context, result.feed, false);
// If loadAllPages=true, check if another page is available and queue it for download
final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES);
final Feed feed = result.feed;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
index 883ba6023..55212cd46 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
@@ -311,7 +311,7 @@ public class PlaybackServiceTaskManager {
if (media.getChapters() == null) {
Completable.create(emitter -> {
- media.loadChapterMarks();
+ media.loadChapterMarks(context);
emitter.onComplete();
})
.subscribeOn(Schedulers.io())
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
index 16e2825b4..9359774e9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -16,6 +16,7 @@ import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.sync.SyncService;
@@ -241,7 +242,12 @@ public final class DBTasks {
feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
}
f.setId(feed.getId());
- DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+
+ if (f.isLocalFeed()) {
+ new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start();
+ } else {
+ DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+ }
}
/**
@@ -367,118 +373,133 @@ public final class DBTasks {
* <p/>
* This method should NOT be executed on the GUI thread.
*
- * @param context Used for accessing the DB.
- * @param newFeeds The new Feed objects.
- * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise.
+ * @param context Used for accessing the DB.
+ * @param newFeed The new Feed object.
+ * @param removeUnlistedItems The item list in the new Feed object is considered to be exhaustive.
+ * I.e. items are removed from the database if they are not in this item list.
+ * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise.
*/
- public static synchronized Feed[] updateFeed(final Context context,
- final Feed... newFeeds) {
- List<Feed> newFeedsList = new ArrayList<>();
- List<Feed> updatedFeedsList = new ArrayList<>();
- Feed[] resultFeeds = new Feed[newFeeds.length];
+ public static synchronized Feed updateFeed(Context context, Feed newFeed, boolean removeUnlistedItems) {
+ Feed resultFeed;
+ List<FeedItem> unlistedItems = new ArrayList<>();
+
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) {
+ // Look up feed in the feedslist
+ final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ if (savedFeed == null) {
+ Log.d(TAG, "Found no existing Feed with title "
+ + newFeed.getTitle() + ". Adding as new one.");
+
+ // Add a new Feed
+ // all new feeds will have the most recent item marked as unplayed
+ FeedItem mostRecent = newFeed.getMostRecentItem();
+ if (mostRecent != null) {
+ mostRecent.setNew();
+ }
+
+ resultFeed = newFeed;
+ } else {
+ Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ + " already exists. Syncing new with existing one.");
- final Feed newFeed = newFeeds[feedIdx];
+ Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
- // Look up feed in the feedslist
- final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter,
- newFeed);
- if (savedFeed == null) {
- Log.d(TAG, "Found no existing Feed with title "
- + newFeed.getTitle() + ". Adding as new one.");
-
- // Add a new Feed
- // all new feeds will have the most recent item marked as unplayed
- FeedItem mostRecent = newFeed.getMostRecentItem();
- if (mostRecent != null) {
- mostRecent.setNew();
+ if (newFeed.getPageNr() == savedFeed.getPageNr()) {
+ if (savedFeed.compareWithOther(newFeed)) {
+ Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes");
+ savedFeed.updateFromOther(newFeed);
}
-
- newFeedsList.add(newFeed);
- resultFeeds[feedIdx] = newFeed;
} else {
- Log.d(TAG, "Feed with title " + newFeed.getTitle()
- + " already exists. Syncing new with existing one.");
+ Log.d(TAG, "New feed has a higher page number.");
+ savedFeed.setNextPageLink(newFeed.getNextPageLink());
+ }
+ if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
+ Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
+ savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
+ }
- Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
+ // get the most recent date now, before we start changing the list
+ FeedItem priorMostRecent = savedFeed.getMostRecentItem();
+ Date priorMostRecentDate = null;
+ if (priorMostRecent != null) {
+ priorMostRecentDate = priorMostRecent.getPubDate();
+ }
- if (newFeed.getPageNr() == savedFeed.getPageNr()) {
- if (savedFeed.compareWithOther(newFeed)) {
- Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes");
- savedFeed.updateFromOther(newFeed);
+ // Look for new or updated Items
+ for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
+ final FeedItem item = newFeed.getItems().get(idx);
+ FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, item.getIdentifyingValue());
+ if (oldItem == null) {
+ // item is new
+ item.setFeed(savedFeed);
+ item.setAutoDownload(savedFeed.getPreferences().getAutoDownload());
+
+ if (idx >= savedFeed.getItems().size()) {
+ savedFeed.getItems().add(item);
+ } else {
+ savedFeed.getItems().add(idx, item);
}
- } else {
- Log.d(TAG, "New feed has a higher page number.");
- savedFeed.setNextPageLink(newFeed.getNextPageLink());
- }
- if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
- Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
- savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
- }
- // get the most recent date now, before we start changing the list
- FeedItem priorMostRecent = savedFeed.getMostRecentItem();
- Date priorMostRecentDate = null;
- if (priorMostRecent != null) {
- priorMostRecentDate = priorMostRecent.getPubDate();
+ // only mark the item new if it was published after or at the same time
+ // as the most recent item
+ // (if the most recent date is null then we can assume there are no items
+ // and this is the first, hence 'new')
+ if (priorMostRecentDate == null
+ || priorMostRecentDate.before(item.getPubDate())
+ || priorMostRecentDate.equals(item.getPubDate())) {
+ Log.d(TAG, "Marking item published on " + item.getPubDate()
+ + " new, prior most recent date = " + priorMostRecentDate);
+ item.setNew();
+ }
+ } else {
+ oldItem.updateFromOther(item);
}
+ }
- // Look for new or updated Items
- for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
- final FeedItem item = newFeed.getItems().get(idx);
- FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed,
- item.getIdentifyingValue());
- if (oldItem == null) {
- // item is new
- item.setFeed(savedFeed);
- item.setAutoDownload(savedFeed.getPreferences().getAutoDownload());
-
- if (idx >= savedFeed.getItems().size()) {
- savedFeed.getItems().add(item);
- } else {
- savedFeed.getItems().add(idx, item);
- }
-
- // only mark the item new if it was published after or at the same time
- // as the most recent item
- // (if the most recent date is null then we can assume there are no items
- // and this is the first, hence 'new')
- if (priorMostRecentDate == null
- || priorMostRecentDate.before(item.getPubDate())
- || priorMostRecentDate.equals(item.getPubDate())) {
- Log.d(TAG, "Marking item published on " + item.getPubDate()
- + " new, prior most recent date = " + priorMostRecentDate);
- item.setNew();
- }
- } else {
- oldItem.updateFromOther(item);
+ // identify items to be removed
+ if (removeUnlistedItems) {
+ Iterator<FeedItem> it = savedFeed.getItems().iterator();
+ while (it.hasNext()) {
+ FeedItem feedItem = it.next();
+ if (searchFeedItemByIdentifyingValue(newFeed, feedItem.getIdentifyingValue()) == null) {
+ unlistedItems.add(feedItem);
+ it.remove();
}
}
- // update attributes
- savedFeed.setLastUpdate(newFeed.getLastUpdate());
- savedFeed.setType(newFeed.getType());
- savedFeed.setLastUpdateFailed(false);
-
- updatedFeedsList.add(savedFeed);
- resultFeeds[feedIdx] = savedFeed;
}
+
+ // update attributes
+ savedFeed.setLastUpdate(newFeed.getLastUpdate());
+ savedFeed.setType(newFeed.getType());
+ savedFeed.setLastUpdateFailed(false);
+
+ resultFeed = savedFeed;
}
adapter.close();
try {
- DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[0])).get();
- DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[0])).get();
+ if (savedFeed == null) {
+ DBWriter.addNewFeed(context, newFeed).get();
+ } else {
+ DBWriter.setCompleteFeed(savedFeed).get();
+ }
+ if (removeUnlistedItems) {
+ DBWriter.deleteFeedItems(context, unlistedItems).get();
+ }
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- EventBus.getDefault().post(new FeedListUpdateEvent(updatedFeedsList));
+ if (savedFeed != null) {
+ EventBus.getDefault().post(new FeedListUpdateEvent(savedFeed));
+ } else {
+ EventBus.getDefault().post(new FeedListUpdateEvent(Collections.emptyList()));
+ }
- return resultFeeds;
+ return resultFeed;
}
/**
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
index e33b67719..9e6041df3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
@@ -141,48 +141,74 @@ public class DBWriter {
return dbExec.submit(() -> {
DownloadRequester requester = DownloadRequester.getInstance();
final Feed feed = DBReader.getFeed(feedId);
+ if (feed == null) {
+ return;
+ }
- if (feed != null) {
- // delete stored media files and mark them as read
- List<FeedItem> queue = DBReader.getQueue();
- List<FeedItem> removed = new ArrayList<>();
- if (feed.getItems() == null) {
- DBReader.getFeedItemList(feed);
- }
+ // delete stored media files and mark them as read
+ if (feed.getItems() == null) {
+ DBReader.getFeedItemList(feed);
+ }
+ deleteFeedItemsSynchronous(context, feed.getItems());
- for (FeedItem item : feed.getItems()) {
- if (queue.remove(item)) {
- removed.add(item);
- }
- if (item.getMedia() != null && item.getMedia().isDownloaded()) {
- deleteFeedMediaSynchronous(context, item.getMedia());
- } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) {
- requester.cancelDownload(context, item.getMedia());
- }
- }
- PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- if (removed.size() > 0) {
- adapter.setQueue(queue);
- for (FeedItem item : removed) {
- EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
- }
- }
- adapter.removeFeed(feed);
- adapter.close();
+ // delete feed
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.removeFeed(feed);
+ adapter.close();
- SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
- EventBus.getDefault().post(new FeedListUpdateEvent(feed));
+ SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
+ EventBus.getDefault().post(new FeedListUpdateEvent(feed));
+ });
+ }
- // we assume we also removed download log entries for the feed or its media files.
- // especially important if download or refresh failed, as the user should not be able
- // to retry these
- EventBus.getDefault().post(DownloadLogEvent.listUpdated());
+ /**
+ * Remove the listed items and their FeedMedia entries.
+ * Deleting media also removes the download log entries.
+ */
+ @NonNull
+ public static Future<?> deleteFeedItems(@NonNull Context context, @NonNull List<FeedItem> items) {
+ return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items));
+ }
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
+ /**
+ * Remove the listed items and their FeedMedia entries.
+ * Deleting media also removes the download log entries.
+ */
+ private static void deleteFeedItemsSynchronous(@NonNull Context context, @NonNull List<FeedItem> items) {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ List<FeedItem> queue = DBReader.getQueue();
+ List<FeedItem> removedFromQueue = new ArrayList<>();
+ for (FeedItem item : items) {
+ if (queue.remove(item)) {
+ removedFromQueue.add(item);
}
- });
+ if (item.getMedia() != null && item.getMedia().isDownloaded()) {
+ deleteFeedMediaSynchronous(context, item.getMedia());
+ } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) {
+ requester.cancelDownload(context, item.getMedia());
+ }
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ if (!removedFromQueue.isEmpty()) {
+ adapter.setQueue(queue);
+ }
+ adapter.removeFeedItems(items);
+ adapter.close();
+
+ for (FeedItem item : removedFromQueue) {
+ EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
+ }
+
+ // we assume we also removed download log entries for the feed or its media files.
+ // especially important if download or refresh failed, as the user should not be able
+ // to retry these
+ EventBus.getDefault().post(DownloadLogEvent.listUpdated());
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
}
/**
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 8a3f44e18..142763d75 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -14,6 +14,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.io.FileUtils;
@@ -852,6 +853,23 @@ public class PodDBAdapter {
}
/**
+ * Remove the listed items and their FeedMedia entries.
+ */
+ public void removeFeedItems(@NonNull List<FeedItem> items) {
+ try {
+ db.beginTransactionNonExclusive();
+ for (FeedItem item : items) {
+ removeFeedItem(item);
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
* Remove a feed with all its FeedItems and Media entries.
*/
public void removeFeed(Feed feed) {
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 737f902b7..859666464 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
@@ -1,12 +1,12 @@
package de.danoeh.antennapod.core.util;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.util.Log;
import java.net.URLConnection;
-import java.util.zip.CheckedOutputStream;
-
import de.danoeh.antennapod.core.ClientConfig;
import org.apache.commons.io.IOUtils;
@@ -52,10 +52,10 @@ public class ChapterUtils {
return chapters.size() - 1;
}
- public static void loadChaptersFromStreamUrl(Playable media) {
- ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media);
+ public static void loadChaptersFromStreamUrl(Playable media, Context context) {
+ ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media, context);
if (media.getChapters() == null) {
- ChapterUtils.readOggChaptersFromPlayableStreamUrl(media);
+ ChapterUtils.readOggChaptersFromPlayableStreamUrl(media, context);
}
}
@@ -74,7 +74,7 @@ public class ChapterUtils {
* Uses the download URL of a media object of a feeditem to read its ID3
* chapters.
*/
- private static void readID3ChaptersFromPlayableStreamUrl(Playable p) {
+ private static void readID3ChaptersFromPlayableStreamUrl(Playable p, Context context) {
if (p == null || p.getStreamUrl() == null) {
Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null");
return;
@@ -82,16 +82,21 @@ public class ChapterUtils {
Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
CountingInputStream in = null;
try {
- URL url = new URL(p.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- in = new CountingInputStream(urlConnection.getInputStream());
+ if (p.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
+ Uri uri = Uri.parse(p.getStreamUrl());
+ in = new CountingInputStream(context.getContentResolver().openInputStream(uri));
+ } else {
+ URL url = new URL(p.getStreamUrl());
+ URLConnection urlConnection = url.openConnection();
+ urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
+ in = new CountingInputStream(urlConnection.getInputStream());
+ }
List<Chapter> chapters = readChaptersFrom(in);
if (!chapters.isEmpty()) {
p.setChapters(chapters);
}
Log.i(TAG, "Chapters loaded");
- } catch (IOException | ID3ReaderException e) {
+ } catch (IOException | ID3ReaderException | IllegalArgumentException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(in);
@@ -147,20 +152,25 @@ public class ChapterUtils {
return chapters;
}
- private static void readOggChaptersFromPlayableStreamUrl(Playable media) {
+ private static void readOggChaptersFromPlayableStreamUrl(Playable media, Context context) {
if (media == null || !media.streamAvailable()) {
return;
}
InputStream input = null;
try {
- URL url = new URL(media.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- input = urlConnection.getInputStream();
+ if (media.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
+ Uri uri = Uri.parse(media.getStreamUrl());
+ input = context.getContentResolver().openInputStream(uri);
+ } else {
+ URL url = new URL(media.getStreamUrl());
+ URLConnection urlConnection = url.openConnection();
+ urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
+ input = urlConnection.getInputStream();
+ }
if (input != null) {
readOggChaptersFromInputStream(media, input);
}
- } catch (IOException e) {
+ } catch (IOException | IllegalArgumentException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(input);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
index b55091009..81937b62e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
@@ -103,7 +103,7 @@ public class ExternalMedia implements Playable {
}
@Override
- public void loadChapterMarks() {
+ public void loadChapterMarks(Context context) {
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 24aabf212..bab8459a4 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -4,11 +4,8 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
import android.preference.PreferenceManager;
-import androidx.annotation.Nullable;
import android.util.Log;
-
-import java.util.List;
-
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.asynctask.ImageResource;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedMedia;
@@ -17,6 +14,8 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.ShownotesProvider;
+import java.util.List;
+
/**
* Interface for objects that can be played by the PlaybackService.
*/
@@ -44,7 +43,7 @@ public interface Playable extends Parcelable,
* Playable objects should load their chapter marks in this method if no
* local file was available when loadMetadata() was called.
*/
- void loadChapterMarks();
+ void loadChapterMarks(Context context);
/**
* Returns the title of the episode that this playable represents
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
index ca09cda4b..669279294 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
@@ -129,8 +129,8 @@ public class RemoteMedia implements Playable {
}
@Override
- public void loadChapterMarks() {
- ChapterUtils.loadChaptersFromStreamUrl(this);
+ public void loadChapterMarks(Context context) {
+ ChapterUtils.loadChaptersFromStreamUrl(this, context);
}
@Override
diff --git a/core/src/main/res/raw/local_feed_default_icon.png b/core/src/main/res/raw/local_feed_default_icon.png
new file mode 100644
index 000000000..c1b24a729
--- /dev/null
+++ b/core/src/main/res/raw/local_feed_default_icon.png
Binary files differ
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index a55378931..ed0b239a5 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -10,6 +10,7 @@
<color name="download_failed_red">#B00020</color>
<color name="image_readability_tint">#80000000</color>
<color name="feed_image_bg">#50000000</color>
+ <color name="feed_text_bg">#ccbfbfbf</color>
<!-- Theme colors -->
<color name="background_light">#FFFFFF</color>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 1059a55c1..ef4602558 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -146,6 +146,7 @@
<string name="share_item_url_label">Share Media File URL</string>
<string name="share_item_url_with_position_label">Share Media File URL with Position</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes).</string>
+ <string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted.</string>
<string name="feed_remover_msg">Removing podcast</string>
<string name="load_complete_feed">Refresh complete podcast</string>
<string name="multi_select">Multi select</string>
@@ -721,6 +722,9 @@
<string name="discover">Discover</string>
<string name="discover_more">more ยป</string>
<string name="search_powered_by">Search powered by %1$s</string>
+ <string name="add_local_folder">Add local folder</string>
+ <string name="add_local_folder_success">Adding local folder succeeded</string>
+ <string name="local_feed_description">This virtual podcast was created by adding a folder to AntennaPod.</string>
<string name="filter">Filter</string>