Identify episodes by guid (#5326)

This commit is contained in:
thrillfall 2021-08-20 20:17:23 +02:00 committed by GitHub
parent 7ebaa9f619
commit db39186760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 398 additions and 94 deletions

View File

@ -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();
}

View File

@ -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));
});

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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());
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);
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());
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);

View File

@ -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();

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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());
}