From 382a54028029fe6297ebab405010d7e5229331d4 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 11:34:01 +0200 Subject: Basic local feeds support Co-authored-by: Igor Almeida --- .../antennapod/fragment/AddFeedFragment.java | 38 ++++++- app/src/main/res/layout/addfeed.xml | 14 +++ .../java/de/danoeh/antennapod/core/feed/Feed.java | 4 + .../antennapod/core/feed/LocalFeedUpdater.java | 111 +++++++++++++++++++++ .../de/danoeh/antennapod/core/storage/DBTasks.java | 8 +- core/src/main/res/values/strings.xml | 3 + 6 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java 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 546684f14..d29a36871 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -14,16 +14,22 @@ import android.view.ViewGroup; import android.widget.EditText; 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.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. */ @@ -31,6 +37,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; @@ -57,8 +64,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); @@ -68,6 +74,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; } @@ -127,6 +142,25 @@ 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()); + 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/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index 9d14d209a..7430579b4 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -100,6 +100,20 @@ android:clickable="true" android:text="@string/add_podcast_by_url"/> + + ()); + } + //make sure it is the latest 'version' of this feed from the db (all items etc) + feed = DBTasks.updateFeed(context, feed)[0]; + + List mediaFiles = new ArrayList<>(); + for (DocumentFile file : documentFolder.listFiles()) { + String mime = file.getType(); + if (mime != null && (mime.startsWith("audio/") || mime.startsWith("video/"))) { + mediaFiles.add(file); + } + } + + List newItems = feed.getItems(); + for (DocumentFile f : mediaFiles) { + FeedItem found = feedContainsFile(feed, f.getName()); + if (found != null) { + //TODO make sure the media has not changed (type, duration) + } else { + FeedItem item = createFeedItem(feed, f, context); + newItems.add(item); + } + } + + List 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; + } + } + + DBTasks.updateFeed(context, feed); + } + + private static FeedItem feedContainsFile(Feed feed, String filename) { + List 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) { + //create item + long globalId = 0; + Date date = new Date(); + String uuid = UUID.randomUUID().toString(); + FeedItem item = new FeedItem(globalId, file.getName(), uuid, file.getName(), date, FeedItem.UNPLAYED, feed); + item.setAutoDownload(false); + + //add the media to the item + long duration = getFileDuration(file, context); + long size = file.length(); + FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(), + file.getUri().toString(), file.getUri().toString(), true, null, 0, 0); + 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); + } + + private static long getFileDuration(DocumentFile f, Context context) { + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(context, f.getUri()); + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + return Long.parseLong(durationStr); + } +} 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..323e34b6a 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); + } } /** diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 2827f666e..4f94a141a 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -718,6 +718,9 @@ Discover more ยป Search powered by %1$s + Add local folder + Adding local folder succeeded + This virtual podcast was created by adding a folder to AntennaPod. Filter -- cgit v1.2.3 From ac87f204ecd8eb56690d91fe91f8208a2298f302 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 21:30:12 +0200 Subject: Removed unnecessary buttons for local episodes --- .../adapter/actionbutton/DownloadActionButton.java | 3 +- .../adapter/actionbutton/ItemActionButton.java | 2 + .../actionbutton/PlayLocalActionButton.java | 51 ++++++++++++++++++++++ .../danoeh/antennapod/fragment/ItemFragment.java | 3 ++ .../antennapod/core/feed/LocalFeedUpdater.java | 2 +- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayLocalActionButton.java 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/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index aaf0fc7d4..814dbc74e 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; @@ -328,6 +329,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/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index f024601d5..da3d6f19d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -89,7 +89,7 @@ public class LocalFeedUpdater { long duration = getFileDuration(file, context); long size = file.length(); FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(), - file.getUri().toString(), file.getUri().toString(), true, null, 0, 0); + file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); item.setMedia(media); return item; -- cgit v1.2.3 From 05cc4244e6a6bda31a25ca8c3b77a6db616a7c4c Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 22:16:03 +0200 Subject: Added image support for local feeds --- .../main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java | 3 +++ .../java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) 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..3ffa92df0 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; @@ -35,5 +37,6 @@ public class ApGlideModule extends AppGlideModule { public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory()); + registry.append(String.class, InputStream.class, new StringLoader.StreamFactory()); } } 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..8c9155bc9 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 { @@ -94,8 +95,9 @@ class ApOkHttpUrlLoader implements ModelLoader { } @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); } private static class NetworkAllowanceInterceptor implements Interceptor { -- cgit v1.2.3 From 643e970a27450148249a3af5ad910d7bcc219be3 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 22:16:18 +0200 Subject: Metadata improvements for local feeds --- .../antennapod/fragment/AddFeedFragment.java | 2 ++ .../antennapod/core/feed/LocalFeedUpdater.java | 35 +++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) 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 d29a36871..cc261c708 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -23,6 +23,7 @@ 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; @@ -153,6 +154,7 @@ public class AddFeedFragment extends Fragment { 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); 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 index da3d6f19d..04d2afb0b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -3,11 +3,13 @@ package de.danoeh.antennapod.core.feed; import android.content.Context; import android.media.MediaMetadataRetriever; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; import androidx.documentfile.provider.DocumentFile; 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.DateUtils; import de.danoeh.antennapod.core.util.DownloadError; import java.util.ArrayList; @@ -46,12 +48,12 @@ public class LocalFeedUpdater { List newItems = feed.getItems(); for (DocumentFile f : mediaFiles) { - FeedItem found = feedContainsFile(feed, f.getName()); - if (found != null) { - //TODO make sure the media has not changed (type, duration) + FeedItem oldItem = feedContainsFile(feed, f.getName()); + FeedItem newItem = createFeedItem(feed, f, context); + if (oldItem == null) { + newItems.add(newItem); } else { - FeedItem item = createFeedItem(feed, f, context); - newItems.add(item); + oldItem.updateFromOther(newItem); } } @@ -78,15 +80,21 @@ public class LocalFeedUpdater { } private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) { - //create item - long globalId = 0; - Date date = new Date(); String uuid = UUID.randomUUID().toString(); - FeedItem item = new FeedItem(globalId, file.getName(), uuid, file.getName(), date, FeedItem.UNPLAYED, feed); + 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 = getFileDuration(file, context); + 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); @@ -101,11 +109,4 @@ public class LocalFeedUpdater { DBWriter.addDownloadStatus(status); DBWriter.setFeedLastUpdateFailed(feed.getId(), true); } - - private static long getFileDuration(DocumentFile f, Context context) { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(context, f.getUri()); - String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - return Long.parseLong(durationStr); - } } -- cgit v1.2.3 From 122bed841bead219be66bc977ebfe68f392f53d1 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 22:28:42 +0200 Subject: Load chapters of local feed items --- .../antennapod/fragment/ChaptersFragment.java | 2 +- .../de/danoeh/antennapod/core/feed/FeedMedia.java | 6 +++--- .../playback/PlaybackServiceTaskManager.java | 2 +- .../danoeh/antennapod/core/util/ChapterUtils.java | 23 +++++++++++----------- .../core/util/playback/ExternalMedia.java | 2 +- .../antennapod/core/util/playback/Playable.java | 9 ++++----- .../antennapod/core/util/playback/RemoteMedia.java | 4 ++-- 7 files changed, 23 insertions(+), 25 deletions(-) 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 6d693f6cb..e57869e6e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -128,7 +128,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/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 7e1a5fd9b..df1de201b 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 @@ -375,7 +375,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 +386,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); 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 17cd1b24a..5e7ee6532 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/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java index b75887154..e0f5e9c03 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,10 +1,10 @@ package de.danoeh.antennapod.core.util; +import android.content.Context; +import android.net.Uri; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.Log; -import java.util.zip.CheckedOutputStream; import org.apache.commons.io.IOUtils; import java.io.BufferedInputStream; @@ -13,7 +13,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.util.Collections; import java.util.List; @@ -49,10 +48,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); } } @@ -71,7 +70,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; @@ -79,8 +78,8 @@ public class ChapterUtils { Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); CountingInputStream in = null; try { - URL url = new URL(p.getStreamUrl()); - in = new CountingInputStream(url.openStream()); + Uri uri = Uri.parse(p.getStreamUrl()); + in = new CountingInputStream(context.getContentResolver().openInputStream(uri)); List chapters = readChaptersFrom(in); if (!chapters.isEmpty()) { p.setChapters(chapters); @@ -142,14 +141,14 @@ 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()); - input = url.openStream(); + Uri uri = Uri.parse(media.getStreamUrl()); + input = context.getContentResolver().openInputStream(uri); if (input != null) { readOggChaptersFromInputStream(media, 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 -- cgit v1.2.3 From 1800704ec26f8d797950ec7419240b1382b8d2f5 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 25 May 2020 22:30:14 +0200 Subject: Added DocumentFile dependency --- core/build.gradle | 1 + 1 file changed, 1 insertion(+) 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" -- cgit v1.2.3 From b1ef9f424fc5ec46e5fcf995cdbe57b682e5e3ac Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 26 May 2020 12:02:56 +0200 Subject: Fixed chapters of non-content uris --- .../service/playback/PlaybackServiceTaskManager.java | 6 +++--- .../de/danoeh/antennapod/core/util/ChapterUtils.java | 20 ++++++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) 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 5e7ee6532..e71c1dfa7 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,9 +311,9 @@ public class PlaybackServiceTaskManager { if (media.getChapters() == null) { Completable.create(emitter -> { - media.loadChapterMarks(context); - emitter.onComplete(); - }) + media.loadChapterMarks(context); + emitter.onComplete(); + }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> callback.onChapterLoaded(media)); 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 e0f5e9c03..550c9f070 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,5 +1,6 @@ package de.danoeh.antennapod.core.util; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; @@ -13,6 +14,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.util.Collections; import java.util.List; @@ -78,8 +80,13 @@ public class ChapterUtils { Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); CountingInputStream in = null; try { - Uri uri = Uri.parse(p.getStreamUrl()); - in = new CountingInputStream(context.getContentResolver().openInputStream(uri)); + 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()); + in = new CountingInputStream(url.openStream()); + } List chapters = readChaptersFrom(in); if (!chapters.isEmpty()) { p.setChapters(chapters); @@ -147,8 +154,13 @@ public class ChapterUtils { } InputStream input = null; try { - Uri uri = Uri.parse(media.getStreamUrl()); - input = context.getContentResolver().openInputStream(uri); + 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()); + input = url.openStream(); + } if (input != null) { readOggChaptersFromInputStream(media, input); } -- cgit v1.2.3 From 984233d1d0c5950330b860446c75fa374d5cc139 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Wed, 8 Jul 2020 21:07:51 +0200 Subject: Delete removed files in local feeds --- .../de/test/antennapod/storage/DBTasksTest.java | 32 +++- .../de/test/antennapod/storage/DBWriterTest.java | 35 ++++ .../antennapod/core/feed/LocalFeedUpdater.java | 24 ++- .../service/download/handler/FeedSyncTask.java | 3 +- .../de/danoeh/antennapod/core/storage/DBTasks.java | 185 +++++++++++---------- .../danoeh/antennapod/core/storage/DBWriter.java | 98 +++++++---- .../antennapod/core/storage/PodDBAdapter.java | 18 ++ 7 files changed, 265 insertions(+), 130 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 090cd2213..63f76e5dd 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -29,6 +29,7 @@ import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; /** @@ -66,7 +67,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); assertTrue(newFeed == feed); assertTrue(feed.getId() != 0); @@ -86,8 +87,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()); } @@ -122,7 +123,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); assertTrue(feed != newFeed); updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); @@ -154,7 +155,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); assertTrue(feed != newFeed); final Feed feedFromDB = DBReader.getFeed(newFeed.getId()); @@ -162,6 +163,27 @@ 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 itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) { assertTrue(newFeed.getId() == feedID); assertTrue(newFeed.getItems().size() == NUM_ITEMS_NEW + NUM_ITEMS_OLD); 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 d82e366da..5d18619a7 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java @@ -422,6 +422,41 @@ 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 itemsToDelete = feed.getItems().subList(0, 2); + DBWriter.deleteFeedItems(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/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 04d2afb0b..2d58b7b52 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -15,7 +15,10 @@ import de.danoeh.antennapod.core.util.DownloadError; 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; public class LocalFeedUpdater { @@ -36,16 +39,20 @@ public class LocalFeedUpdater { 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)[0]; + feed = DBTasks.updateFeed(context, feed, false); + // list files in feed folder List mediaFiles = new ArrayList<>(); + Set 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 newItems = feed.getItems(); for (DocumentFile f : mediaFiles) { FeedItem oldItem = feedContainsFile(feed, f.getName()); @@ -57,6 +64,15 @@ public class LocalFeedUpdater { } } + // remove feed items without corresponding file + Iterator it = newItems.iterator(); + while (it.hasNext()) { + FeedItem feedItem = it.next(); + if (!mediaFileNames.contains(feedItem.getLink())) { + it.remove(); + } + } + List iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png"); for (String iconLocation : iconLocations) { DocumentFile image = documentFolder.findFile(iconLocation); @@ -66,7 +82,11 @@ public class LocalFeedUpdater { } } - DBTasks.updateFeed(context, feed); + // 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); } private static FeedItem feedContainsFile(Feed feed, String filename) { 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/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 323e34b6a..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 @@ -373,118 +373,133 @@ public final class DBTasks { *

