Compare commits

...

6 Commits

Author SHA1 Message Date
Hans-Peter Lehmann
88e57cfb47
Fix transcripts jumping when close to bottom (#7985) 2025-09-12 22:31:00 +02:00
Hans-Peter Lehmann
428d1901b4
Tweak error messages when interacting with text only episode (#7984) 2025-09-12 22:11:29 +02:00
Hans-Peter Lehmann
bc16763c5e
Fix spelling of Gpodder Sync Nextcloud plugin (#7983) 2025-09-12 22:02:43 +02:00
Hans-Peter Lehmann
720aba1602
Move remaining subscription settings to subscription screen (#7981) 2025-09-12 21:27:23 +02:00
Hans-Peter Lehmann
60f4c4cb20
Show error-printing step as failed on CI (#7982) 2025-09-12 21:26:30 +02:00
Hans-Peter Lehmann
0772b4998d
Fix and tune feed item duplicate guesser (#7979) 2025-09-12 21:00:56 +02:00
17 changed files with 319 additions and 138 deletions

View File

@ -49,6 +49,7 @@ jobs:
run: |
git diff --name-only | xargs -I '{}' echo "::error file={},line=1,endLine=1,title=XML Format::Run android-xml-formatter.jar on this file or view CI output to see how it should be formatted."
python .github/workflows/errorPrinter.py
false
unit-test:
name: "Unit Test: ${{ matrix.variant }}"

View File

@ -213,6 +213,10 @@ public class TranscriptDialogFragment extends DialogFragment
doInitialScroll = false;
boolean quickScroll = Math.abs(layoutManager.findFirstVisibleItemPosition() - pos) > 5;
if (layoutManager.findFirstVisibleItemPosition() < pos - 1
&& !viewBinding.transcriptList.canScrollVertically(1)) {
return;
}
if (quickScroll) {
viewBinding.transcriptList.scrollToPosition(pos - 1);
// Additionally, smooth scroll, so that currently active segment is on top of screen

View File

@ -6,28 +6,22 @@ import android.os.Build;
import android.os.Bundle;
import android.widget.Button;
import android.widget.ListView;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.preference.Preference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.storage.preferences.UsageStatistics;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.preferences.screen.AnimatedPreferenceFragment;
import de.danoeh.antennapod.ui.screen.subscriptions.FeedSortDialog;
import de.danoeh.antennapod.ui.screen.drawer.DrawerPreferencesDialog;
import org.greenrobot.eventbus.EventBus;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.screen.drawer.DrawerPreferencesDialog;
import de.danoeh.antennapod.ui.screen.subscriptions.SubscriptionsFilterDialog;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
public class UserInterfacePreferencesFragment extends AnimatedPreferenceFragment {
private static final String PREF_SWIPE = "prefSwipe";
@ -76,17 +70,6 @@ public class UserInterfacePreferencesFragment extends AnimatedPreferenceFragment
showFullNotificationButtonsDialog();
return true;
});
findPreference(UserPreferences.PREF_FILTER_FEED)
.setOnPreferenceClickListener((preference -> {
new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter");
return true;
}));
findPreference(UserPreferences.PREF_DRAWER_FEED_ORDER)
.setOnPreferenceClickListener((preference -> {
FeedSortDialog.showDialog(requireContext());
return true;
}));
findPreference(PREF_SWIPE)
.setOnPreferenceClickListener(preference -> {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_swipe);

View File

@ -0,0 +1,37 @@
package de.danoeh.antennapod.ui.screen.subscriptions;
import android.content.Context;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import org.greenrobot.eventbus.EventBus;
import java.util.Arrays;
import java.util.List;
public class FeedCounterDialog {
public static void showDialog(Context context) {
MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(context);
dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_counter_title));
dialog.setNegativeButton(android.R.string.cancel, (d, listener) -> d.dismiss());
int selected = UserPreferences.getFeedCounterSetting().id;
List<String> entryValues =
Arrays.asList(context.getResources().getStringArray(R.array.nav_drawer_feed_counter_values));
final int selectedIndex = entryValues.indexOf("" + selected);
String[] items = context.getResources().getStringArray(R.array.nav_drawer_feed_counter_options);
dialog.setSingleChoiceItems(items, selectedIndex, (d, which) -> {
if (selectedIndex != which) {
UserPreferences.setFeedCounterSetting(
FeedCounter.fromOrdinal(Integer.parseInt(entryValues.get(which))));
//Update subscriptions
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
}
d.dismiss();
});
dialog.show();
}
}

View File

@ -17,7 +17,7 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences;
public class FeedSortDialog {
public static void showDialog(Context context) {
MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(context);
dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_order_title));
dialog.setTitle(context.getString(R.string.sort));
dialog.setNegativeButton(android.R.string.cancel, (d, listener) -> d.dismiss());
int selected = UserPreferences.getFeedOrder().id;

View File

@ -233,6 +233,9 @@ public class SubscriptionFragment extends Fragment
} else if (itemId == R.id.subscriptions_sort) {
FeedSortDialog.showDialog(requireContext());
return true;
} else if (itemId == R.id.subscriptions_counter) {
FeedCounterDialog.showDialog(requireContext());
return true;
} else if (itemId == R.id.subscription_display_list) {
setColumnNumber(1);
return true;

View File

@ -5,9 +5,11 @@ import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import org.greenrobot.eventbus.EventBus;
public class AddToQueueSwipeAction implements SwipeAction {
@ -33,15 +35,20 @@ public class AddToQueueSwipeAction implements SwipeAction {
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
if (!item.isTagged(FeedItem.TAG_QUEUE)) {
DBWriter.addQueueItem(fragment.requireContext(), item);
} else {
if (item.isTagged(FeedItem.TAG_QUEUE)) {
new RemoveFromQueueSwipeAction().performAction(item, fragment, filter);
} else if (item.getMedia() == null) {
EventBus.getDefault().post(new MessageEvent(fragment.getString(R.string.no_media_label)));
} else {
DBWriter.addQueueItem(fragment.requireContext(), item);
}
}
@Override
public boolean willRemove(FeedItemFilter filter, FeedItem item) {
return filter.showQueued || filter.showNew;
if (item.getMedia() == null) {
return false;
}
return filter.showNotQueued || filter.showNew;
}
}

View File

@ -4,8 +4,10 @@ import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.actionbutton.DownloadActionButton;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import org.greenrobot.eventbus.EventBus;
public class StartDownloadSwipeAction implements SwipeAction {
@ -31,9 +33,12 @@ public class StartDownloadSwipeAction implements SwipeAction {
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) {
new DownloadActionButton(item)
.onClick(fragment.requireContext());
if (item.getMedia() == null) {
EventBus.getDefault().post(new MessageEvent(fragment.getString(R.string.no_media_label)));
} else if (item.getFeed().isLocalFeed() || item.isDownloaded()) {
EventBus.getDefault().post(new MessageEvent(fragment.getString(R.string.already_downloaded)));
} else {
new DownloadActionButton(item).onClick(fragment.requireContext());
}
}

View File

@ -24,6 +24,10 @@
android:id="@+id/subscriptions_sort"
android:title="@string/sort"
custom:showAsAction="never" />
<item
android:id="@+id/subscriptions_counter"
android:title="@string/pref_nav_drawer_feed_counter_title"
custom:showAsAction="never" />
<item
android:id="@+id/subscription_num_columns"
android:title="@string/subscription_num_columns"

View File

@ -1,7 +1,6 @@
package de.danoeh.antennapod.storage.database;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.event.FeedListUpdateEvent;
import de.danoeh.antennapod.model.download.DownloadError;
@ -45,38 +44,6 @@ public abstract class FeedDatabaseWriter {
return null;
}
/**
* Get a FeedItem by its identifying value.
*/
private static FeedItem searchFeedItemByIdentifyingValue(List<FeedItem> items, FeedItem searchItem) {
for (FeedItem item : items) {
if (TextUtils.equals(item.getIdentifyingValue(), searchItem.getIdentifyingValue())) {
return item;
}
}
return null;
}
/**
* Guess if one of the items could actually mean the searched item, even if it uses another identifying value.
* This is to work around podcasters breaking their GUIDs.
*/
private static FeedItem searchFeedItemGuessDuplicate(List<FeedItem> items, FeedItem searchItem) {
// First, see if it is a well-behaving feed that contains an item with the same identifier
for (FeedItem item : items) {
if (FeedItemDuplicateGuesser.sameAndNotEmpty(item.getItemIdentifier(), searchItem.getItemIdentifier())) {
return item;
}
}
// Not found yet, start more expensive guessing
for (FeedItem item : items) {
if (FeedItemDuplicateGuesser.seemDuplicates(item, searchItem)) {
return item;
}
}
return null;
}
/**
* Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same
* identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed.
@ -108,6 +75,9 @@ public abstract class FeedDatabaseWriter {
+ " already exists. Syncing new with existing one.");
Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
FeedItemDuplicateGuesserPool newFeedDuplicateGuesser = new FeedItemDuplicateGuesserPool(newFeed.getItems());
FeedItemDuplicateGuesserPool savedFeedDuplicateGuesser
= new FeedItemDuplicateGuesserPool(savedFeed.getItems());
if (newFeed.getPageNr() == savedFeed.getPageNr()) {
savedFeed.updateFromOther(newFeed);
@ -128,7 +98,7 @@ public abstract class FeedDatabaseWriter {
for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
final FeedItem item = newFeed.getItems().get(idx);
FeedItem possibleDuplicate = searchFeedItemGuessDuplicate(newFeed.getItems(), item);
FeedItem possibleDuplicate = newFeedDuplicateGuesser.guessDuplicate(item);
if (!newFeed.isLocalFeed() && possibleDuplicate != null && item != possibleDuplicate) {
// Canonical episode is the first one returned (usually oldest)
DBWriter.addDownloadStatus(new DownloadResult(item.getTitle(),
@ -142,9 +112,9 @@ public abstract class FeedDatabaseWriter {
continue;
}
FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed.getItems(), item);
FeedItem oldItem = savedFeedDuplicateGuesser.findById(item);
if (!newFeed.isLocalFeed() && oldItem == null) {
oldItem = searchFeedItemGuessDuplicate(savedFeed.getItems(), item);
oldItem = savedFeedDuplicateGuesser.guessDuplicate(item);
if (oldItem != null) {
Log.d(TAG, "Repaired duplicate: " + oldItem + ", " + item);
DBWriter.addDownloadStatus(new DownloadResult(item.getTitle(),
@ -181,6 +151,7 @@ public abstract class FeedDatabaseWriter {
} else {
savedFeed.getItems().add(idx, item);
}
savedFeedDuplicateGuesser.add(item);
boolean shouldPerformNewEpisodesAction = item.getPubDate() == null
|| priorMostRecentDate == null
@ -217,7 +188,7 @@ public abstract class FeedDatabaseWriter {
Iterator<FeedItem> it = savedFeed.getItems().iterator();
while (it.hasNext()) {
FeedItem feedItem = it.next();
if (searchFeedItemByIdentifyingValue(newFeed.getItems(), feedItem) == null) {
if (newFeedDuplicateGuesser.findById(feedItem) == null) {
unlistedItems.add(feedItem);
it.remove();
}

View File

@ -69,7 +69,7 @@ public class FeedItemDuplicateGuesser {
return sameAndNotEmpty(canonicalizeTitle(item1.getTitle()), canonicalizeTitle(item2.getTitle()));
}
private static String canonicalizeTitle(String title) {
public static String canonicalizeTitle(String title) {
if (title == null) {
return "";
}

View File

@ -0,0 +1,60 @@
package de.danoeh.antennapod.storage.database;
import de.danoeh.antennapod.model.feed.FeedItem;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FeedItemDuplicateGuesserPool {
private final Map<String, List<FeedItem>> normalizedTitles = new HashMap<>();
private final Map<String, FeedItem> downloadUrls = new HashMap<>();
private final Map<String, FeedItem> identifiers = new HashMap<>();
public FeedItemDuplicateGuesserPool(List<FeedItem> itemsList) {
for (FeedItem item : itemsList) {
add(item);
}
}
public void add(FeedItem item) {
String normalizedTitle = FeedItemDuplicateGuesser.canonicalizeTitle(item.getTitle());
if (!normalizedTitles.containsKey(normalizedTitle)) {
normalizedTitles.put(normalizedTitle, new java.util.ArrayList<>());
}
normalizedTitles.get(normalizedTitle).add(item);
if (item.getMedia() != null && !StringUtils.isEmpty(item.getMedia().getStreamUrl())
&& !downloadUrls.containsKey(item.getMedia().getStreamUrl())) {
downloadUrls.put(item.getMedia().getStreamUrl(), item);
}
if (item.getIdentifyingValue() != null && !identifiers.containsKey(item.getIdentifyingValue())) {
identifiers.put(item.getIdentifyingValue(), item);
}
}
public FeedItem guessDuplicate(FeedItem searchItem) {
if (searchItem.getMedia() != null && !StringUtils.isEmpty(searchItem.getMedia().getStreamUrl())
&& downloadUrls.containsKey(searchItem.getMedia().getStreamUrl())) {
return downloadUrls.get(searchItem.getMedia().getStreamUrl());
}
String normalizedTitle = FeedItemDuplicateGuesser.canonicalizeTitle(searchItem.getTitle());
List<FeedItem> candidates = normalizedTitles.get(normalizedTitle);
if (candidates == null) {
return null;
}
for (FeedItem item : candidates) {
if (FeedItemDuplicateGuesser.seemDuplicates(item, searchItem)) {
return item;
}
}
return null;
}
public FeedItem findById(FeedItem item) {
if (identifiers.containsKey(item.getIdentifyingValue())) {
return identifiers.get(item.getIdentifyingValue());
}
return null;
}
}

View File

@ -1,33 +1,28 @@
package de.danoeh.antennapod.net.download.service.episode.autodownload;
package de.danoeh.antennapod.storage.database;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import de.danoeh.antennapod.model.download.DownloadError;
import de.danoeh.antennapod.model.download.DownloadResult;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueue;
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueStub;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.storage.database.FeedDatabaseWriter;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.junit.After;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -35,40 +30,119 @@ import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Test class for {@link FeedDatabaseWriter}.
*/
@RunWith(RobolectricTestRunner.class)
public class DbTasksTest {
public class FeedDatabaseWriterTest {
private Context context;
@Before
public void setUp() {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
context = RuntimeEnvironment.getApplication();
UserPreferences.init(context);
PlaybackPreferences.init(context);
SynchronizationQueue.setInstance(new SynchronizationQueueStub());
// create new database
PodDBAdapter.init(context);
PodDBAdapter.deleteDatabase();
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
adapter.close();
SynchronizationQueue.setInstance(new SynchronizationQueueStub());
}
@After
public void tearDown() {
DBWriter.tearDownTests();
PodDBAdapter.tearDownTests();
@Test
public void testStoreNewFeed() {
Feed feed = createFeed();
for (int i = 0; i < 3; i++) {
feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
}
Feed updatedFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
List<FeedItem> storedItems = DBReader.getFeedItemList(updatedFeed, FeedItemFilter.unfiltered(),
SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
assertEquals(3, storedItems.size());
for (int i = 0; i < 3; i++) {
assertEquals("item-" + i, storedItems.get(i).getItemIdentifier());
}
}
@Test
public void testAddItemsToExistingFeed() {
Feed feed = createFeed();
for (int i = 0; i < 3; i++) {
feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
}
feed = FeedDatabaseWriter.updateFeed(context, feed, false);
Feed updatedFeed = createFeed();
updatedFeed.setId(feed.getId());
for (int i = 3; i < 6; i++) {
updatedFeed.getItems().add(createItem("item-" + i, "Item " + i, feed));
}
FeedDatabaseWriter.updateFeed(context, updatedFeed, false);
List<FeedItem> dbItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
assertEquals(6, dbItems.size());
for (int i = 0; i < 6; i++) {
assertEquals("item-" + i, dbItems.get(i).getItemIdentifier());
}
}
@Test
public void testAddOrUpdateItems() throws ExecutionException, InterruptedException {
Feed feed = createFeed();
for (int i = 0; i < 3; i++) {
feed.getItems().add(createItem("item-" + i, "Item " + i, feed));
}
feed = FeedDatabaseWriter.updateFeed(context, feed, false);
DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
DBWriter.markItemPlayed(feed.getItems().get(2), FeedItem.PLAYED, false).get();
Feed updatedFeed = createFeed();
updatedFeed.setId(feed.getId());
for (int i = 2; i < 5; i++) {
updatedFeed.getItems().add(createItem("item-" + i, "Item " + i, feed));
}
FeedDatabaseWriter.updateFeed(context, updatedFeed, false);
List<FeedItem> dbItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(),
SortOrder.EPISODE_TITLE_A_Z, 0, Integer.MAX_VALUE);
assertEquals(5, dbItems.size());
for (int i = 0; i < 5; i++) {
assertEquals("item-" + i, dbItems.get(i).getItemIdentifier());
}
assertEquals(FeedItem.PLAYED, dbItems.get(2).getPlayState());
}
@Test
public void testDuplicateItemsInFeed() {
Feed feed = createFeed();
feed.getItems().add(createItem("id1", "Duplicate Title", feed));
feed.getItems().add(createItem("id2", "Duplicate Title", feed));
FeedDatabaseWriter.updateFeed(context, feed, false); // First update just takes the feed without complaining
FeedDatabaseWriter.updateFeed(context, feed, false);
List<DownloadResult> downloadLog = DBReader.getDownloadLog();
assertEquals(1, downloadLog.size());
assertEquals(DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, downloadLog.get(0).getReason());
}
@Test
public void testGuidUpdated() {
Feed feed = createFeed();
feed.getItems().add(createItem("old-id", "Unique Title", feed));
FeedDatabaseWriter.updateFeed(context, feed, false);
Feed newFeed = createFeed();
newFeed.getItems().add(createItem("new-id", "Unique Title", newFeed));
Feed stored = FeedDatabaseWriter.updateFeed(context, newFeed, false);
assertEquals(1, stored.getItems().size());
assertEquals("new-id", stored.getItems().get(0).getItemIdentifier());
}
@Test
public void testUpdateFeedNewFeed() {
final int numItems = 10;
Feed feed = new Feed("url", null, "title");
feed.setItems(new ArrayList<>());
Feed feed = createFeed();
for (int i = 0; i < numItems; i++) {
feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
new Date(), FeedItem.UNPLAYED, feed));
@ -86,12 +160,9 @@ public class DbTasksTest {
/** Two feeds with the same title, but different download URLs should be treated as different feeds. */
@Test
public void testUpdateFeedSameTitle() {
Feed feed1 = new Feed("url1", null, "title");
Feed feed2 = new Feed("url2", null, "title");
feed1.setItems(new ArrayList<>());
feed2.setItems(new ArrayList<>());
Feed feed1 = createFeed();
Feed feed2 = createFeed();
feed2.setDownloadUrl("different url");
Feed savedFeed1 = FeedDatabaseWriter.updateFeed(context, feed1, false);
Feed savedFeed2 = FeedDatabaseWriter.updateFeed(context, feed2, false);
@ -104,8 +175,7 @@ public class DbTasksTest {
final int numItemsOld = 10;
final int numItemsNew = 10;
final Feed feed = new Feed("url", null, "title");
feed.setItems(new ArrayList<>());
final Feed feed = createFeed();
for (int i = 0; i < numItemsOld; i++) {
feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
new Date(i), FeedItem.PLAYED, feed));
@ -144,9 +214,9 @@ public class DbTasksTest {
@Test
public void testUpdateFeedMediaUrlResetState() {
final Feed feed = new Feed("url", null, "title");
final Feed feed = createFeed();
FeedItem item = new FeedItem(0, "item", "id", "link", new Date(), FeedItem.PLAYED, feed);
feed.setItems(singletonList(item));
feed.setItems(Collections.singletonList(item));
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
@ -173,8 +243,7 @@ public class DbTasksTest {
@Test
public void testUpdateFeedRemoveUnlistedItems() {
final Feed feed = new Feed("url", null, "title");
feed.setItems(new ArrayList<>());
final Feed feed = createFeed();
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));
@ -195,8 +264,7 @@ public class DbTasksTest {
@Test
public void testUpdateFeedSetDuplicate() {
final Feed feed = new Feed("url", null, "title");
feed.setItems(new ArrayList<>());
final Feed feed = createFeed();
for (int i = 0; i < 10; i++) {
FeedItem item =
new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed);
@ -249,4 +317,19 @@ public class DbTasksTest {
lastDate = item.getPubDate();
}
}
private Feed createFeed() {
Feed feed = new Feed("url", null, null);
feed.setItems(new ArrayList<>());
return feed;
}
private FeedItem createItem(String identifier, String title, Feed feed) {
FeedItem item = new FeedItem();
item.setItemIdentifier(identifier);
item.setTitle(title);
item.setMedia(new FeedMedia(item, "url-" + title, 2, "mime"));
item.setFeed(feed);
return item;
}
}

View File

@ -0,0 +1,40 @@
package de.danoeh.antennapod.storage.database;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import static org.junit.Assert.assertSame;
@RunWith(JUnit4.class)
public class FeedItemDuplicateGuesserPoolTest {
@Test
public void testDuplicateIsConsistent() {
Feed feed = new Feed("url", null, null);
FeedItem item1 = createItem("id1", "Title", feed);
FeedItem item2 = createItem("id2", "Title", feed);
FeedItemDuplicateGuesserPool pool = new FeedItemDuplicateGuesserPool(new ArrayList<>());
pool.add(item1);
assertSame(item1, pool.guessDuplicate(item1));
assertSame(item1, pool.guessDuplicate(item2));
pool.add(item2);
assertSame(item1, pool.guessDuplicate(item1));
assertSame(item1, pool.guessDuplicate(item2));
}
private FeedItem createItem(String identifier, String title, Feed feed) {
FeedItem item = new FeedItem();
item.setItemIdentifier(identifier);
item.setTitle(title);
item.setMedia(new FeedMedia(item, "url-" + title, 2, "mime"));
item.setFeed(feed);
return item;
}
}

View File

@ -278,6 +278,10 @@ public abstract class UserPreferences {
return FeedCounter.fromOrdinal(Integer.parseInt(value));
}
public static void setFeedCounterSetting(FeedCounter counter) {
prefs.edit().putString(PREF_DRAWER_FEED_COUNTER, "" + counter.id).apply();
}
/**
* @return {@code true} if episodes should use their own cover, {@code false} otherwise
*/

View File

@ -345,6 +345,7 @@
<string name="confirm_mobile_streaming_notification_message">Streaming over mobile data connection is disabled in the settings. Tap to stream anyway.</string>
<string name="confirm_mobile_streaming_button_always">Always</string>
<string name="confirm_mobile_streaming_button_once">Once</string>
<string name="already_downloaded">Episode is already downloaded</string>
<!-- Mediaplayer messages -->
<string name="playback_error_generic">The media file could not be played.\n\n- Try deleting and re-downloading the episode.\n- Check your network connection, and make sure no VPN or login page is blocking access.\n- Try long-pressing and sharing the \"Media address\" to your web browser to see if it can be played there. If not, contact the podcast creators.</string>
@ -489,10 +490,7 @@
<string name="bottom_navigation_summary">Access the most important screens from everywhere, in a single tap</string>
<string name="pref_nav_drawer_items_title">Customize navigation</string>
<string name="pref_nav_drawer_items_sum">Change which items appear in the navigation drawer or bottom navigation</string>
<string name="pref_nav_drawer_feed_order_title">Set subscription order</string>
<string name="pref_nav_drawer_feed_order_sum">Change the order of your subscriptions</string>
<string name="pref_nav_drawer_feed_counter_title">Set subscription counter</string>
<string name="pref_nav_drawer_feed_counter_sum">Change the information displayed by the subscription counter. Also affects the sorting of subscriptions if \'Subscription Order\' is set to \'Counter\'.</string>
<string name="pref_nav_drawer_feed_counter_title">Counter</string>
<string name="pref_automatic_download_title">Automatic download</string>
<string name="pref_automatic_download_global_description">Automatically download episodes from the inbox. Can be overridden per podcast.</string>
<string name="pref_automatic_download_queue_title">Download queued</string>
@ -561,8 +559,6 @@
<string name="pref_delete_removes_from_queue_sum">Automatically remove an episode from the queue when it is deleted</string>
<string name="pref_downloads_button_action_title">Play from downloads screen</string>
<string name="pref_downloads_button_action_sum">Display play button instead of delete button on downloads screen</string>
<string name="pref_filter_feed_title">Subscription filter</string>
<string name="pref_filter_feed_sum">Filter your subscriptions in navigation drawer and subscriptions screen</string>
<string name="subscriptions_counter_greater_zero">Counter greater than zero</string>
<string name="auto_downloaded">Auto downloaded</string>
<string name="not_auto_downloaded">Not auto downloaded</string>
@ -678,7 +674,7 @@
<string name="synchronization_summary_unchoosen">You can choose from multiple providers to synchronize your subscriptions and episode play state with</string>
<string name="dialog_choose_sync_service_title">Choose synchronization provider</string>
<string name="gpodnet_description">Gpodder.net is an open-source podcast synchronization service that you can install on your own server. Gpodder.net is independent of the AntennaPod project.</string>
<string name="synchronization_summary_nextcloud">Gpoddersync is an open-source Nextcloud app that you can easily install on your own server. The app is independent of the AntennaPod project.</string>
<string name="synchronization_summary_nextcloud">Gpodder Sync is an open-source Nextcloud app that you can easily install on your own server. Gpodder Sync is independent of the AntennaPod project.</string>
<string name="synchronization_host_explanation">You can pick your own server to synchronize with. When you have identified your preferred synchronization server, please enter its address here.</string>
<string name="synchronization_host_label">Server address</string>
<string name="proceed_to_login_butLabel">Proceed to login</string>
@ -709,7 +705,7 @@
<string name="gpodnetsync_pref_report_successful">Successful</string>
<string name="gpodnetsync_pref_report_failed">Failed</string>
<string name="gpodnetsync_username_characters_error">Usernames may only contain letters, digits, hyphens and underscores.</string>
<string name="nextcloud_login_error_generic">Unable to log into your Nextcloud.\n\n- Check your network connection.\n- Confirm that you are using the correct server address.\n- Make sure that the gpoddersync Nextcloud plugin is installed.</string>
<string name="nextcloud_login_error_generic">Unable to log into your Nextcloud.\n\n- Check your network connection.\n- Confirm that you are using the correct server address.\n- Make sure that the Gpodder Sync Nextcloud plugin is installed.</string>
<!-- Directory chooser -->
<string name="choose_data_directory">Choose data folder</string>

View File

@ -17,23 +17,6 @@
android:summary="@string/pref_tinted_theme_message"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/subscriptions_label">
<Preference
android:title="@string/pref_nav_drawer_feed_order_title"
android:key="prefDrawerFeedOrder"
android:summary="@string/pref_nav_drawer_feed_order_sum"/>
<de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
android:entryValues="@array/nav_drawer_feed_counter_values"
android:entries="@array/nav_drawer_feed_counter_options"
android:title="@string/pref_nav_drawer_feed_counter_title"
android:key="prefDrawerFeedIndicator"
android:summary="@string/pref_nav_drawer_feed_counter_sum"
android:defaultValue="1"/>
<Preference
android:title="@string/pref_filter_feed_title"
android:key="prefSubscriptionsFilter"
android:summary="@string/pref_filter_feed_sum" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/episode_information">
<SwitchPreferenceCompat
android:title="@string/pref_episode_cover_title"