summaryrefslogtreecommitdiff
path: root/core/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main')
-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.java161
-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/PlaybackService.java9
-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.java199
-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.java33
-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
21 files changed, 509 insertions, 183 deletions
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 3657bcdc6..a3b66c951 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..7ebb8633b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -0,0 +1,161 @@
+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 java.util.concurrent.ExecutionException;
+
+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));
+ }
+ if (feed.getPreferences().getAutoDownload()) {
+ feed.getPreferences().setAutoDownload(false);
+ feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
+ try {
+ DBWriter.setFeedPreferences(feed.getPreferences()).get();
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // 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/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index 8677ea030..9f53c6da0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -37,6 +37,7 @@ import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
+import android.webkit.URLUtil;
import android.widget.Toast;
import com.bumptech.glide.Glide;
@@ -496,7 +497,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (allowStreamAlways) {
UserPreferences.setAllowMobileStreaming(true);
}
- if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime) {
+ boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
+ if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime && !localFeed) {
displayStreamingNotAllowedNotification(intent);
PlaybackPreferences.writeNoMediaPlaying();
stateManager.stopService();
@@ -682,7 +684,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private void startPlayingFromPreferences() {
Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
if (playable != null) {
- if (PlaybackPreferences.getCurrentEpisodeIsStream() && !NetworkUtils.isStreamingAllowed()) {
+ boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
+ if (PlaybackPreferences.getCurrentEpisodeIsStream() && !NetworkUtils.isStreamingAllowed() && !localFeed) {
displayStreamingNotAllowedNotification(
new PlaybackServiceStarter(this, playable)
.prepareImmediately(true)
@@ -971,7 +974,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
if (!nextItem.getMedia().localFileAvailable() && !NetworkUtils.isStreamingAllowed()
- && UserPreferences.isFollowQueue()) {
+ && UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) {
displayStreamingNotAllowedNotification(
new PlaybackServiceStarter(this, nextItem.getMedia())
.prepareImmediately(true)
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 1cf665f12..05d64ea3e 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
@@ -303,7 +303,7 @@ public class PlaybackServiceTaskManager {
if (media.getChapters() == null) {
chapterLoaderFuture = 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 4f2417b7d..c059e696a 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);
+ }
}
/**
@@ -366,118 +372,135 @@ 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();
+ }
- final Feed newFeed = newFeeds[feedIdx];
+ resultFeed = newFeed;
+ } else {
+ Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ + " already exists. Syncing new with existing one.");
- // 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();
- }
+ Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
- newFeedsList.add(newFeed);
- resultFeeds[feedIdx] = newFeed;
+ 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);
+ }
} 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;
}
- }
- adapter.close();
+ // update attributes
+ savedFeed.setLastUpdate(newFeed.getLastUpdate());
+ savedFeed.setType(newFeed.getType());
+ savedFeed.setLastUpdateFailed(false);
+
+ resultFeed = savedFeed;
+ }
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();
+ // Update with default values that are set in database
+ resultFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ } else {
+ DBWriter.setCompleteFeed(savedFeed).get();
+ }
+ if (removeUnlistedItems) {
+ DBWriter.deleteFeedItems(context, unlistedItems).get();
+ }
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- EventBus.getDefault().post(new FeedListUpdateEvent(updatedFeedsList));
+ adapter.close();
+
+ 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 775485880..935b06cd6 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;
@@ -359,6 +360,21 @@ public class PodDBAdapter {
// do nothing
}
+ /**
+ * <p>Resets all database connections to ensure new database connections for
+ * the next test case. Call method only for unit tests.</p>
+ *
+ * <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
+ * that leads to an error <tt>IllegalStateException: Illegal connection
+ * pointer</tt> if several threads try to use the same database connection.
+ * For more information see
+ * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
+ */
+ public static void tearDownTests() {
+ db = null;
+ SingletonHolder.dbHelper.close();
+ }
+
public static boolean deleteDatabase() {
PodDBAdapter adapter = getInstance();
adapter.open();
@@ -859,6 +875,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 c44c0f925..5b15913c8 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 androidx.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 187f3725c..d09f53d64 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 7f3c40e75..8cb369961 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -150,6 +150,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>
@@ -738,6 +739,9 @@
<string name="discover_more">more ยป</string>
<string name="discover_powered_by_itunes">Suggestions by iTunes</string>
<string name="search_powered_by">Results 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>