From db391867608fa37f6568eca02678b3b376e52fa8 Mon Sep 17 00:00:00 2001 From: thrillfall Date: Fri, 20 Aug 2021 20:17:23 +0200 Subject: Identify episodes by guid (#5326) --- .../danoeh/antennapod/core/storage/DBReader.java | 17 ++-- .../danoeh/antennapod/core/storage/DBWriter.java | 2 +- .../antennapod/core/storage/PodDBAdapter.java | 23 +++-- .../antennapod/core/sync/EpisodeActionFilter.java | 79 ++++++++++++++++ .../danoeh/antennapod/core/sync/GuidValidator.java | 10 ++ .../danoeh/antennapod/core/sync/SyncService.java | 102 +++++++-------------- 6 files changed, 145 insertions(+), 88 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java (limited to 'core/src/main/java/de/danoeh') 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 items) { + public void storeFeedItemlist(List 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, EpisodeAction> getRemoteActionsOverridingLocalActions( + List remoteActions, + List queuedEpisodeActions) { + // make sure more recent local actions are not overwritten by older remote actions + Map, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>(); + Map, EpisodeAction> localMostRecentPlayActions = + createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); + for (EpisodeAction remoteAction : remoteActions) { + Log.d(TAG, "Processing remoteAction: " + remoteAction.toString()); + Pair 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, EpisodeAction> createUniqueLocalMostRecentPlayActions( + List queuedEpisodeActions) { + Map, EpisodeAction> localMostRecentPlayAction; + localMostRecentPlayAction = new ArrayMap<>(); + for (EpisodeAction action : queuedEpisodeActions) { + Pair 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, EpisodeAction> localMostRecentPlayAction = new ArrayMap<>(); - for (EpisodeAction action : getQueuedEpisodeActions()) { - Pair 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, 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 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, EpisodeAction> playActionsToUpdate = EpisodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions()); LongList queueToBeRemoved = new LongList(); List 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); -- cgit v1.2.3