package de.danoeh.antennapod.core.storage; import android.app.backup.BackupManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.util.Log; import org.shredzone.flattr4j.model.Flattr; import java.io.File; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; import de.greenrobot.event.EventBus; /** * Provides methods for writing data to AntennaPod's database. * In general, DBWriter-methods will be executed on an internal ExecutorService. * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's * will NOT contain any results. * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously. * This class will use the {@link EventDistributor} to notify listeners about changes in the database. */ public class DBWriter { private static final String TAG = "DBWriter"; private static final ExecutorService dbExec; static { dbExec = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r); t.setPriority(Thread.MIN_PRIORITY); return t; }); } private DBWriter() { } /** * Deletes a downloaded FeedMedia file from the storage device. * * @param context A context that is used for opening a database connection. * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted. */ public static Future<?> deleteFeedMediaOfItem(final Context context, final long mediaId) { return dbExec.submit(() -> { final FeedMedia media = DBReader.getFeedMedia(mediaId); if (media != null) { Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded()))); boolean result = false; if (media.isDownloaded()) { // delete downloaded media file File mediaFile = new File(media.getFile_url()); if (mediaFile.exists()) { result = mediaFile.delete(); } media.setDownloaded(false); media.setFile_url(null); media.setHasEmbeddedPicture(false); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setMedia(media); adapter.close(); // If media is currently being played, change playback // type to 'stream' and shutdown playback service SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { if (media.getId() == PlaybackPreferences .getCurrentlyPlayingFeedMediaId()) { SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean( PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, true); editor.commit(); } if (PlaybackPreferences .getCurrentlyPlayingFeedMediaId() == media .getId()) { context.sendBroadcast(new Intent( PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); } } // Gpodder: queue delete action for synchronization if(GpodnetPreferences.loggedIn()) { FeedItem item = media.getItem(); GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE) .currentDeviceId() .currentTimestamp() .build(); GpodnetPreferences.enqueueEpisodeAction(action); } } Log.d(TAG, "Deleting File. Result: " + result); EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem()))); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } }); } /** * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. * * @param context A context that is used for opening a database connection. * @param feedId ID of the Feed that should be deleted. */ public static Future<?> deleteFeed(final Context context, final long feedId) { return dbExec.submit(() -> { DownloadRequester requester = DownloadRequester.getInstance(); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context .getApplicationContext()); final Feed feed = DBReader.getFeed(feedId); if (feed != null) { if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA && PlaybackPreferences.getLastPlayedFeedId() == feed .getId()) { context.sendBroadcast(new Intent( PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); SharedPreferences.Editor editor = prefs.edit(); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, -1); editor.commit(); } // delete image file if (feed.getImage() != null) { if (feed.getImage().isDownloaded() && feed.getImage().getFile_url() != null) { File imageFile = new File(feed.getImage() .getFile_url()); imageFile.delete(); } else if (requester.isDownloadingFile(feed.getImage())) { requester.cancelDownload(context, feed.getImage()); } } // delete stored media files and mark them as read List<FeedItem> queue = DBReader.getQueue(); List<FeedItem> removed = new ArrayList<>(); if (feed.getItems() == null) { DBReader.getFeedItemList(feed); } for (FeedItem item : feed.getItems()) { if(queue.remove(item)) { removed.add(item); } if (item.getMedia() != null && item.getMedia().isDownloaded()) { File mediaFile = new File(item.getMedia() .getFile_url()); mediaFile.delete(); } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) { requester.cancelDownload(context, item.getMedia()); } if (item.hasItemImage()) { FeedImage image = item.getImage(); if (image.isDownloaded() && image.getFile_url() != null) { File imgFile = new File(image.getFile_url()); imgFile.delete(); } else if (requester.isDownloadingFile(image)) { requester.cancelDownload(context, item.getImage()); } } } PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); if (removed.size() > 0) { adapter.setQueue(queue); for(FeedItem item : removed) { EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item)); } } adapter.removeFeed(feed); adapter.close(); if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); } EventDistributor.getInstance().sendFeedUpdateBroadcast(); // we assume we also removed download log entries for the feed or its media files. // especially important if download or refresh failed, as the user should not be able // to retry these EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); BackupManager backupManager = new BackupManager(context); backupManager.dataChanged(); } }); } /** * Deletes the entire playback history. * */ public static Future<?> clearPlaybackHistory() { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.clearPlaybackHistory(); adapter.close(); EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); }); } /** * Deletes the entire download log. */ public static Future<?> clearDownloadLog() { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.clearDownloadLog(); adapter.close(); EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); }); } /** * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if * its playback completion date is set to a non-null value. This method will set the playback completion date to the * current date regardless of the current value. * * @param media FeedMedia that should be added to the playback history. */ public static Future<?> addItemToPlaybackHistory(final FeedMedia media) { return dbExec.submit(() -> { Log.d(TAG, "Adding new item to playback history"); media.setPlaybackCompletionDate(new Date()); // reset played_duration to 0 so that it behaves correctly when the episode is played again media.setPlayedDuration(0); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedMediaPlaybackCompletionDate(media); adapter.close(); EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); }); } /** * Adds a Download status object to the download log. * * @param status The DownloadStatus object. */ public static Future<?> addDownloadStatus(final DownloadStatus status) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setDownloadStatus(status); adapter.close(); EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); }); } /** * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to * true. If the FeedItem is already in the queue, the queue will not be modified. * * @param context A context that is used for opening a database connection. * @param itemId ID of the FeedItem that should be added to the queue. * @param index Destination index. Must be in range 0..queue.size() * @param performAutoDownload True if an auto-download process should be started after the operation * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() */ public static Future<?> addQueueItemAt(final Context context, final long itemId, final int index, final boolean performAutoDownload) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); FeedItem item; if (queue != null) { if (!itemListContains(queue, itemId)) { item = DBReader.getFeedItem(itemId); if (item != null) { queue.add(index, item); adapter.setQueue(queue); item.addTag(FeedItem.TAG_QUEUE); EventBus.getDefault().post(QueueEvent.added(item, index)); EventBus.getDefault().post(FeedItemEvent.updated(item)); if (item.isNew()) { DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); } } } } adapter.close(); if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); } }); } public static Future<?> addQueueItem(final Context context, final FeedItem... items) { LongList itemIds = new LongList(items.length); for (FeedItem item : items) { itemIds.add(item.getId()); item.addTag(FeedItem.TAG_QUEUE); } return addQueueItem(context, false, itemIds.toArray()); } /** * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. * * @param context A context that is used for opening a database connection. * @param performAutoDownload true if an auto-download process should be started after the operation. * @param itemIds IDs of the FeedItem objects that should be added to the queue. */ public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload, final long... itemIds) { return dbExec.submit(() -> { if (itemIds.length > 0) { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { boolean queueModified = false; LongList markAsUnplayedIds = new LongList(); List<QueueEvent> events = new ArrayList<>(); List<FeedItem> updatedItems = new ArrayList<>(); for (int i = 0; i < itemIds.length; i++) { if (!itemListContains(queue, itemIds[i])) { final FeedItem item = DBReader.getFeedItem(itemIds[i]); if (item != null) { // add item to either front ot back of queue boolean addToFront = UserPreferences.enqueueAtFront(); if (addToFront) { queue.add(0 + i, item); events.add(QueueEvent.added(item, 0 + i)); } else { queue.add(item); events.add(QueueEvent.added(item, queue.size() - 1)); } item.addTag(FeedItem.TAG_QUEUE); updatedItems.add(item); queueModified = true; if (item.isNew()) { markAsUnplayedIds.add(item.getId()); } } } } if (queueModified) { adapter.setQueue(queue); for (QueueEvent event : events) { EventBus.getDefault().post(event); } EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); if (markAsUnplayedIds.size() > 0) { DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray()); } } } adapter.close(); if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); } } }); } /** * Removes all FeedItem objects from the queue. * */ public static Future<?> clearQueue() { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.clearQueue(); adapter.close(); EventBus.getDefault().post(QueueEvent.cleared()); }); } /** * Removes a FeedItem object from the queue. * * @param context A context that is used for opening a database connection. * @param item FeedItem that should be removed. * @param performAutoDownload true if an auto-download process should be started after the operation. */ public static Future<?> removeQueueItem(final Context context, final FeedItem item, final boolean performAutoDownload) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { int position = queue.indexOf(item); if (position >= 0) { queue.remove(position); adapter.setQueue(queue); item.removeTag(FeedItem.TAG_QUEUE); EventBus.getDefault().post(QueueEvent.removed(item)); EventBus.getDefault().post(FeedItemEvent.updated(item)); } else { Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } } else { Log.e(TAG, "removeQueueItem: Could not load queue"); } adapter.close(); if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); } }); } public static Future<?> addFavoriteItem(final FeedItem item) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); adapter.addFavoriteItem(item); adapter.close(); item.addTag(FeedItem.TAG_FAVORITE); EventBus.getDefault().post(FavoritesEvent.added(item)); EventBus.getDefault().post(FeedItemEvent.updated(item)); }); } public static Future<?> addFavoriteItemById(final long itemId) { return dbExec.submit(() -> { final FeedItem item = DBReader.getFeedItem(itemId); if (item == null) { Log.d(TAG, "Can't find item for itemId " + itemId); return; } final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); adapter.addFavoriteItem(item); adapter.close(); item.addTag(FeedItem.TAG_FAVORITE); EventBus.getDefault().post(FavoritesEvent.added(item)); EventBus.getDefault().post(FeedItemEvent.updated(item)); }); } public static Future<?> removeFavoriteItem(final FeedItem item) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); adapter.removeFavoriteItem(item); adapter.close(); item.removeTag(FeedItem.TAG_FAVORITE); EventBus.getDefault().post(FavoritesEvent.removed(item)); EventBus.getDefault().post(FeedItemEvent.updated(item)); }); } /** * Moves the specified item to the top of the queue. * @param itemId The item to move to the top of the queue * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to */ public static Future<?> moveQueueItemToTop(final long itemId, final boolean broadcastUpdate) { return dbExec.submit(() -> { LongList queueIdList = DBReader.getQueueIDList(); int index = queueIdList.indexOf(itemId); if (index >=0) { moveQueueItemHelper(index, 0, broadcastUpdate); } else { Log.e(TAG, "moveQueueItemToTop: item not found"); } }); } /** * Moves the specified item to the bottom of the queue. * @param itemId The item to move to the bottom of the queue * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to */ public static Future<?> moveQueueItemToBottom(final long itemId, final boolean broadcastUpdate) { return dbExec.submit(() -> { LongList queueIdList = DBReader.getQueueIDList(); int index = queueIdList.indexOf(itemId); if (index >= 0) { moveQueueItemHelper(index, queueIdList.size() - 1, broadcastUpdate); } else { Log.e(TAG, "moveQueueItemToBottom: item not found"); } }); } /** * Changes the position of a FeedItem in the queue. * * @param from Source index. Must be in range 0..queue.size()-1. * @param to Destination index. Must be in range 0..queue.size()-1. * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to * false if the caller wants to avoid unexpected updates of the GUI. * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ public static Future<?> moveQueueItem(final int from, final int to, final boolean broadcastUpdate) { return dbExec.submit(() -> moveQueueItemHelper(from, to, broadcastUpdate)); } /** * Changes the position of a FeedItem in the queue. * <p/> * This function must be run using the ExecutorService (dbExec). * * @param from Source index. Must be in range 0..queue.size()-1. * @param to Destination index. Must be in range 0..queue.size()-1. * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to * false if the caller wants to avoid unexpected updates of the GUI. * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ private static void moveQueueItemHelper(final int from, final int to, final boolean broadcastUpdate) { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { final FeedItem item = queue.remove(from); queue.add(to, item); adapter.setQueue(queue); if (broadcastUpdate) { EventBus.getDefault().post(QueueEvent.moved(item, to)); } } } else { Log.e(TAG, "moveQueueItemHelper: Could not load queue"); } adapter.close(); } /* * Sets the 'read'-attribute of all specified FeedItems * * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW, * FeedItem.UNPLAYED * @param itemIds IDs of the FeedItems. */ public static Future<?> markItemPlayed(final int played, final long... itemIds) { return markItemPlayed(played, true, itemIds); } /* * Sets the 'read'-attribute of all specified FeedItems * * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW, * FeedItem.UNPLAYED * @param broadcastUpdate true if this operation should trigger a UnreadItemsUpdate broadcast. * This option is usually set to true * @param itemIds IDs of the FeedItems. */ public static Future<?> markItemPlayed(final int played, final boolean broadcastUpdate, final long... itemIds) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemRead(played, itemIds); adapter.close(); if(broadcastUpdate) { EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } }); } /** * Sets the 'read'-attribute of a FeedItem to the specified value. * @param item The FeedItem object * @param played New value of the 'read'-attribute one of FeedItem.PLAYED, * FeedItem.NEW, FeedItem.UNPLAYED * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. */ public static Future<?> markItemPlayed(FeedItem item, int played, boolean resetMediaPosition) { long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; return markItemPlayed(item.getId(), played, mediaId, resetMediaPosition); } private static Future<?> markItemPlayed(final long itemId, final int played, final long mediaId, final boolean resetMediaPosition) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemRead(played, itemId, mediaId, resetMediaPosition); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. * * @param feedId ID of the Feed. */ public static Future<?> markFeedSeen(final long feedId) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemCursor = adapter.getNewItemsIdsCursor(feedId); long[] ids = new long[itemCursor.getCount()]; itemCursor.moveToFirst(); for (int i = 0; i < ids.length; i++) { ids[i] = itemCursor.getLong(0); itemCursor.moveToNext(); } itemCursor.close(); adapter.setFeedItemRead(FeedItem.UNPLAYED, ids); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. * * @param feedId ID of the Feed. */ public static Future<?> markFeedRead(final long feedId) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); long[] itemIds = new long[itemCursor.getCount()]; itemCursor.moveToFirst(); for (int i = 0; i < itemIds.length; i++) { int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID); itemIds[i] = itemCursor.getLong(indexId); itemCursor.moveToNext(); } itemCursor.close(); adapter.setFeedItemRead(FeedItem.PLAYED, itemIds); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Sets the 'read'-attribute of all FeedItems to true. */ public static Future<?> markAllItemsRead() { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemCursor = adapter.getUnreadItemsCursor(); long[] itemIds = new long[itemCursor.getCount()]; itemCursor.moveToFirst(); for (int i = 0; i < itemIds.length; i++) { int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID); itemIds[i] = itemCursor.getLong(indexId); itemCursor.moveToNext(); } itemCursor.close(); adapter.setFeedItemRead(FeedItem.PLAYED, itemIds); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } static Future<?> addNewFeed(final Context context, final Feed... feeds) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setCompleteFeed(feeds); adapter.close(); if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { for (Feed feed : feeds) { GpodnetPreferences.addAddedFeed(feed.getDownload_url()); } } BackupManager backupManager = new BackupManager(context); backupManager.dataChanged(); }); } static Future<?> setCompleteFeed(final Feed... feeds) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setCompleteFeed(feeds); adapter.close(); }); } /** * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. * * @param media The FeedMedia object. */ public static Future<?> setFeedMedia(final FeedMedia media) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setMedia(media); adapter.close(); }); } /** * Saves the 'position', 'duration' and 'last played time' attributes of a FeedMedia object * * @param media The FeedMedia object. */ public static Future<?> setFeedMediaPlaybackInformation(final FeedMedia media) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedMediaPlaybackInformation(media); adapter.close(); }); } /** * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including * the content of FeedComponent-attributes. * * @param item The FeedItem object. */ public static Future<?> setFeedItem(final FeedItem item) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setSingleFeedItem(item); adapter.close(); EventBus.getDefault().post(FeedItemEvent.updated(item)); }); } /** * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. * * @param image The FeedImage object. */ public static Future<?> setFeedImage(final FeedImage image) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setImage(image); adapter.close(); }); } /** * Updates download URL of a feed */ public static Future<?> updateFeedDownloadURL(final String original, final String updated) { Log.d(TAG, "updateFeedDownloadURL(original: " + original + ", updated: " + updated +")"); return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedDownloadUrl(original, updated); adapter.close(); }); } /** * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0. * * @param preferences The FeedPreferences object. */ public static Future<?> setFeedPreferences(final FeedPreferences preferences) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedPreferences(preferences); adapter.close(); EventDistributor.getInstance().sendFeedUpdateBroadcast(); }); } private static boolean itemListContains(List<FeedItem> items, long itemId) { for (FeedItem item : items) { if (item.getId() == itemId) { return true; } } return false; } /** * Saves the FlattrStatus of a FeedItem object in the database. * * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved */ public static Future<?> setFeedItemFlattrStatus(final Context context, final FeedItem item, final boolean startFlattrClickWorker) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemFlattrStatus(item); adapter.close(); if (startFlattrClickWorker) { new FlattrClickWorker(context).executeAsync(); } }); } /** * Saves the FlattrStatus of a Feed object in the database. * * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved */ private static Future<?> setFeedFlattrStatus(final Context context, final Feed feed, final boolean startFlattrClickWorker) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedFlattrStatus(feed); adapter.close(); if (startFlattrClickWorker) { new FlattrClickWorker(context).executeAsync(); } }); } /** * Saves if a feed's last update failed * * @param lastUpdateFailed true if last update failed */ public static Future<?> setFeedLastUpdateFailed(final long feedId, final boolean lastUpdateFailed) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed); adapter.close(); }); } /** * format an url for querying the database * (postfix a / and apply percent-encoding) */ private static String formatURIForQuery(String uri) { try { return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); } catch (UnsupportedEncodingException e) { Log.e(TAG, e.getMessage()); return ""; } } /** * Set flattr status of the passed thing (either a FeedItem or a Feed) * * @param context * @param thing * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved * @return */ public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { // must propagate this to back db if (thing instanceof FeedItem) { return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); } else if (thing instanceof Feed) { return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); } else if (thing instanceof SimpleFlattrThing) { // SimpleFlattrThings are generated on the fly and do not have DB backing } else { Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); } return null; } /** * Reset flattr status to unflattrd for all items */ public static Future<?> clearAllFlattrStatus() { Log.d(TAG, "clearAllFlattrStatus()"); return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.clearAllFlattrStatus(); adapter.close(); }); } /** * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, * where the information has been retrieved from the flattr API */ public static Future<?> setFlattredStatus(final List<Flattr> flattrList) { Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); // clear flattr status in db clearAllFlattrStatus(); // submit list with flattred things having normalized URLs to db return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); for (Flattr flattr : flattrList) { adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); } adapter.close(); }); } /** * Sort the FeedItems in the queue with the given Comparator. * @param comparator FeedItem comparator * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to */ public static Future<?> sortQueue(final Comparator<FeedItem> comparator, final boolean broadcastUpdate) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { Collections.sort(queue, comparator); adapter.setQueue(queue); if (broadcastUpdate) { EventBus.getDefault().post(QueueEvent.sorted(queue)); } } else { Log.e(TAG, "sortQueue: Could not load queue"); } adapter.close(); }); } /** * Sets the 'auto_download'-attribute of specific FeedItem. * * @param feedItem FeedItem. * @param autoDownload true enables auto download, false disables it */ public static Future<?> setFeedItemAutoDownload(final FeedItem feedItem, final boolean autoDownload) { return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemAutoDownload(feedItem, autoDownload ? 1 : 0); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) { return dbExec.submit(() -> { int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1; long autoDownload; if(!feedItem.getAutoDownload() || failedAttempts >= 10) { autoDownload = 0; // giving up, disable auto download feedItem.setAutoDownload(false); } else { long now = System.currentTimeMillis(); autoDownload = (now / 10) * 10 + failedAttempts; } final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemAutoDownload(feedItem, autoDownload); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Sets the 'auto_download'-attribute of specific FeedItem. * * @param feed This feed's episodes will be processed. * @param autoDownload If true, auto download will be enabled for the feed's episodes. Else, */ public static Future<?> setFeedsItemsAutoDownload(final Feed feed, final boolean autoDownload) { Log.d(TAG, (autoDownload ? "Enabling" : "Disabling") + " auto download for items of feed " + feed.getId()); return dbExec.submit(() -> { final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedsItemsAutoDownload(feed, autoDownload); adapter.close(); EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Set filter of the feed * @param feedId The feed's ID * @param filterValues Values that represent properties to filter by */ public static Future<?> setFeedItemsFilter(final long feedId, final Set<String> filterValues) { Log.d(TAG, "setFeedItemsFilter() called with: " + "feedId = [" + feedId + "], filterValues = [" + filterValues + "]"); return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); adapter.setFeedItemFilter(feedId, filterValues); adapter.close(); EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)); }); } }