content: package allen.town.podcast.core.storage;
  
  import android.app.backup.BackupManager;
  import android.content.Context;
  import android.util.Log;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.core.app.NotificationManagerCompat;
  
  import com.wyjson.router.GoRouter;
  
  import org.greenrobot.eventbus.EventBus;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.List;
  import java.util.Locale;
  import java.util.Set;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.Future;
  import java.util.concurrent.TimeUnit;
  
  import allen.town.core.service.PayService;
  import allen.town.focus_common.util.Timber;
  import allen.town.podcast.core.R;
  import allen.town.podcast.core.event.DownloadLogEvent;
  import allen.town.podcast.core.feed.FeedEvent;
  import allen.town.podcast.core.pref.PlaybackPreferences;
  import allen.town.podcast.core.pref.Prefs;
  import allen.town.podcast.core.service.download.DownloadService;
  import allen.town.podcast.core.service.playback.PlaybackService;
  import allen.town.podcast.core.sync.queue.SynchronizationQueueSink;
  import allen.town.podcast.core.util.FeedItemPermutors;
  import allen.town.podcast.core.util.IntentUtils;
  import allen.town.podcast.core.util.LongList;
  import allen.town.podcast.core.util.Permutor;
  import allen.town.podcast.core.util.playback.PlayableUtils;
  import allen.town.podcast.event.FavoritesEvent;
  import allen.town.podcast.event.FeedItemEvent;
  import allen.town.podcast.event.FeedListUpdateEvent;
  import allen.town.podcast.event.QueueEvent;
  import allen.town.podcast.event.SubscribedFeedLimitEvent;
  import allen.town.podcast.event.UnreadItemsUpdateEvent;
  import allen.town.podcast.event.playback.PlaybackHistoryEvent;
  import allen.town.podcast.model.download.DownloadStatus;
  import allen.town.podcast.model.feed.Feed;
  import allen.town.podcast.model.feed.FeedItem;
  import allen.town.podcast.model.feed.FeedMedia;
  import allen.town.podcast.model.feed.FeedPreferences;
  import allen.town.podcast.model.feed.SortOrder;
  import allen.town.podcast.model.playback.Playable;
  import allen.town.podcast.storage.db.Db;
  import allen.town.podcast.sync.model.EpisodeAction;
  
  /**
   * Provides methods for writing data to 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.
   */
  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.setName("DatabaseExecutor");
              t.setPriority(Thread.MIN_PRIORITY);
              return t;
          });
      }
  
      private DBWriter() {
      }
  
      /**
       * Wait until all threads are finished to avoid the "Illegal connection pointer" error of
       * Robolectric. Call this method only for unit tests.
       */
      public static void tearDownTests() {
          try {
              dbExec.awaitTermination(1, TimeUnit.SECONDS);
          } catch (InterruptedException e) {
              // ignore error
          }
      }
  
      /**
       * 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(@NonNull final Context context,
                                                    final long mediaId) {
          return dbExec.submit(() -> {
              final FeedMedia media = DBReader.getFeedMedia(mediaId);
              if (media != null) {
                  boolean result = deleteFeedMediaSynchronous(context, media);
  
                  if (result && Prefs.shouldDeleteRemoveFromQueue()) {
                      DBWriter.removeQueueItemSynchronous(context, false, media.getItem().getId());
                  }
              }
          });
      }
  
      private static boolean deleteFeedMediaSynchronous(
              @NonNull Context context, @NonNull FeedMedia media) {
          Log.d(TAG, String.format(Locale.US, "requested to delete FeedMedia %d, %s, %s",
                  media.getId(), media.getEpisodeTitle(), media.isDownloaded()));
          if (media.isDownloaded()) {
              // delete downloaded media file
              File mediaFile = new File(media.getFile_url());
              if (mediaFile.exists() && !mediaFile.delete()) {
                  //本来就是默默删除，删除失败提示也没用
  //                MessageEvent evt = new MessageEvent(context.getString(R.string.delete_failed));
  //                EventBus.getDefault().post(evt);
                  Log.e(TAG,"delete feed media file failed");
                  return false;
              }
              media.setDownloaded(false);
              media.setFile_url(null);
              media.setHasEmbeddedPicture(false);
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setMedia(media);
              adapter.close();
  
              if (media.getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) {
                  PlaybackPreferences.writeNoMediaPlaying();
                  IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE);
  
                  NotificationManagerCompat nm = NotificationManagerCompat.from(context);
                  nm.cancel(R.id.notification_playing);
              }
  
              // Gpodder: queue delete action for synchronization
              FeedItem item = media.getItem();
              EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE)
                      .currentTimestamp()
                      .build();
              SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action);
          }
          EventBus.getDefault().post(FeedItemEvent.updated(media.getItem()));
          return true;
      }
  
      /**
       * 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(() -> {
              final Feed feed = DBReader.getFeed(feedId);
              if (feed == null) {
                  return;
              }
  
              // delete stored media files and mark them as read
              if (feed.getItems() == null) {
                  DBReader.getFeedItemList(feed);
              }
              deleteFeedItemsSynchronous(context, feed.getItems());
  
              // delete feed
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.removeFeed(feed);
              adapter.close();
  
              if (!feed.isLocalFeed()) {
                  SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.getDownload_url());
              }
              EventBus.getDefault().post(new FeedListUpdateEvent(feed));
          });
      }
  
      /**
       * Remove the listed items and their FeedMedia entries.
       * Deleting media also removes the download log entries.
       */
      @NonNull
      public static Future<?> deleteFeedItems(@NonNull Context context, @NonNull List<FeedItem> items) {
          return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items));
      }
  
      /**
       * Deletes the entire playback history.
       */
      public static void clearUnuseAndNotSubedFeedItems(@NonNull Context context) {
  //            List<FeedItem> items = DBReader.getUnsubFeedItems();
              //item删除了，feed不删除也很奇怪
  //            if (items.size() > (BuildConfig.DEBUG ? 0 : 1000)) {
  //                Timber.i("found items not subed to delete " + items.size());
                  Db adapter = Db.getInstance();
                  try {
  //                    deleteFeedItemsSynchronous(context, items);
                      List<Long> notSubAndNotInPlaylistAndFavFeedList = DBReader.getNotSubAndNotInPlaylistAndFavFeedList(adapter);
  //                    adapter.getDb().beginTransactionNonExclusive();
                      if (notSubAndNotInPlaylistAndFavFeedList.size() > 0) {
                          Timber.i("found feeds not subed and no-fav and no-playlist to delete " + notSubAndNotInPlaylistAndFavFeedList.size());
                          for (Long feedId : notSubAndNotInPlaylistAndFavFeedList
                          ) {
                              //加了失误好像死锁了
                              deleteFeed(context,feedId);
                          }
                      }
  
  //                    adapter.getDb().setTransactionSuccessful();
                  } catch (Exception e) {
                      Log.e(TAG, Log.getStackTraceString(e));
                  } finally {
                      adapter.getDb().endTransaction();
                  }
  
      }
  
      /**
       * Remove the listed items and their FeedMedia entries.
       * Deleting media also removes the download log entries.
       */
      private static void deleteFeedItemsSynchronous(@NonNull Context context, @NonNull List<FeedItem> items) {
          List<FeedItem> queue = DBReader.getQueue();
          List<FeedItem> removedFromQueue = new ArrayList<>();
          for (FeedItem item : items) {
              if (queue.remove(item)) {
                  removedFromQueue.add(item);
              }
              if (item.getMedia() != null) {
                  if (item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) {
                      // Applies to both downloaded and streamed media
                      PlaybackPreferences.writeNoMediaPlaying();
                      IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE);
                  }
                  if (item.getMedia().isDownloaded()) {
                      deleteFeedMediaSynchronous(context, item.getMedia());
                  }
                  DownloadService.cancel(context, item.getMedia().getDownload_url());
              }
          }
  
          Db adapter = Db.getInstance();
          adapter.open();
          if (!removedFromQueue.isEmpty()) {
              adapter.setQueue(queue);
          }
          adapter.removeFeedItems(items);
          adapter.close();
  
          for (FeedItem item : removedFromQueue) {
              EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
          }
  
          // 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
          EventBus.getDefault().post(DownloadLogEvent.listUpdated());
  
          BackupManager backupManager = new BackupManager(context);
          backupManager.dataChanged();
      }
  
      /**
       * Deletes the entire playback history.
       */
      public static Future<?> clearPlaybackHistory() {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.clearPlaybackHistory();
              adapter.close();
              EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated());
          });
      }
  
      /**
       * Deletes the entire download log.
       */
      public static Future<?> clearDownloadLog() {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.clearDownloadLog();
              adapter.close();
              EventBus.getDefault().post(DownloadLogEvent.listUpdated());
          });
      }
  
  
      /**
       * 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());
  
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedMediaPlaybackCompletionDate(media);
              adapter.close();
              EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated());
  
          });
      }
  
      /**
       * Adds a Download status object to the download log.
       *
       * @param status The DownloadStatus object.
       */
      public static Future<?> addDownloadStatus(final DownloadStatus status) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setDownloadStatus(status);
              adapter.close();
              EventBus.getDefault().post(DownloadLogEvent.listUpdated());
          });
  
      }
  
      /**
       * 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 Db adapter = Db.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) {
          return addQueueItem(context, true, items);
      }
  
      public static Future<?> addQueueItem(final Context context, boolean markAsUnplayed, 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, markAsUnplayed, 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 addQueueItem(context, performAutoDownload, true, itemIds);
      }
  
      /**
       * 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 markAsUnplayed      true if the items should be marked as unplayed when enqueueing
       * @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 boolean markAsUnplayed, final long... itemIds) {
          return dbExec.submit(() -> {
              if (itemIds.length < 1) {
                  return;
              }
  
              final Db adapter = Db.getInstance();
              adapter.open();
              final List<FeedItem> queue = DBReader.getQueue(adapter);
  
              boolean queueModified = false;
              LongList markAsUnplayedIds = new LongList();
              List<QueueEvent> events = new ArrayList<>();
              List<FeedItem> updatedItems = new ArrayList<>();
              ItemEnqueuePositionCalculator positionCalculator =
                      new ItemEnqueuePositionCalculator(Prefs.getEnqueueLocation());
              Playable currentlyPlaying = PlayableUtils.createInstanceFromPreferences(context);
              int insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying);
              for (long itemId : itemIds) {
                  if (!itemListContains(queue, itemId)) {
                      final FeedItem item = DBReader.getFeedItem(itemId);
                      if (item != null) {
                          queue.add(insertPosition, item);
                          events.add(QueueEvent.added(item, insertPosition));
  
                          item.addTag(FeedItem.TAG_QUEUE);
                          updatedItems.add(item);
                          queueModified = true;
                          if (item.isNew()) {
                              markAsUnplayedIds.add(item.getId());
                          }
                          insertPosition++;
                      }
                  }
              }
              if (queueModified) {
                  applySortOrder(queue, events);
                  adapter.setQueue(queue);
                  for (QueueEvent event : events) {
                      EventBus.getDefault().post(event);
                  }
                  EventBus.getDefault().post(FeedItemEvent.updated(updatedItems));
                  if (markAsUnplayed && markAsUnplayedIds.size() > 0) {
                      DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray());
                  }
              }
              adapter.close();
              if (performAutoDownload) {
                  DBTasks.autodownloadUndownloadedItems(context);
              }
          });
      }
  
      /**
       * Sorts the queue depending on the configured sort order.
       * If the queue is not in keep sorted mode, nothing happens.
       *
       * @param queue  The queue to be sorted.
       * @param events Replaces the events by a single SORT event if the list has to be sorted automatically.
       */
      private static void applySortOrder(List<FeedItem> queue, List<QueueEvent> events) {
          if (!Prefs.isPlaylistKeepSorted()) {
              // queue is not in keep sorted mode, there's nothing to do
              return;
          }
  
          // Sort queue by configured sort order
          SortOrder sortOrder = Prefs.getQueueKeepSortedOrder();
          if (sortOrder == SortOrder.RANDOM || sortOrder == SortOrder.RANDOM_FAKE) {
              // do not shuffle the list on every change
              return;
          }
          Permutor<FeedItem> permutor = FeedItemPermutors.getPermutor(sortOrder);
          permutor.reorder(queue);
  
          // Replace ADDED events by a single SORTED event
          events.clear();
          events.add(QueueEvent.sorted(queue));
      }
  
      /**
       * Removes all FeedItem objects from the queue.
       */
      public static Future<?> clearQueue() {
          return dbExec.submit(() -> {
              Db adapter = Db.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 performAutoDownload true if an auto-download process should be started after the operation.
       * @param item                FeedItem that should be removed.
       */
      public static Future<?> removeQueueItem(final Context context,
                                              final boolean performAutoDownload, final FeedItem item) {
          return dbExec.submit(() -> removeQueueItemSynchronous(context, performAutoDownload, item.getId()));
      }
  
      public static Future<?> removeQueueItem(final Context context, final boolean performAutoDownload,
                                              final long... itemIds) {
          return dbExec.submit(() -> removeQueueItemSynchronous(context, performAutoDownload, itemIds));
      }
  
      private static void removeQueueItemSynchronous(final Context context,
                                                     final boolean performAutoDownload,
                                                     final long... itemIds) {
          if (itemIds.length < 1) {
              return;
          }
          final Db adapter = Db.getInstance();
          adapter.open();
          final List<FeedItem> queue = DBReader.getQueue(adapter);
  
          if (queue != null) {
              boolean queueModified = false;
              List<QueueEvent> events = new ArrayList<>();
              List<FeedItem> updatedItems = new ArrayList<>();
              for (long itemId : itemIds) {
                  int position = indexInItemList(queue, itemId);
                  if (position >= 0) {
                      final FeedItem item = DBReader.getFeedItem(itemId);
                      if (item == null) {
                          Log.e(TAG, "removeQueueItem - item in queue but somehow cannot be loaded." +
                                  " Item ignored. It should never happen. id:" + itemId);
                          continue;
                      }
                      queue.remove(position);
                      item.removeTag(FeedItem.TAG_QUEUE);
                      events.add(QueueEvent.removed(item));
                      updatedItems.add(item);
                      queueModified = true;
                  } else {
                      Log.v(TAG, "removeQueueItem - item  not in queue:" + itemId);
                  }
              }
              if (queueModified) {
                  adapter.setQueue(queue);
                  for (QueueEvent event : events) {
                      EventBus.getDefault().post(event);
                  }
                  EventBus.getDefault().post(FeedItemEvent.updated(updatedItems));
              } 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<?> toggleFavoriteItem(final FeedItem item) {
          if (item.isTagged(FeedItem.TAG_FAVORITE)) {
              return removeFavoriteItem(item);
          } else {
              return addFavoriteItem(item);
          }
      }
  
      public static Future<?> addFavoriteItem(final FeedItem item) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance().open();
              adapter.addFavoriteItem(item);
              adapter.close();
              item.addTag(FeedItem.TAG_FAVORITE);
              EventBus.getDefault().post(new FavoritesEvent());
              EventBus.getDefault().post(FeedItemEvent.updated(item));
          });
      }
  
      public static Future<?> removeFavoriteItem(final FeedItem item) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance().open();
              adapter.removeFavoriteItem(item);
              adapter.close();
              item.removeTag(FeedItem.TAG_FAVORITE);
              EventBus.getDefault().post(new FavoritesEvent());
              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 Db adapter = Db.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 Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItemRead(played, itemIds);
              adapter.close();
              if (broadcastUpdate) {
                  EventBus.getDefault().post(new UnreadItemsUpdateEvent());
              }
          });
      }
  
  
      /**
       * 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.
       */
      @NonNull
      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);
      }
  
      @NonNull
      private static Future<?> markItemPlayed(final long itemId,
                                              final int played,
                                              final long mediaId,
                                              final boolean resetMediaPosition) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItemRead(played, itemId, mediaId,
                      resetMediaPosition);
              adapter.close();
  
              EventBus.getDefault().post(new UnreadItemsUpdateEvent());
          });
      }
  
      /**
       * Sets the 'read'-attribute of all NEW FeedItems of a specific Feed to UNPLAYED.
       *
       * @param feedId ID of the Feed.
       */
      public static Future<?> removeFeedNewFlag(final long feedId) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED, feedId);
              adapter.close();
  
              EventBus.getDefault().post(new UnreadItemsUpdateEvent());
          });
      }
  
      /**
       * Sets the 'read'-attribute of all FeedItems of a specific Feed to PLAYED.
       *
       * @param feedId ID of the Feed.
       */
      public static Future<?> markFeedRead(final long feedId) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItems(FeedItem.PLAYED, feedId);
              adapter.close();
  
              EventBus.getDefault().post(new UnreadItemsUpdateEvent());
          });
      }
  
      /**
       * Sets the 'read'-attribute of all FeedItems to PLAYED.
       */
      public static Future<?> markAllItemsRead() {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItems(FeedItem.PLAYED);
              adapter.close();
  
              EventBus.getDefault().post(new UnreadItemsUpdateEvent());
          });
      }
  
      /**
       * Sets the 'read'-attribute of all NEW FeedItems to UNPLAYED.
       */
      public static Future<?> removeAllNewFlags() {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED);
              adapter.close();
  
              EventBus.getDefault().post(new UnreadItemsUpdateEvent());
          });
      }
  
      public static Future<?> addNewFeed(final Context context, final Feed... feeds) {
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              adapter.setCompleteFeed(feeds);
              adapter.close();
  
              for (Feed feed : feeds) {
                  if (!feed.isLocalFeed()) {
                      SynchronizationQueueSink.enqueueFeedAddedIfSynchronizationIsActive(context, feed.getDownload_url());
                  }
              }
  
              BackupManager backupManager = new BackupManager(context);
              backupManager.dataChanged();
          });
      }
  
      static Future<?> setCompleteFeed(final Feed... feeds) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setCompleteFeed(feeds);
              adapter.close();
          });
      }
  
      public static Future<?> setItemList(final List<FeedItem> items) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.storeFeedItemlist(items);
              adapter.close();
              EventBus.getDefault().post(FeedItemEvent.updated(items));
          });
      }
  
      /**
       * 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(() -> {
              Db adapter = Db.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(() -> {
              Db adapter = Db.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(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setSingleFeedItem(item);
              adapter.close();
              EventBus.getDefault().post(FeedItemEvent.updated(item));
          });
      }
  
      public static Future<?> setFeedItemExcludeFeed(final FeedItem item) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setSingleFeedItemExcludeFeed(item);
              adapter.close();
              EventBus.getDefault().post(FeedItemEvent.updated(item));
          });
      }
  
      /**
       * 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(() -> {
              Db adapter = Db.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(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedPreferences(preferences);
              adapter.close();
              EventBus.getDefault().post(new FeedListUpdateEvent(preferences.getFeedID()));
          });
      }
  
      private static boolean itemListContains(List<FeedItem> items, long itemId) {
          return indexInItemList(items, itemId) >= 0;
      }
  
      private static int indexInItemList(List<FeedItem> items, long itemId) {
          for (int i = 0; i < items.size(); i++) {
              FeedItem item = items.get(i);
              if (item.getId() == itemId) {
                  return i;
              }
          }
          return -1;
      }
  
      /**
       * 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(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed);
              adapter.close();
              EventBus.getDefault().post(new FeedListUpdateEvent(feedId));
          });
      }
  
      public static Future<?> setFeedCustomTitle(Feed feed) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedCustomTitle(feed.getId(), feed.getCustomTitle());
              adapter.close();
              EventBus.getDefault().post(new FeedListUpdateEvent(feed));
          });
      }
  
      public static Future<?> setFeedItunesId(Feed feed) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItunesId(feed.getId(), feed.getItunesId());
              adapter.close();
              EventBus.getDefault().post(new FeedListUpdateEvent(feed));
          });
      }
  
  
  
      /**
       * 订阅某个feed
       * @param feed
       * @return
       */
      public static Future<?> subscribeFeed(Feed feed, Context context) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              if (GoRouter.getInstance().getService(PayService.class).isPurchase(context, false)) {
                  adapter.subscribeFeed(feed.getId());
              } else if (DBReader.getSubscribedFeedsCount() >= Feed.MAX_SUBSCRIBED_FEEDS_FOR_FREE) {
                  EventBus.getDefault().post(new SubscribedFeedLimitEvent());
                  return;
              } else {
                  adapter.subscribeFeed(feed.getId());
              }
              adapter.close();
              EventBus.getDefault().post(new FeedListUpdateEvent(feed));
  
          });
      }
  
  
  
      /**
       * Sort the FeedItems in the queue with the given the named sort order.
       *
       * @param broadcastUpdate <code>true</code> if this operation should trigger a
       *                        QueueUpdateBroadcast. This option should be set to <code>false</code>
       *                        if the caller wants to avoid unexpected updates of the GUI.
       */
      public static Future<?> reorderQueue(@Nullable SortOrder sortOrder, final boolean broadcastUpdate) {
          if (sortOrder == null) {
              Log.w(TAG, "reorderQueue() - sortOrder is null. Do nothing.");
              return dbExec.submit(() -> { });
          }
          final Permutor<FeedItem> permutor = FeedItemPermutors.getPermutor(sortOrder);
          return dbExec.submit(() -> {
              final Db adapter = Db.getInstance();
              adapter.open();
              final List<FeedItem> queue = DBReader.getQueue(adapter);
  
              if (queue != null) {
                  permutor.reorder(queue);
                  adapter.setQueue(queue);
                  if (broadcastUpdate) {
                      EventBus.getDefault().post(QueueEvent.sorted(queue));
                  }
              } else {
                  Log.e(TAG, "reorderQueue: Could not load queue");
              }
              adapter.close();
          });
      }
  
      /**
       * 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(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItemFilter(feedId, filterValues);
              adapter.close();
              EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId));
          });
      }
  
      /**
       * Set item sort order of the feed
       *
       */
      public static Future<?> setFeedItemSortOrder(long feedId, @Nullable SortOrder sortOrder) {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.setFeedItemSortOrder(feedId, sortOrder);
              adapter.close();
              EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.SORT_ORDER_CHANGED, feedId));
          });
      }
  
      /**
       * 重置数据库中的播客播放时长
       */
      @NonNull
      public static Future<?> resetStatistics() {
          return dbExec.submit(() -> {
              Db adapter = Db.getInstance();
              adapter.open();
              adapter.resetAllMediaPlayedDuration();
              adapter.close();
          });
      }
  }
  