diff options
11 files changed, 397 insertions, 93 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index dc15b8a9a..49eca1027 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -567,15 +567,16 @@ public final class DBReader { /** * Loads a specific FeedItem from the database. * - * @param podcastUrl the corresponding feed's url + * @param guid feed item guid * @param episodeUrl the feed item's url * @return The FeedItem or null if the FeedItem could not be found. * Does NOT load additional attributes like feed or queue state. */ @Nullable - private static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { - Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); - try (Cursor cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl)) { + private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl, + PodDBAdapter adapter) { + Log.d(TAG, "Loading feeditem with guid " + guid + " or episode url " + episodeUrl); + try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) { if (!cursor.moveToNext()) { return null; } @@ -626,18 +627,18 @@ public final class DBReader { /** * Loads a specific FeedItem from the database. * - * @param podcastUrl the corresponding feed's url + * @param guid feed item guid * @param episodeUrl the feed item's url * @return The FeedItem or null if the FeedItem could not be found. * Does NOT load additional attributes like feed or queue state. */ - public static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl) { - Log.d(TAG, "getFeedItem() called with: " + "podcastUrl = [" + podcastUrl + "], episodeUrl = [" + episodeUrl + "]"); + public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) { + Log.d(TAG, "getFeedItem() called with: " + "guid = [" + guid + "], episodeUrl = [" + episodeUrl + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try { - return getFeedItemByUrl(podcastUrl, episodeUrl, adapter); + return getFeedItemByGuidOrEpisodeUrl(guid, episodeUrl, adapter); } finally { adapter.close(); } 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 9e60f4a4d..34ea5e207 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 @@ -803,7 +803,7 @@ public class DBWriter { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - adapter.setFeedItemlist(items); + adapter.storeFeedItemlist(items); adapter.close(); EventBus.getDefault().post(FeedItemEvent.updated(items)); }); 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 21ca1043f..85ce2dc99 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 @@ -558,7 +558,7 @@ public class PodDBAdapter { setFeed(feed); if (feed.getItems() != null) { for (FeedItem item : feed.getItems()) { - setFeedItem(item, false); + updateOrInsertFeedItem(item, false); } } if (feed.getPreferences() != null) { @@ -582,11 +582,11 @@ public class PodDBAdapter { db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); } - public void setFeedItemlist(List<FeedItem> items) { + public void storeFeedItemlist(List<FeedItem> items) { try { db.beginTransactionNonExclusive(); for (FeedItem item : items) { - setFeedItem(item, true); + updateOrInsertFeedItem(item, true); } db.setTransactionSuccessful(); } catch (SQLException e) { @@ -600,7 +600,7 @@ public class PodDBAdapter { long result = 0; try { db.beginTransactionNonExclusive(); - result = setFeedItem(item, true); + result = updateOrInsertFeedItem(item, true); db.setTransactionSuccessful(); } catch (SQLException e) { Log.e(TAG, Log.getStackTraceString(e)); @@ -618,7 +618,7 @@ public class PodDBAdapter { * false if the method is executed on a list of FeedItems of the same Feed. * @return the id of the entry */ - private long setFeedItem(FeedItem item, boolean saveFeed) { + private long updateOrInsertFeedItem(FeedItem item, boolean saveFeed) { if (item.getId() == 0 && item.getPubDate() == null) { Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate"); item.setPubDate(new Date()); @@ -1110,14 +1110,19 @@ public class PodDBAdapter { return db.rawQuery(query, null); } - public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) { - String escapedPodcastUrl = DatabaseUtils.sqlEscapeString(podcastUrl); + public final Cursor getFeedItemCursor(final String guid, final String episodeUrl) { String escapedEpisodeUrl = DatabaseUtils.sqlEscapeString(episodeUrl); + String whereClauseCondition = TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + "=" + escapedEpisodeUrl; + + if (guid != null) { + String escapedGuid = DatabaseUtils.sqlEscapeString(guid); + whereClauseCondition = TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "=" + escapedGuid; + } + final String query = SELECT_FEED_ITEMS_AND_MEDIA + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID - + " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + "=" + escapedEpisodeUrl - + " AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "=" + escapedPodcastUrl; + + " WHERE " + whereClauseCondition; Log.d(TAG, "SQL: " + query); return db.rawQuery(query, null); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java new file mode 100644 index 000000000..c74356d98 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java @@ -0,0 +1,79 @@ +package de.danoeh.antennapod.core.sync; + +import android.util.Log; + +import androidx.collection.ArrayMap; +import androidx.core.util.Pair; + +import java.util.List; +import java.util.Map; + +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + +public class EpisodeActionFilter { + + public static final String TAG = "EpisodeActionFilter"; + + public static Map<Pair<String, String>, EpisodeAction> getRemoteActionsOverridingLocalActions( + List<EpisodeAction> remoteActions, + List<EpisodeAction> queuedEpisodeActions) { + // make sure more recent local actions are not overwritten by older remote actions + Map<Pair<String, String>, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>(); + Map<Pair<String, String>, EpisodeAction> localMostRecentPlayActions = + createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); + for (EpisodeAction remoteAction : remoteActions) { + Log.d(TAG, "Processing remoteAction: " + remoteAction.toString()); + Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode()); + switch (remoteAction.getAction()) { + case NEW: + remoteActionsThatOverrideLocalActions.put(key, remoteAction); + break; + case DOWNLOAD: + break; + case PLAY: + EpisodeAction localMostRecent = localMostRecentPlayActions.get(key); + if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) { + break; + } + EpisodeAction remoteMostRecentAction = remoteActionsThatOverrideLocalActions.get(key); + if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) { + break; + } + remoteActionsThatOverrideLocalActions.put(key, remoteAction); + break; + case DELETE: + // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop + break; + default: + Log.e(TAG, "Unknown remoteAction: " + remoteAction); + break; + } + } + + return remoteActionsThatOverrideLocalActions; + } + + private static Map<Pair<String, String>, EpisodeAction> createUniqueLocalMostRecentPlayActions( + List<EpisodeAction> queuedEpisodeActions) { + Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction; + localMostRecentPlayAction = new ArrayMap<>(); + for (EpisodeAction action : queuedEpisodeActions) { + Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode()); + EpisodeAction mostRecent = localMostRecentPlayAction.get(key); + if (mostRecent == null || mostRecent.getTimestamp() == null) { + localMostRecentPlayAction.put(key, action); + } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + localMostRecentPlayAction.put(key, action); + } + } + return localMostRecentPlayAction; + } + + private static boolean secondActionOverridesFirstAction(EpisodeAction firstAction, + EpisodeAction secondAction) { + return secondAction != null + && secondAction.getTimestamp() != null + && secondAction.getTimestamp().after(firstAction.getTimestamp()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java b/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java new file mode 100644 index 000000000..6d80a6457 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java @@ -0,0 +1,10 @@ +package de.danoeh.antennapod.core.sync; + +public class GuidValidator { + + public static boolean isValidGuid(String guid) { + return guid != null + && !guid.trim().isEmpty(); + } +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 4736a2c33..9803a29db 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -7,8 +7,8 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.util.Log; + import androidx.annotation.NonNull; -import androidx.collection.ArrayMap; import androidx.core.app.NotificationCompat; import androidx.core.util.Pair; import androidx.work.BackoffPolicy; @@ -19,6 +19,7 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; + import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.SyncServiceEvent; import de.danoeh.antennapod.model.feed.Feed; @@ -45,6 +46,7 @@ import de.danoeh.antennapod.net.sync.model.SyncServiceException; import de.danoeh.antennapod.net.sync.model.UploadChangesResponse; import io.reactivex.Completable; import io.reactivex.schedulers.Schedulers; + import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import org.json.JSONArray; @@ -112,13 +114,13 @@ public class SyncService extends Worker { public static void clearQueue(Context context) { executeLockedAsync(() -> context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) - .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]") - .putString(PREF_QUEUED_FEEDS_ADDED, "[]") - .putString(PREF_QUEUED_FEEDS_REMOVED, "[]") - .apply()); + .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) + .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) + .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) + .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]") + .putString(PREF_QUEUED_FEEDS_ADDED, "[]") + .putString(PREF_QUEUED_FEEDS_REMOVED, "[]") + .apply()); } public static void enqueueFeedAdded(Context context, String downloadUrl) { @@ -258,7 +260,7 @@ public class SyncService extends Worker { lock.unlock(); } }).subscribeOn(Schedulers.io()) - .subscribe(); + .subscribe(); } } @@ -429,71 +431,31 @@ public class SyncService extends Worker { return; } - Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction = new ArrayMap<>(); - for (EpisodeAction action : getQueuedEpisodeActions()) { - Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode()); - EpisodeAction mostRecent = localMostRecentPlayAction.get(key); - if (mostRecent == null || mostRecent.getTimestamp() == null) { - localMostRecentPlayAction.put(key, action); - } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { - localMostRecentPlayAction.put(key, action); - } - } - - // make sure more recent local actions are not overwritten by older remote actions - Map<Pair<String, String>, EpisodeAction> mostRecentPlayAction = new ArrayMap<>(); - for (EpisodeAction action : remoteActions) { - Log.d(TAG, "Processing action: " + action.toString()); - switch (action.getAction()) { - case NEW: - FeedItem newItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode()); - if (newItem != null) { - DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true); - } else { - Log.i(TAG, "Unknown feed item: " + action); - } - break; - case DOWNLOAD: - break; - case PLAY: - Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode()); - EpisodeAction localMostRecent = localMostRecentPlayAction.get(key); - if (localMostRecent == null || localMostRecent.getTimestamp() == null - || localMostRecent.getTimestamp().before(action.getTimestamp())) { - EpisodeAction mostRecent = mostRecentPlayAction.get(key); - if (mostRecent == null || mostRecent.getTimestamp() == null) { - mostRecentPlayAction.put(key, action); - } else if (action.getTimestamp() != null - && mostRecent.getTimestamp().before(action.getTimestamp())) { - mostRecentPlayAction.put(key, action); - } else { - Log.d(TAG, "No date information in action, skipping it"); - } - } - break; - case DELETE: - // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop - break; - default: - Log.e(TAG, "Unknown action: " + action); - break; - } - } + Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions()); LongList queueToBeRemoved = new LongList(); List<FeedItem> updatedItems = new ArrayList<>(); - for (EpisodeAction action : mostRecentPlayAction.values()) { - FeedItem playItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode()); + for (EpisodeAction action : playActionsToUpdate.values()) { + String guid = GuidValidator.isValidGuid(action.getGuid()) ? action.getGuid() : null; + FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(guid, action.getEpisode()); + if (feedItem == null) { + Log.i(TAG, "Unknown feed item: " + action); + continue; + } + if (action.getAction() == EpisodeAction.NEW) { + DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true); + continue; + } Log.d(TAG, "Most recent play action: " + action.toString()); - if (playItem != null) { - FeedMedia media = playItem.getMedia(); - media.setPosition(action.getPosition() * 1000); - if (FeedItemUtil.hasAlmostEnded(playItem.getMedia())) { - Log.d(TAG, "Marking as played"); - playItem.setPlayed(true); - queueToBeRemoved.add(playItem.getId()); - } - updatedItems.add(playItem); + FeedMedia media = feedItem.getMedia(); + media.setPosition(action.getPosition() * 1000); + if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) { + Log.d(TAG, "Marking as played"); + feedItem.setPlayed(true); + queueToBeRemoved.add(feedItem.getId()); } + updatedItems.add(feedItem); + } DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); DBReader.loadAdditionalFeedItemListData(updatedItems); 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 e7daf866c..e1f52aa9f 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 @@ -192,7 +192,7 @@ public class CastUtils { } } if (result == null) { - FeedItem feedItem = DBReader.getFeedItemByUrl(metadata.getString(KEY_FEED_URL), + FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null, metadata.getString(KEY_EPISODE_IDENTIFIER)); if (feedItem != null) { result = feedItem.getMedia(); diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java index 07d7242aa..c9be06afd 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java @@ -218,7 +218,7 @@ public class DbReaderTest { } PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - adapter.setFeedItemlist(downloaded); + adapter.storeFeedItemlist(downloaded); adapter.close(); return downloaded; } @@ -257,7 +257,7 @@ public class DbReaderTest { } PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - adapter.setFeedItemlist(newItems); + adapter.storeFeedItemlist(newItems); adapter.close(); return newItems; } @@ -417,4 +417,23 @@ public class DbReaderTest { assertTrue(item2.hasChapters()); assertEquals(item1.getChapters(), item2.getChapters()); } + + @Test + public void testGetItemByEpisodeUrl() { + List<Feed> feeds = saveFeedlist(1, 1, true); + FeedItem item1 = feeds.get(0).getItems().get(0); + FeedItem feedItemByEpisodeUrl = DBReader.getFeedItemByGuidOrEpisodeUrl(null, + item1.getMedia().getDownload_url()); + assertEquals(item1.getItemIdentifier(), feedItemByEpisodeUrl.getItemIdentifier()); + } + + @Test + public void testGetItemByGuid() { + List<Feed> feeds = saveFeedlist(1, 1, true); + FeedItem item1 = feeds.get(0).getItems().get(0); + + FeedItem feedItemByGuid = DBReader.getFeedItemByGuidOrEpisodeUrl(item1.getItemIdentifier(), + item1.getMedia().getDownload_url()); + assertEquals(item1.getItemIdentifier(), feedItemByGuid.getItemIdentifier()); + } } diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java new file mode 100644 index 000000000..94695ca95 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java @@ -0,0 +1,187 @@ +package de.danoeh.antennapod.core.sync; + + +import androidx.core.util.Pair; + +import junit.framework.TestCase; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + + +public class EpisodeActionFilterTest extends TestCase { + + EpisodeActionFilter episodeActionFilter = new EpisodeActionFilter(); + + public void testGetRemoteActionsHappeningAfterLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + Date lateMorning = format.parse("2021-01-01 09:00:00"); + + List<EpisodeAction> episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(lateMorning) + .position(20) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); + List<EpisodeAction> remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(5) + .build() + ); + + Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(1, uniqueList.size()); + } + + public void testGetRemoteActionsHappeningBeforeLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + Date lateMorning = format.parse("2021-01-01 09:00:00"); + + List<EpisodeAction> episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(lateMorning) + .position(20) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); + List<EpisodeAction> remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(5) + .build() + ); + + Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(0, uniqueList.size()); + } + + public void testGetMultipleRemoteActionsHappeningAfterLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + + List<EpisodeAction> episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); + List<EpisodeAction> remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(5) + .build() + ); + + Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertEquals(2, uniqueList.size()); + } + + public void testGetMultipleRemoteActionsHappeningBeforeLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + + List<EpisodeAction> episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); + List<EpisodeAction> remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(5) + .build() + ); + + Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertEquals(0, uniqueList.size()); + } +}
\ No newline at end of file diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java new file mode 100644 index 000000000..356a7f77e --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java @@ -0,0 +1,18 @@ +package de.danoeh.antennapod.core.sync; + +import junit.framework.TestCase; + +public class GuidValidatorTest extends TestCase { + + public void testIsValidGuid() { + assertTrue(GuidValidator.isValidGuid("skfjsdvgsd")); + } + + public void testIsInvalidGuid() { + assertFalse(GuidValidator.isValidGuid("")); + assertFalse(GuidValidator.isValidGuid(" ")); + assertFalse(GuidValidator.isValidGuid("\n")); + assertFalse(GuidValidator.isValidGuid(" \n")); + assertFalse(GuidValidator.isValidGuid(null)); + } +}
\ No newline at end of file diff --git a/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java b/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java index 1aae5c811..cf4744c3e 100644 --- a/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java +++ b/net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java @@ -25,6 +25,7 @@ public class EpisodeAction { private final String podcast; private final String episode; + private final String guid; private final Action action; private final Date timestamp; private final int started; @@ -34,6 +35,7 @@ public class EpisodeAction { private EpisodeAction(Builder builder) { this.podcast = builder.podcast; this.episode = builder.episode; + this.guid = builder.guid; this.action = builder.action; this.timestamp = builder.timestamp; this.started = builder.started; @@ -72,6 +74,10 @@ public class EpisodeAction { e.printStackTrace(); } } + String guid = object.optString("guid", null); + if (!TextUtils.isEmpty(guid)) { + builder.guid(guid); + } if (action == EpisodeAction.Action.PLAY) { int started = object.optInt("started", -1); int position = object.optInt("position", -1); @@ -94,6 +100,10 @@ public class EpisodeAction { return this.episode; } + public String getGuid() { + return this.guid; + } + public Action getAction() { return this.action; } @@ -143,16 +153,21 @@ public class EpisodeAction { } EpisodeAction that = (EpisodeAction) o; - return started == that.started && position == that.position && total == that.total && action != that.action + return started == that.started + && position == that.position + && total == that.total + && action != that.action && ObjectsCompat.equals(podcast, that.podcast) && ObjectsCompat.equals(episode, that.episode) - && ObjectsCompat.equals(timestamp, that.timestamp); + && ObjectsCompat.equals(timestamp, that.timestamp) + && ObjectsCompat.equals(guid, that.guid); } @Override public int hashCode() { int result = podcast != null ? podcast.hashCode() : 0; result = 31 * result + (episode != null ? episode.hashCode() : 0); + result = 31 * result + (guid != null ? guid.hashCode() : 0); result = 31 * result + (action != null ? action.hashCode() : 0); result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0); result = 31 * result + started; @@ -171,6 +186,7 @@ public class EpisodeAction { try { obj.putOpt("podcast", this.podcast); obj.putOpt("episode", this.episode); + obj.putOpt("guid", this.guid); obj.put("action", this.getActionString()); SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -193,6 +209,7 @@ public class EpisodeAction { return "EpisodeAction{" + "podcast='" + podcast + '\'' + ", episode='" + episode + '\'' + + ", guid='" + guid + '\'' + ", action=" + action + ", timestamp=" + timestamp + ", started=" + started @@ -217,6 +234,7 @@ public class EpisodeAction { private int started = -1; private int position = -1; private int total = -1; + private String guid; public Builder(FeedItem item, Action action) { this(item.getFeed().getDownload_url(), item.getMedia().getDownload_url(), action); @@ -233,6 +251,11 @@ public class EpisodeAction { return this; } + public Builder guid(String guid) { + this.guid = guid; + return this; + } + public Builder currentTimestamp() { return timestamp(new Date()); } |