summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java79
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java102
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java2
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java23
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java187
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java18
-rw-r--r--net/sync/model/src/main/java/de/danoeh/antennapod/net/sync/model/EpisodeAction.java27
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());
}