* 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 newFeedsList = new ArrayList<>(); - List updatedFeedsList = new ArrayList<>(); - Feed[] resultFeeds = new Feed[newFeeds.length]; + public static synchronized Feed updateFeed(Context context, Feed newFeed, boolean removeUnlistedItems) { + Feed resultFeed; + List 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 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..9f917b44e 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 queue = DBReader.getQueue(); - List 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 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 items) { + DownloadRequester requester = DownloadRequester.getInstance(); + List queue = DBReader.getQueue(); + List 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 e6d47b32a..a2247a3db 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; @@ -851,6 +852,23 @@ public class PodDBAdapter { new String[]{String.valueOf(item.getId())}); } + /** + * Remove the listed items and their FeedMedia entries. + */ + public void removeFeedItems(@NonNull List 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. */ -- cgit v1.2.3 From 6a18c1978435ffb59d961caeffe18a869ee55ad1 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Sun, 12 Jul 2020 22:29:55 +0200 Subject: Multi select: handle local feeds --- .../antennapod/dialog/EpisodesApplyActionFragment.java | 14 ++++---------- .../danoeh/antennapod/fragment/FeedItemlistFragment.java | 8 +++++++- 2 files changed, 11 insertions(+), 11 deletions(-) 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 ac1e94437..8ae3f6079 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 items) { - return newInstance(items, ACTION_ALL); - } - public static EpisodesApplyActionFragment newInstance(List 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 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/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index b90da7447..2ca591909 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -257,8 +257,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: -- cgit v1.2.3 From 2488d93225caecb298a2da918643e47e64d62269 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 12 Jul 2020 22:40:45 +0200 Subject: Make checkstyle happy --- app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java | 3 ++- core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java | 2 +- 2 files changed, 3 insertions(+), 2 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 63f76e5dd..806d5a07a 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -168,7 +168,8 @@ public class DBTasksTest { 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)); + feed.getItems().add( + new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed)); } PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); 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 9f917b44e..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 @@ -168,7 +168,7 @@ public class DBWriter { */ @NonNull public static Future deleteFeedItems(@NonNull Context context, @NonNull List items) { - return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items) ); + return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items)); } /** -- cgit v1.2.3 From 75ef24159e163305193b2489da9c80a2a17c33be Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 12 Jul 2020 23:05:30 +0200 Subject: Do not allow to visit website of local items --- .../java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 1f15f66ec..5860a2c5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -68,10 +68,13 @@ public class FeedItemMenuHandler { setItemVisibility(menu, R.id.share_download_url_item, false); setItemVisibility(menu, R.id.share_download_url_with_position_item, false); } - if(!hasMedia || selectedItem.getMedia().getPosition() <= 0) { + if (!hasMedia || selectedItem.getMedia().getPosition() <= 0) { setItemVisibility(menu, R.id.share_download_url_with_position_item, false); setItemVisibility(menu, R.id.share_link_with_position_item, false); } + if (selectedItem.getFeed().isLocalFeed()) { + setItemVisibility(menu, R.id.visit_website_item, false); + } boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); setItemVisibility(menu, R.id.share_file, fileDownloaded); -- cgit v1.2.3 From d90b2b37bce50dae741841f68971d1637e12f5fa Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 12 Jul 2020 23:30:43 +0200 Subject: Fixed crash when local file was deleted --- core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 550c9f070..4fd54156b 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 @@ -92,7 +92,7 @@ public class ChapterUtils { 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); @@ -164,7 +164,7 @@ public class ChapterUtils { if (input != null) { readOggChaptersFromInputStream(media, input); } - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Log.e(TAG, Log.getStackTraceString(e)); } finally { IOUtils.closeQuietly(input); -- cgit v1.2.3 From 9cd1c9490600402276e7216ee009b91d5b4ec485 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 13 Jul 2020 00:21:20 +0200 Subject: Load embedded covers of local feed items --- .../de/danoeh/antennapod/core/feed/FeedItem.java | 2 +- .../de/danoeh/antennapod/core/feed/FeedMedia.java | 3 +- .../antennapod/core/feed/LocalFeedUpdater.java | 1 + .../antennapod/core/glide/ApGlideModule.java | 6 ++- .../antennapod/core/glide/ApOkHttpUrlLoader.java | 18 +------ .../antennapod/core/glide/AudioCoverFetcher.java | 14 +++++- .../core/glide/MetadataRetrieverLoader.java | 57 ++++++++++++++++++++++ 7 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java 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 20ed402fc..892592a4b 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 @@ -378,7 +378,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 df1de201b..083e8c500 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"; @@ -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 index 2d58b7b52..f9863b0a6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -118,6 +118,7 @@ public class LocalFeedUpdater { 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; 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 3ffa92df0..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 @@ -35,8 +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.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.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 8c9155bc9..963ce7596 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 @@ -53,14 +53,7 @@ class ApOkHttpUrlLoader implements ModelLoader { * 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 @@ -84,14 +77,7 @@ class ApOkHttpUrlLoader implements ModelLoader { @Nullable @Override public LoadData 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 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 { 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 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 { @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 { + + /** + * The default factory for {@link MetadataRetrieverLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + private final Context context; + + Factory(Context context) { + this.context = context; + } + + @NonNull + @Override + public ModelLoader 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 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); + } +} -- cgit v1.2.3 From 9d76676421f8f422fa7559f3ca34d9557bf7f8a0 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Tue, 21 Jul 2020 09:50:20 +0200 Subject: Do not warn that all files are deleted when removing a local feed --- .../main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java | 4 +++- .../main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java | 4 +++- .../main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java | 4 +++- core/src/main/res/values/strings.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) 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 2ca591909..284b775ea 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -279,9 +279,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/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 0dff8f24b..ceae7f847 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -199,9 +199,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 ba5d44b4d..1e248af07 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -270,7 +270,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/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 4f94a141a..a0408a07a 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -145,6 +145,7 @@ Share Media File URL Share Media File URL with Position Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes). + Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted. Removing podcast Refresh complete podcast Multi select -- cgit v1.2.3 From 3c5e1138ca1c616dd7add9d567442cc0d9a510ac Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Sat, 8 Aug 2020 14:37:51 +0200 Subject: Local feeds: Use default cover image if source folder doesn't contain a file like folder.png --- .../de/danoeh/antennapod/adapter/CoverLoader.java | 29 +++++++++++++++++--- .../antennapod/adapter/SubscriptionsAdapter.java | 5 +++- app/src/main/res/layout/subscription_item.xml | 3 ++- .../antennapod/core/feed/LocalFeedUpdater.java | 30 ++++++++++++++++----- .../antennapod/core/glide/ApOkHttpUrlLoader.java | 4 ++- core/src/main/res/raw/local_feed_default_icon.png | Bin 0 -> 1240 bytes core/src/main/res/values/colors.xml | 1 + 7 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 core/src/main/res/raw/local_feed_default_icon.png 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 { private final WeakReference placeholder; private final WeakReference 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 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/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index 29f94a7e6..e0c821868 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -21,7 +21,7 @@ android:id="@+id/txtvTitle" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/light_gray" + android:background="@color/non_square_icon_background" android:layout_alignLeft="@+id/imgvCover" android:layout_alignRight="@+id/imgvCover" android:layout_alignStart="@+id/imgvCover" @@ -30,6 +30,7 @@ android:layout_alignBottom="@+id/imgvCover" android:ellipsize="end" android:gravity="center" + android:textColor="?android:attr/textColorPrimary" tools:text="@string/app_name" /> items = feed.getItems(); for (FeedItem i : items) { 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 963ce7596..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 @@ -83,7 +83,9 @@ class ApOkHttpUrlLoader implements ModelLoader { @Override public boolean handles(@NonNull String model) { // Leave content URIs to Glide's default loaders - return !TextUtils.isEmpty(model) && !model.startsWith(ContentResolver.SCHEME_CONTENT); + 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/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 Binary files /dev/null and b/core/src/main/res/raw/local_feed_default_icon.png differ diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index a86d61eba..b9c5eca0d 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -10,6 +10,7 @@ #B00020 #80000000 #50000000 + #ccbfbfbf #FFFFFF -- cgit v1.2.3 From 401da0a2074d3f86d23cab054cd38a79241a4b35 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sat, 12 Sep 2020 22:02:46 +0200 Subject: Hide local feed button on old Android versions --- .../antennapod/fragment/AddFeedFragment.java | 50 ++++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) 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 290bf1845..5e014db3f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -6,6 +6,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -27,6 +28,7 @@ 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.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.SortOrder; import de.danoeh.antennapod.discovery.CombinedSearcher; import de.danoeh.antennapod.discovery.FyydPodcastSearcher; @@ -83,6 +85,9 @@ public class AddFeedFragment extends Fragment { } }); root.findViewById(R.id.btn_add_local_folder).setOnClickListener(v -> { + if (Build.VERSION.SDK_INT < 21) { + return; + } try { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -91,6 +96,9 @@ public class AddFeedFragment extends Fragment { Log.e(TAG, "No activity found. Should never happen..."); } }); + if (Build.VERSION.SDK_INT < 21) { + root.findViewById(R.id.btn_add_local_folder).setVisibility(View.GONE); + } root.findViewById(R.id.search_icon).setOnClickListener(view -> performSearch()); return root; } @@ -151,25 +159,31 @@ public class AddFeedFragment extends Fragment { 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); + addLocalFolder(uri); + } + } + + private void addLocalFolder(Uri uri) { + if (Build.VERSION.SDK_INT < 21) { + return; + } + 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 (DownloadRequestException | IllegalArgumentException e) { + Log.e(TAG, Log.getStackTraceString(e)); + ((MainActivity) getActivity()).showSnackbarAbovePlayer(e.getLocalizedMessage(), Snackbar.LENGTH_LONG); } } } -- cgit v1.2.3 From bce1fb9513e5b57a3f38802e532a010bd31d7244 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 13 Sep 2020 23:40:16 +0200 Subject: Initialize auto-download disabled for local feeds --- .../java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java | 10 ++++++++++ .../main/java/de/danoeh/antennapod/core/storage/DBTasks.java | 6 ++++-- 2 files changed, 14 insertions(+), 2 deletions(-) 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 index 706aed2c3..7ebb8633b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -18,6 +18,7 @@ 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; @@ -89,6 +90,15 @@ public class LocalFeedUpdater { // 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 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 9359774e9..477a39968 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 @@ -478,11 +478,11 @@ public final class DBTasks { resultFeed = savedFeed; } - adapter.close(); - try { 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(); } @@ -493,6 +493,8 @@ public final class DBTasks { e.printStackTrace(); } + adapter.close(); + if (savedFeed != null) { EventBus.getDefault().post(new FeedListUpdateEvent(savedFeed)); } else { -- cgit v1.2.3 From 4996d4685197d8a7e7813a88e19a183c27c9a794 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 13 Sep 2020 23:40:29 +0200 Subject: Hide irrelevant options for local feeds --- .../antennapod/fragment/FeedSettingsFragment.java | 34 +++++++++++++++++----- app/src/main/res/xml/feed_settings.xml | 4 ++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index a82c60d6c..31abd4c7b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment; import androidx.preference.ListPreference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; +import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent; @@ -100,6 +101,9 @@ public class FeedSettingsFragment extends Fragment { public static class FeedSettingsPreferenceFragment extends PreferenceFragmentCompat { private static final CharSequence PREF_EPISODE_FILTER = "episodeFilter"; private static final CharSequence PREF_SCREEN = "feedSettingsScreen"; + private static final CharSequence PREF_AUTHENTICATION = "authentication"; + private static final CharSequence PREF_AUTO_DELETE = "autoDelete"; + private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory"; private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed"; private static final String PREF_AUTO_SKIP = "feedAutoSkip"; private static final DecimalFormat SPEED_FORMAT = @@ -117,11 +121,20 @@ public class FeedSettingsFragment extends Fragment { return fragment; } + @Override + public RecyclerView onCreateRecyclerView (LayoutInflater inflater, ViewGroup parent, Bundle state) { + final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state); + // To prevent transition animation because of summary update + view.setItemAnimator(null); + view.setLayoutAnimation(null); + return view; + } + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.feed_settings); - findPreference(PREF_SCREEN).setEnabled(false); - setupAutoDownloadGlobalPreference(); // To prevent transition animation because of summary update + // To prevent displaying partially loaded data + findPreference(PREF_SCREEN).setVisible(false); long feedId = getArguments().getLong(EXTRA_FEED_ID); disposable = Maybe.create((MaybeOnSubscribe) emitter -> { @@ -138,6 +151,7 @@ public class FeedSettingsFragment extends Fragment { feed = result; feedPreferences = feed.getPreferences(); + setupAutoDownloadGlobalPreference(); setupAutoDownloadPreference(); setupKeepUpdatedPreference(); setupAutoDeletePreference(); @@ -151,7 +165,14 @@ public class FeedSettingsFragment extends Fragment { updateVolumeReductionValue(); updateAutoDownloadEnabled(); updatePlaybackSpeedPreference(); - findPreference(PREF_SCREEN).setEnabled(true); + + if (feed.isLocalFeed()) { + findPreference(PREF_AUTHENTICATION).setVisible(false); + findPreference(PREF_AUTO_DELETE).setVisible(false); + findPreference(PREF_CATEGORY_AUTO_DOWNLOAD).setVisible(false); + } + + findPreference(PREF_SCREEN).setVisible(true); }, error -> Log.d(TAG, Log.getStackTraceString(error)), () -> { }); } @@ -222,7 +243,7 @@ public class FeedSettingsFragment extends Fragment { } private void setupAuthentificationPreference() { - findPreference("authentication").setOnPreferenceClickListener(preference -> { + findPreference(PREF_AUTHENTICATION).setOnPreferenceClickListener(preference -> { new AuthenticationDialog(getContext(), R.string.authentication_label, true, feedPreferences.getUsername(), feedPreferences.getPassword()) { @@ -238,8 +259,7 @@ public class FeedSettingsFragment extends Fragment { } private void setupAutoDeletePreference() { - ListPreference autoDeletePreference = findPreference("autoDelete"); - autoDeletePreference.setOnPreferenceChangeListener((preference, newValue) -> { + findPreference(PREF_AUTO_DELETE).setOnPreferenceChangeListener((preference, newValue) -> { switch ((String) newValue) { case "global": feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.GLOBAL); @@ -265,7 +285,7 @@ public class FeedSettingsFragment extends Fragment { } private void updateAutoDeleteSummary() { - ListPreference autoDeletePreference = findPreference("autoDelete"); + ListPreference autoDeletePreference = findPreference(PREF_AUTO_DELETE); switch (feedPreferences.getAutoDeleteAction()) { case GLOBAL: diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index a6820a4ad..9d5ed5e8b 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -44,7 +44,9 @@ android:defaultValue="off" android:key="volumeReduction"/> - + -- cgit v1.2.3 From cf8d4c42f9edc8071190040ff132c6a0ab0f9932 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Sun, 27 Sep 2020 21:48:35 +0200 Subject: Local feeds: Hide "Share..." menu item (#4444) --- .../de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java | 8 +++++--- .../java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) 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 06b1c55bc..7604b94d6 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -64,9 +64,6 @@ 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(); @@ -105,6 +102,11 @@ public class FeedItemMenuHandler { setItemVisibility(menu, R.id.remove_item, fileDownloaded); + if (selectedItem.getFeed().isLocalFeed()) { + setItemVisibility(menu, R.id.visit_website_item, false); + setItemVisibility(menu, R.id.share_item, false); + } + return true; } diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index bcf202c05..5446d0191 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -51,6 +51,10 @@ public class FeedMenuHandler { menu.findItem(R.id.visit_website_item).setVisible(false); menu.findItem(R.id.share_link_item).setVisible(false); } + if (selectedFeed.isLocalFeed()) { + // hide complete submenu "Share..." as both sub menu items are not visible + menu.findItem(R.id.share_item).setVisible(false); + } return true; } -- cgit v1.2.3 From 94f8c6012d8e7dccfab37f48a259c6a9113db3a8 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 27 Sep 2020 21:52:12 +0200 Subject: Checkstyle fix --- .../main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 31abd4c7b..21280896a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -122,7 +122,7 @@ public class FeedSettingsFragment extends Fragment { } @Override - public RecyclerView onCreateRecyclerView (LayoutInflater inflater, ViewGroup parent, Bundle state) { + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle state) { final RecyclerView view = super.onCreateRecyclerView(inflater, parent, state); // To prevent transition animation because of summary update view.setItemAnimator(null); -- cgit v1.2.3 From 1d77ad472bb103ce2617712d4055e97477dd046d Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 29 Sep 2020 18:00:14 +0200 Subject: Fixed test --- app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 84b8d0e09..c28ce5003 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -70,7 +70,7 @@ public class DBTasksTest { } Feed newFeed = DBTasks.updateFeed(context, feed, false); - assertSame(feed, newFeed); + assertEquals(feed.getId(), newFeed.getId()); assertTrue(feed.getId() != 0); for (FeedItem item : feed.getItems()) { assertFalse(item.isPlayed()); -- cgit v1.2.3 From 41580b57cc06c297615bc3484274859bb0c9c5c1 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Fri, 2 Oct 2020 23:03:30 +0200 Subject: Local feeds: Do not display streaming confirmation (#4468) --- .../danoeh/antennapod/core/service/playback/PlaybackService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 9b373b3b9..bf485e55f 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 @@ -35,6 +35,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; @@ -482,7 +483,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(); @@ -668,7 +670,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) @@ -957,7 +960,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) -- cgit v1.2.3 From 28ebbedbdf34b72b31c536a118bcf5108b3ea7e5 Mon Sep 17 00:00:00 2001 From: Herbert Reiter <46045854+damoasda@users.noreply.github.com> Date: Sun, 25 Oct 2020 17:22:36 +0100 Subject: Local feeds: Unit tests for LocalFeedUpdater (#4551) --- core/build.gradle | 10 +- .../antennapod/core/storage/PodDBAdapter.java | 15 ++ core/src/test/assets/local-feed1/track1.mp3 | Bin 0 -> 43341 bytes core/src/test/assets/local-feed2/folder.png | Bin 0 -> 1589 bytes core/src/test/assets/local-feed2/track1.mp3 | Bin 0 -> 43341 bytes core/src/test/assets/local-feed2/track2.mp3 | Bin 0 -> 43497 bytes .../documentfile/provider/AssetsDocumentFile.java | 138 ++++++++++++++ .../antennapod/core/feed/LocalFeedUpdaterTest.java | 208 +++++++++++++++++++++ .../storage/ItemEnqueuePositionCalculatorTest.java | 6 +- 9 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 core/src/test/assets/local-feed1/track1.mp3 create mode 100644 core/src/test/assets/local-feed2/folder.png create mode 100644 core/src/test/assets/local-feed2/track1.mp3 create mode 100644 core/src/test/assets/local-feed2/track2.mp3 create mode 100644 core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java create mode 100644 core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java diff --git a/core/build.gradle b/core/build.gradle index fe130063e..96f0f7f04 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -52,6 +52,12 @@ android { dimension "market" } } + + testOptions { + unitTests { + includeAndroidResources = true + } + } } dependencies { @@ -94,7 +100,9 @@ dependencies { testImplementation "org.awaitility:awaitility:$awaitilityVersion" testImplementation 'junit:junit:4.13' - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'org.mockito:mockito-inline:3.5.13' + testImplementation 'org.robolectric:robolectric:4.3.1' + testImplementation 'javax.inject:javax.inject:1' androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test:runner:$runnerVersion" 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 142763d75..4c594783a 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 @@ -358,6 +358,21 @@ public class PodDBAdapter { // do nothing } + /** + *

Resets all database connections to ensure new database connections for + * the next test case. Call method only for unit tests.

+ * + *

That's a workaround for a Robolectric issue in ShadowSQLiteConnection + * that leads to an error IllegalStateException: Illegal connection + * pointer if several threads try to use the same database connection. + * For more information see + * robolectric/robolectric#1890.

+ */ + public static void tearDownTests() { + db = null; + SingletonHolder.dbHelper.close(); + } + public static boolean deleteDatabase() { PodDBAdapter adapter = getInstance(); adapter.open(); diff --git a/core/src/test/assets/local-feed1/track1.mp3 b/core/src/test/assets/local-feed1/track1.mp3 new file mode 100644 index 000000000..b1f993c3f Binary files /dev/null and b/core/src/test/assets/local-feed1/track1.mp3 differ diff --git a/core/src/test/assets/local-feed2/folder.png b/core/src/test/assets/local-feed2/folder.png new file mode 100644 index 000000000..9e522a986 Binary files /dev/null and b/core/src/test/assets/local-feed2/folder.png differ diff --git a/core/src/test/assets/local-feed2/track1.mp3 b/core/src/test/assets/local-feed2/track1.mp3 new file mode 100644 index 000000000..b1f993c3f Binary files /dev/null and b/core/src/test/assets/local-feed2/track1.mp3 differ diff --git a/core/src/test/assets/local-feed2/track2.mp3 b/core/src/test/assets/local-feed2/track2.mp3 new file mode 100644 index 000000000..310cddd6b Binary files /dev/null and b/core/src/test/assets/local-feed2/track2.mp3 differ diff --git a/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java b/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java new file mode 100644 index 000000000..8a8205c10 --- /dev/null +++ b/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java @@ -0,0 +1,138 @@ +package androidx.documentfile.provider; + +import android.content.res.AssetManager; +import android.net.Uri; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; + +/** + *

Wraps an Android assets file or folder as a DocumentFile object.

+ * + *

This is used to emulate access to the external storage.

+ */ +public class AssetsDocumentFile extends DocumentFile { + + /** + * Absolute file path in the assets folder. + */ + @NonNull + private final String fileName; + + @NonNull + private final AssetManager assetManager; + + public AssetsDocumentFile(@NonNull String fileName, @NonNull AssetManager assetManager) { + super(null); + this.fileName = fileName; + this.assetManager = assetManager; + } + + @Nullable + @Override + public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) { + return null; + } + + @Nullable + @Override + public DocumentFile createDirectory(@NonNull String displayName) { + return null; + } + + @NonNull + @Override + public Uri getUri() { + return Uri.parse(fileName); + } + + @Nullable + @Override + public String getName() { + int pos = fileName.indexOf('/'); + if (pos >= 0) { + return fileName.substring(pos + 1); + } else { + return fileName; + } + } + + @Nullable + @Override + public String getType() { + String extension = MimeTypeMap.getFileExtensionFromUrl(fileName); + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public long lastModified() { + return 0; + } + + @Override + public long length() { + return 0; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return false; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public boolean exists() { + return true; + } + + @NonNull + @Override + public DocumentFile[] listFiles() { + try { + String[] files = assetManager.list(fileName); + if (files == null) { + return new DocumentFile[0]; + } + DocumentFile[] result = new DocumentFile[files.length]; + for (int i = 0; i < files.length; i++) { + String subFileName = fileName + '/' + files[i]; + result[i] = new AssetsDocumentFile(subFileName, assetManager); + } + return result; + } catch (IOException e) { + return new DocumentFile[0]; + } + } + + @Override + public boolean renameTo(@NonNull String displayName) { + return false; + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java new file mode 100644 index 000000000..90bf59e92 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java @@ -0,0 +1,208 @@ +package de.danoeh.antennapod.core.feed; + +import android.app.Application; +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.documentfile.provider.AssetsDocumentFile; +import androidx.documentfile.provider.DocumentFile; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowMediaMetadataRetriever; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.core.ApplicationCallbacks; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +/** + * Test local feeds handling in class LocalFeedUpdater. + */ +@RunWith(RobolectricTestRunner.class) +public class LocalFeedUpdaterTest { + + /** + * URL to locate the local feed media files on the external storage (SD card). + * The exact URL doesn't matter here as access to external storage is mocked + * (seems not to be supported by Robolectric). + */ + private static final String FEED_URL = + "content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed"; + private static final String LOCAL_FEED_DIR1 = "local-feed1"; + private static final String LOCAL_FEED_DIR2 = "local-feed2"; + + private Context context; + + @Before + public void setUp() throws Exception { + // Initialize environment + context = InstrumentationRegistry.getInstrumentation().getContext(); + UserPreferences.init(context); + + Application app = (Application) context; + ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class); + when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app); + + // Initialize database + PodDBAdapter.init(context); + PodDBAdapter.deleteDatabase(); + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.close(); + + mapDummyMetadata(LOCAL_FEED_DIR1); + mapDummyMetadata(LOCAL_FEED_DIR2); + shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("mp3", "audio/mp3"); + } + + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + } + + /** + * Test adding a new local feed. + */ + @Test + public void testUpdateFeed_AddNewFeed() { + // check for empty database + List feedListBefore = DBReader.getFeedList(); + assertTrue(feedListBefore.isEmpty()); + + callUpdateFeed(LOCAL_FEED_DIR2); + + // verify new feed in database + verifySingleFeedInDatabaseAndItemCount(2); + Feed feedAfter = verifySingleFeedInDatabase(); + assertEquals(FEED_URL, feedAfter.getDownload_url()); + } + + /** + * Test adding further items to an existing local feed. + */ + @Test + public void testUpdateFeed_AddMoreItems() { + // add local feed with 1 item (localFeedDir1) + callUpdateFeed(LOCAL_FEED_DIR1); + + // now add another item (by changing to local feed folder localFeedDir2) + callUpdateFeed(LOCAL_FEED_DIR2); + + verifySingleFeedInDatabaseAndItemCount(2); + } + + /** + * Test removing items from an existing local feed without a corresponding media file. + */ + @Test + public void testUpdateFeed_RemoveItems() { + // add local feed with 2 items (localFeedDir1) + callUpdateFeed(LOCAL_FEED_DIR2); + + // now remove an item (by changing to local feed folder localFeedDir1) + callUpdateFeed(LOCAL_FEED_DIR1); + + verifySingleFeedInDatabaseAndItemCount(1); + } + + /** + * Test feed icon defined in the local feed media folder. + */ + @Test + public void testUpdateFeed_FeedIconFromFolder() { + callUpdateFeed(LOCAL_FEED_DIR2); + + Feed feedAfter = verifySingleFeedInDatabase(); + assertTrue(feedAfter.getImageUrl().contains("local-feed2/folder.png")); + } + + /** + * Test default feed icon if there is no matching file in the local feed media folder. + */ + @Test + public void testUpdateFeed_FeedIconDefault() { + callUpdateFeed(LOCAL_FEED_DIR1); + + Feed feedAfter = verifySingleFeedInDatabase(); + String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); + assertTrue(feedAfter.getImageUrl().contains(resourceEntryName)); + } + + /** + * Fill ShadowMediaMetadataRetriever with dummy duration and title. + * + * @param localFeedDir assets local feed folder with media files + */ + private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException { + String[] fileNames = context.getAssets().list(localFeedDir); + for (String fileName : fileNames) { + String path = localFeedDir + '/' + fileName; + ShadowMediaMetadataRetriever.addMetadata(path, + MediaMetadataRetriever.METADATA_KEY_DURATION, "10"); + ShadowMediaMetadataRetriever.addMetadata(path, + MediaMetadataRetriever.METADATA_KEY_TITLE, fileName); + } + + } + + /** + * Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with + * the given local feed folder. + * + * @param localFeedDir assets local feed folder with media files + */ + private void callUpdateFeed(@NonNull String localFeedDir) { + DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets()); + try (MockedStatic dfMock = Mockito.mockStatic(DocumentFile.class)) { + // mock external storage + dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile); + + // call method to test + Feed feed = new Feed(FEED_URL, null); + LocalFeedUpdater.updateFeed(feed, context); + } + } + + /** + * Verify that the database contains exactly one feed and return that feed. + */ + @NonNull + private static Feed verifySingleFeedInDatabase() { + List feedListAfter = DBReader.getFeedList(); + assertEquals(1, feedListAfter.size()); + return feedListAfter.get(0); + } + + /** + * Verify that the database contains exactly one feed and the number of + * items in the feed. + * + * @param expectedItemCount expected number of items in the feed + */ + private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) { + Feed feed = verifySingleFeedInDatabase(); + List feedItems = DBReader.getFeedItemList(feed); + assertEquals(expectedItemCount, feedItems.size()); + } +} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java index ed7d2fa75..6c5a9daf1 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -32,7 +32,7 @@ import static java.util.Collections.emptyList; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; +import static org.mockito.Mockito.when; public class ItemEnqueuePositionCalculatorTest { @@ -189,7 +189,7 @@ public class ItemEnqueuePositionCalculatorTest { // ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class); - stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false); + when(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).thenReturn(false); calculator.downloadStateProvider = stubDownloadStateProvider; // Setup initial data @@ -232,7 +232,7 @@ public class ItemEnqueuePositionCalculatorTest { private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider, boolean isDownloading) { - stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading); + when(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).thenReturn(isDownloading); return item; } -- cgit v1.2.3 From b9c63ca992b02015fcd88c1966f805064b0068cb Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 25 Oct 2020 17:38:01 +0100 Subject: Add error message for old Android versions --- app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java | 2 ++ 1 file changed, 2 insertions(+) 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 fa0df9abb..8ceee3db0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -89,6 +89,8 @@ public class AddFeedFragment extends Fragment { }); root.findViewById(R.id.btn_add_local_folder).setOnClickListener(v -> { if (Build.VERSION.SDK_INT < 21) { + ((MainActivity) getActivity()).showSnackbarAbovePlayer( + "Local folders are only supported on Android 5.0 and later", Snackbar.LENGTH_LONG); return; } try { -- cgit v1.2.3 From 361db64a07feb66e5632b8d60b14337b2e3497b0 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 25 Oct 2020 17:58:57 +0100 Subject: Allow to re-connect SAF document tree --- .../antennapod/fragment/FeedInfoFragment.java | 67 ++++++++++++++++++++++ app/src/main/res/menu/feedinfo.xml | 14 +++-- .../antennapod/core/feed/LocalFeedUpdater.java | 5 +- core/src/main/res/values/strings.xml | 2 + 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index ae03b5032..67d531173 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -1,16 +1,21 @@ package de.danoeh.antennapod.fragment; +import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.LightingColorFilter; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; import android.text.TextUtils; import android.util.Log; @@ -36,6 +41,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.LangUtils; @@ -43,12 +49,17 @@ import de.danoeh.antennapod.core.util.ThemeUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; import de.danoeh.antennapod.view.ToolbarIconTintManager; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.MaybeOnSubscribe; +import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import java.util.Collections; + /** * Displays information about a feed. */ @@ -56,6 +67,7 @@ public class FeedInfoFragment extends Fragment { private static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; private static final String TAG = "FeedInfoActivity"; + private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2; private Feed feed; private Disposable disposable; @@ -237,6 +249,7 @@ public class FeedInfoFragment extends Fragment { @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); menu.findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); @@ -256,6 +269,60 @@ public class FeedInfoFragment extends Fragment { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog(getContext(), e.getMessage()); } + + if (item.getItemId() == R.id.reconnect_local_folder) { + AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); + alert.setMessage(R.string.reconnect_local_folder_warning); + alert.setPositiveButton(android.R.string.ok, (dialog, which) -> { + 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..."); + } + }); + alert.setNegativeButton(android.R.string.cancel, null); + alert.show(); + return true; + } + return handled || super.onOptionsItemSelected(item); } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK || data == null) { + return; + } + Uri uri = data.getData(); + + if (requestCode == REQUEST_CODE_ADD_LOCAL_FOLDER) { + reconnectLocalFolder(uri); + } + } + + private void reconnectLocalFolder(Uri uri) { + if (Build.VERSION.SDK_INT < 21 || feed == null) { + return; + } + + Completable.fromAction(() -> { + 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.setDownload_url(Feed.PREFIX_LOCAL_FOLDER + uri.toString()); + DBTasks.updateFeed(getContext(), feed, true); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + () -> ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(R.string.add_local_folder_success, Snackbar.LENGTH_SHORT), + error -> ((MainActivity) getActivity()) + .showSnackbarAbovePlayer(error.getLocalizedMessage(), Snackbar.LENGTH_LONG)); + } } diff --git a/app/src/main/res/menu/feedinfo.xml b/app/src/main/res/menu/feedinfo.xml index f20d679a5..b1daf1f36 100644 --- a/app/src/main/res/menu/feedinfo.xml +++ b/app/src/main/res/menu/feedinfo.xml @@ -6,17 +6,19 @@ android:icon="?attr/location_web_site" custom:showAsAction="ifRoom|collapseActionView" android:title="@string/visit_website_label" - android:visible="true"> - + android:visible="true"/> - + android:title="@string/share_website_url_label"/> - + android:title="@string/share_feed_url_label"/> + 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 index 7ebb8633b..2791be08c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -32,11 +32,12 @@ public class LocalFeedUpdater { 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"); + reportError(feed, "Unable to retrieve document tree." + + "Try re-connecting the folder on the podcast info page."); return; } if (!documentFolder.exists() || !documentFolder.canRead()) { - reportError(feed, "Cannot read local directory"); + reportError(feed, "Cannot read local directory. Try re-connecting the folder on the podcast info page."); return; } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 8cb369961..30f64b35f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -741,6 +741,8 @@ Results by %1$s Add local folder Adding local folder succeeded + Re-connect local folder + In case of permission denials, you can use this to re-connect to the exact same folder. Do not select another folder. This virtual podcast was created by adding a folder to AntennaPod. Filter -- cgit v1.2.3 From e767282bffdfa6c79d82eea039271d90de57586d Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 25 Oct 2020 18:15:37 +0100 Subject: Fail gracefully when trying to cast local feed --- .../play/java/de/danoeh/antennapod/core/cast/CastUtils.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java index f01a8b638..6fa874eca 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.cast; +import android.content.ContentResolver; import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -50,15 +51,18 @@ public class CastUtils { public static final int FORMAT_VERSION_VALUE = 1; public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999; - public static boolean isCastable(Playable media){ + public static boolean isCastable(Playable media) { if (media == null || media instanceof ExternalMedia) { return false; } - if (media instanceof FeedMedia || media instanceof RemoteMedia){ + if (media instanceof FeedMedia || media instanceof RemoteMedia) { String url = media.getStreamUrl(); - if(url == null || url.isEmpty()){ + if (url == null || url.isEmpty()) { return false; } + if (url.startsWith(ContentResolver.SCHEME_CONTENT)) { + return false; // Local feed + } switch (media.getMediaType()) { case UNKNOWN: return false; -- cgit v1.2.3 From 587edab8b666f7bba59c5ca5532d99e31aef082d Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 25 Oct 2020 18:16:45 +0100 Subject: Make Lint happy --- app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index 67d531173..317fecfcb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -270,7 +270,7 @@ public class FeedInfoFragment extends Fragment { DownloadRequestErrorDialogCreator.newRequestErrorDialog(getContext(), e.getMessage()); } - if (item.getItemId() == R.id.reconnect_local_folder) { + if (item.getItemId() == R.id.reconnect_local_folder && Build.VERSION.SDK_INT >= 21) { AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setMessage(R.string.reconnect_local_folder_warning); alert.setPositiveButton(android.R.string.ok, (dialog, which) -> { -- cgit v1.2.3 From caaf2c72db2220c84ce24b60853c5fe6a5874b18 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 25 Oct 2020 18:45:30 +0100 Subject: Upgrade roboelectric for API 30 compatibility --- core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/build.gradle b/core/build.gradle index ada62faa8..f443ebb9b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -114,7 +114,7 @@ dependencies { testImplementation "org.awaitility:awaitility:$awaitilityVersion" testImplementation 'junit:junit:4.13' testImplementation 'org.mockito:mockito-inline:3.5.13' - testImplementation 'org.robolectric:robolectric:4.3.1' + testImplementation 'org.robolectric:robolectric:4.5-alpha-1' testImplementation 'javax.inject:javax.inject:1' androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" -- cgit v1.2.3