content: package allen.town.podcast.core.service.download;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.Service;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.os.Build;
  import android.os.IBinder;
  import android.text.TextUtils;
  import android.util.Log;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.VisibleForTesting;
  import androidx.core.app.ServiceCompat;
  
  import androidx.core.content.ContextCompat;
  
  import com.wyjson.router.GoRouter;
  
  import allen.town.core.service.PayService;
  import allen.town.focus_common.util.Timber;
  import allen.town.podcast.core.BuildConfig;
  import allen.town.podcast.core.R;
  import allen.town.podcast.core.feed.LocalFeedUpdater;
  import allen.town.podcast.core.storage.EpisodeCleanupAlgorithmFactory;
  import allen.town.podcast.event.SubscribedFeedLimitEvent;
  import allen.town.podcast.model.download.DownloadStatus;
  import org.apache.commons.io.FileUtils;
  import org.greenrobot.eventbus.EventBus;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.concurrent.CopyOnWriteArrayList;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.ScheduledFuture;
  import java.util.concurrent.ScheduledThreadPoolExecutor;
  import java.util.concurrent.TimeUnit;
  
  import allen.town.podcast.core.event.DownloadEvent;
  import allen.town.podcast.core.util.download.ConnectionStateMonitor;
  import allen.town.podcast.event.FeedItemEvent;
  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.core.pref.Prefs;
  import allen.town.podcast.core.service.download.handler.FailedDownloadHandler;
  import allen.town.podcast.core.service.download.handler.FeedSyncTask;
  import allen.town.podcast.core.service.download.handler.MediaDownloadedHandler;
  import allen.town.podcast.core.service.download.handler.PostDownloaderTask;
  import allen.town.podcast.core.storage.DBReader;
  import allen.town.podcast.core.storage.DBTasks;
  import allen.town.podcast.core.storage.DBWriter;
  import allen.town.podcast.model.download.DownloadError;
  
  /**
   * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent.
   * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUESTS field of
   * the intent.
   * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the
   * type of the feedfile.
   */
  public class DownloadService extends Service {
      private static final String TAG = "DownloadService";
      private static final int SCHED_EX_POOL_SIZE = 1;
      public static final String ACTION_CANCEL_DOWNLOAD = "action.allen.town.podcast.core.service.cancelDownload";
      public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.allen.town.podcast.core.service.cancelAll";
      public static final String EXTRA_DOWNLOAD_URL = "downloadUrl";
      public static final String EXTRA_REQUESTS = "downloadRequests";
      public static final String EXTRA_REFRESH_ALL = "refreshAll";
      public static final String EXTRA_INITIATED_BY_USER = "initiatedByUser";
      public static final String EXTRA_CLEANUP_MEDIA = "cleanupMedia";
  
      public static boolean isRunning = false;
  
      // Can be modified from another thread while iterating. Both possible race conditions are not critical:
      // Remove while iterating: We think it is still downloading and don't start a new download with the same file.
      // Add while iterating: We think it is not downloading and might start a second download with the same file.
      static final List<Downloader> downloads = Collections.synchronizedList(new CopyOnWriteArrayList<>());
      private final ExecutorService downloadHandleExecutor;
      private final ExecutorService downloadEnqueueExecutor;
  
      private final List<DownloadStatus> reportQueue = new ArrayList<>();
      private DownloadServiceNotification notificationManager;
      private final NewEpisodesNotification newEpisodesNotification;
      private NotificationUpdater notificationUpdater;
      private ScheduledFuture<?> notificationUpdaterFuture;
      private ScheduledFuture<?> downloadPostFuture;
      private final ScheduledThreadPoolExecutor notificationUpdateExecutor;
      private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory();
      private ConnectionStateMonitor connectionMonitor;
  
      @Override
      public IBinder onBind(Intent intent) {
          return null;
      }
  
      public DownloadService() {
          newEpisodesNotification = new NewEpisodesNotification();
  
          downloadEnqueueExecutor = Executors.newSingleThreadExecutor(r -> {
              Thread t = new Thread(r, "EnqueueThread");
              t.setPriority(Thread.MIN_PRIORITY);
              return t;
          });
          // Must be the first runnable in syncExecutor
          downloadEnqueueExecutor.execute(newEpisodesNotification::loadCountersBeforeRefresh);
  
          Log.d(TAG, "init download service " + Prefs.getParallelDownloads());
          downloadHandleExecutor = Executors.newFixedThreadPool(Prefs.getParallelDownloads(),
                  r -> {
                      Thread t = new Thread(r, "DownloadThread");
                      t.setPriority(Thread.MIN_PRIORITY);
                      return t;
                  });
          notificationUpdateExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE,
                  r -> {
                      Thread t = new Thread(r, "NotificationUpdateExecutor");
                      t.setPriority(Thread.MIN_PRIORITY);
                      return t;
                  }, (r, executor) -> Log.w(TAG, "SchedEx rejected submission of new task")
          );
      }
  
      @Override
      public void onCreate() {
          Log.d(TAG, "onCreate");
          isRunning = true;
          notificationManager = new DownloadServiceNotification(this);
  
          IntentFilter cancelDownloadReceiverFilter = new IntentFilter();
          cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS);
          cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
              registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter,RECEIVER_EXPORTED);
          } else {
              registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter);
          }
  
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
              connectionMonitor = new ConnectionStateMonitor();
              connectionMonitor.enable(getApplicationContext());
          }
      }
  
      public static void download(Context context, boolean cleanupMedia, DownloadRequest... requests) {
          ArrayList<DownloadRequest> requestsToSend = new ArrayList<>();
          int count = 0;
          for (DownloadRequest request : requests) {
              if (!isDownloadingFile(request.getSource())) {
                  if(request.isNeedAutoSubscribe()){
                      //自动订阅需要判断由没有达到上限，如果是多个请求不能循环调用不然每次查询到订阅数都是没变化
                      if(!GoRouter.getInstance().getService(PayService.class).isPurchase(context,false)
                              && (DBReader.getSubscribedFeedsCount() + count++) >= Feed.MAX_SUBSCRIBED_FEEDS_FOR_FREE){
                          EventBus.getDefault().post(new SubscribedFeedLimitEvent());
                          Timber.w("The maximum number of subscriptions for the free version has been reached");
                          break;
                      }
  
                  }
                  Timber.i("got request " + request.getTitle());
                  requestsToSend.add(request);
              }
          }
          if (requestsToSend.isEmpty()) {
              return;
          } else if (requestsToSend.size() > 100) {
              if (BuildConfig.DEBUG) {
                  throw new IllegalArgumentException("Android silently drops intent payloads that are too large");
              } else {
                  Log.i(TAG, "Too many download requests. Dropping some to avoid Android dropping all.");
                  requestsToSend = new ArrayList<>(requestsToSend.subList(0, 100));
              }
          }
  
          Intent launchIntent = new Intent(context, DownloadService.class);
          launchIntent.putParcelableArrayListExtra(DownloadService.EXTRA_REQUESTS, requestsToSend);
          if (cleanupMedia) {
              launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, true);
          }
          ContextCompat.startForegroundService(context, launchIntent);
      }
  
      public static void refreshAllFeeds(Context context, boolean initiatedByUser) {
          Intent launchIntent = new Intent(context, DownloadService.class);
          launchIntent.putExtra(DownloadService.EXTRA_REFRESH_ALL, true);
          launchIntent.putExtra(DownloadService.EXTRA_INITIATED_BY_USER, initiatedByUser);
          ContextCompat.startForegroundService(context, launchIntent);
      }
  
      public static void cancel(Context context, String url) {
          if (!isRunning) {
              return;
          }
          Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD);
          cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, url);
          cancelIntent.setPackage(context.getPackageName());
          context.sendBroadcast(cancelIntent);
      }
  
      public static void cancelAll(Context context) {
          if (!isRunning) {
              return;
          }
          Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS);
          cancelIntent.setPackage(context.getPackageName());
          context.sendBroadcast(cancelIntent);
      }
  
      public static boolean isDownloadingFeeds() {
          if (!isRunning) {
              return false;
          }
          for (Downloader downloader : downloads) {
              if (downloader.request.getFeedfileType() == Feed.FEEDFILETYPE_FEED && !downloader.cancelled) {
                  return true;
              }
          }
          return false;
      }
  
      public static boolean isDownloadingFile(String downloadUrl) {
          if (!isRunning) {
              return false;
          }
          for (Downloader downloader : downloads) {
              if (downloader.request.getSource().equals(downloadUrl) && !downloader.cancelled) {
                  return true;
              }
          }
          return false;
      }
  
      public static DownloadRequest findRequest(String downloadUrl) {
          for (Downloader downloader : downloads) {
              if (downloader.request.getSource().equals(downloadUrl)) {
                  return downloader.request;
              }
          }
          return null;
      }
  
      @Override
      public int onStartCommand(Intent intent, int flags, int startId) {
          if (intent != null && intent.hasExtra(EXTRA_REQUESTS)) {
              Notification notification = notificationManager.updateNotifications(downloads);
              startForeground(R.id.notification_downloading, notification);
              setupNotificationUpdaterIfNecessary();
              downloadEnqueueExecutor.execute(() -> onDownloadQueued(intent));
          } else if (intent != null && intent.getBooleanExtra(EXTRA_REFRESH_ALL, false)) {
              Notification notification = notificationManager.updateNotifications(downloads);
              startForeground(R.id.notification_downloading, notification);
              setupNotificationUpdaterIfNecessary();
              downloadEnqueueExecutor.execute(() -> enqueueAll(intent));
          } else if (downloads.size() == 0) {
              shutdown();
          } else {
              Log.d(TAG, "unknown intent");
          }
          return Service.START_NOT_STICKY;
      }
  
      @Override
      public void onDestroy() {
          Log.d(TAG, "onDestroy");
          isRunning = false;
  
          boolean showAutoDownloadReport = Prefs.showAutoDownloadReport();
          if (Prefs.showDownloadReport() || showAutoDownloadReport) {
              notificationManager.updateReport(reportQueue, showAutoDownloadReport);
              reportQueue.clear();
          }
  
          unregisterReceiver(cancelDownloadReceiver);
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
              connectionMonitor.disable(getApplicationContext());
          }
  
          EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList()));
          cancelNotificationUpdater();
          downloadEnqueueExecutor.shutdownNow();
          downloadHandleExecutor.shutdownNow();
          notificationUpdateExecutor.shutdownNow();
          if (downloadPostFuture != null) {
              downloadPostFuture.cancel(true);
          }
          downloads.clear();
  
          // start auto download in case anything new has shown up
          DBTasks.autodownloadUndownloadedItems(getApplicationContext());
      }
  
      /**
       * This method MUST NOT, in any case, throw an exception.
       * Otherwise, it hangs up the refresh thread pool.
       */
      private void performDownload(Downloader downloader) {
          try {
              downloader.call();
          } catch (Exception e) {
              e.printStackTrace();
          }
          try {
              if (downloader.getResult().isSuccessful()) {
                  handleSuccessfulDownload(downloader);
              } else {
                  handleFailedDownload(downloader);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
          downloadEnqueueExecutor.submit(() -> {
              downloads.remove(downloader);
              stopServiceIfEverythingDone();
          });
      }
  
      /**
       * This method MUST NOT, in any case, throw an exception.
       * Otherwise, it hangs up the refresh thread pool.
       */
      private void performLocalFeedRefresh(Downloader downloader, DownloadRequest request) {
          try {
              Feed feed = DBReader.getFeed(request.getFeedfileId());
              LocalFeedUpdater.updateFeed(feed, DownloadService.this, (scanned, totalFiles) -> {
                  request.setSize(totalFiles);
                  request.setSoFar(scanned);
                  request.setProgressPercent((int) (100.0 * scanned / totalFiles));
              });
          } catch (Exception e) {
              e.printStackTrace();
          }
          downloadEnqueueExecutor.submit(() -> {
              downloads.remove(downloader);
              stopServiceIfEverythingDone();
          });
      }
  
  
      private void handleSuccessfulDownload(Downloader downloader) {
          DownloadRequest request = downloader.getDownloadRequest();
          DownloadStatus status = downloader.getResult();
          final int type = status.getFeedfileType();
  
          if (type == Feed.FEEDFILETYPE_FEED) {
              Log.d(TAG, "feed completed download");
              FeedSyncTask feedSyncTask = new FeedSyncTask(DownloadService.this, request);
              boolean success = feedSyncTask.run();
  
              if (success) {
                  if (request.getFeedfileId() == 0) {
                      return; // No download logs for new subscriptions
                  }
                  // we create a 'successful' download log if the feed's last refresh failed
                  List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId());
                  if (log.size() > 0 && !log.get(0).isSuccessful()) {
                      saveDownloadStatus(feedSyncTask.getDownloadStatus());
                  }
                  if (!request.isInitiatedByUser()) {
                      // Was stored in the database before and not initiated manually
                      newEpisodesNotification.showIfNeeded(DownloadService.this, feedSyncTask.getSavedFeed());
                  }
                  if (downloader.permanentRedirectUrl != null) {
                      DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl);
                  } else if (feedSyncTask.getRedirectUrl() != null
                          && !feedSyncTask.getRedirectUrl().equals(request.getSource())) {
                      DBWriter.updateFeedDownloadURL(request.getSource(), feedSyncTask.getRedirectUrl());
                  }
              } else {
                  DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true);
                  saveDownloadStatus(feedSyncTask.getDownloadStatus());
              }
          } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
              Log.d(TAG, "FeedMedia completed download");
              MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, status, request);
              handler.run();
              saveDownloadStatus(handler.getUpdatedStatus());
          }
      }
  
      private void handleFailedDownload(Downloader downloader) {
          DownloadStatus status = downloader.getResult();
          final int type = status.getFeedfileType();
  
          if (!status.isCancelled()) {
              if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) {
                  notificationManager.postAuthenticationNotification(downloader.getDownloadRequest());
              } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
                      && Integer.parseInt(status.getReasonDetailed()) == 416) {
  
                  Log.d(TAG, "invalid range restarting download");
                  FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
                  download(this, false, downloader.getDownloadRequest());
              } else {
                  Log.e(TAG, "download failed");
                  saveDownloadStatus(status);
                  new FailedDownloadHandler(downloader.getDownloadRequest()).run();
  
                  if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
                      FeedItem item = getFeedItemFromId(status.getFeedfileId());
                      if (item == null) {
                          return;
                      }
                      item.increaseFailedAutoDownloadAttempts(System.currentTimeMillis());
                      DBWriter.setFeedItem(item);
                      // to make lists reload the failed item, we fake an item update
                      EventBus.getDefault().post(FeedItemEvent.updated(item));
                  }
              }
          } else {
              // if FeedMedia download has been canceled, fake FeedItem update
              // so that lists reload that it
              if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
                  FeedItem item = getFeedItemFromId(status.getFeedfileId());
                  if (item == null) {
                      return;
                  }
                  EventBus.getDefault().post(FeedItemEvent.updated(item));
              }
          }
      }
  
      private final BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() {
  
          @Override
          public void onReceive(Context context, Intent intent) {
              Log.d(TAG, "receiver cancel download intent " + intent.getAction());
              if (!isRunning) {
                  return;
              }
              if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) {
                  String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL);
                  if (url == null) {
                      throw new IllegalArgumentException("ACTION_CANCEL_DOWNLOAD intent needs download url extra");
                  }
                  downloadEnqueueExecutor.execute(() -> {
                      doCancel(url);
                      postDownloaders();
                      stopServiceIfEverythingDone();
                  });
              } else if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) {
                  downloadEnqueueExecutor.execute(() -> {
                      for (Downloader d : downloads) {
                          d.cancel();
                      }
                      Log.d(TAG, "cancel all downloads");
                      postDownloaders();
                      stopServiceIfEverythingDone();
                  });
              }
          }
      };
  
      private void doCancel(String url) {
          Log.d(TAG, "cancel download url " + url);
          for (Downloader downloader : downloads) {
              if (downloader.cancelled || !downloader.getDownloadRequest().getSource().equals(url)) {
                  continue;
              }
              downloader.cancel();
              DownloadRequest request = downloader.getDownloadRequest();
              FeedItem item = getFeedItemFromId(request.getFeedfileId());
              if (item != null) {
                  EventBus.getDefault().post(FeedItemEvent.updated(item));
                  // undo enqueue upon cancel
                  if (request.isMediaEnqueued()) {
                      Log.v(TAG, "Undoing enqueue upon cancelling download");
                      DBWriter.removeQueueItem(getApplicationContext(), false, item);
                  }
              }
          }
      }
  
      private void onDownloadQueued(Intent intent) {
          List<DownloadRequest> requests = intent.getParcelableArrayListExtra(EXTRA_REQUESTS);
          if (requests == null) {
              throw new IllegalArgumentException("ACTION_ENQUEUE_DOWNLOAD intent needs request extra");
          }
          Log.d(TAG, "receive download enqueue request " + requests.size());
  
          if (intent.getBooleanExtra(EXTRA_CLEANUP_MEDIA, false)) {
              EpisodeCleanupAlgorithmFactory.build().makeRoomForEpisodes(getApplicationContext(), requests.size());
          }
  
          for (DownloadRequest request : requests) {
              addNewRequest(request);
          }
          postDownloaders();
          stopServiceIfEverythingDone();
  
          // Add to-download items to the queue before actual download completed
          // so that the resulting queue order is the same as when download is clicked
          enqueueFeedItems(requests);
      }
  
      private void enqueueFeedItems(@NonNull List<DownloadRequest> requests) {
          List<FeedItem> feedItems = new ArrayList<>();
          for (DownloadRequest request : requests) {
              if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
                  long mediaId = request.getFeedfileId();
                  FeedMedia media = DBReader.getFeedMedia(mediaId);
                  if (media == null) {
                      Log.w(TAG, "enqueueFeedItems() : FeedFile Id " + mediaId + " is not found. ignore it.");
                      continue;
                  }
                  feedItems.add(media.getItem());
              }
          }
          List<FeedItem> actuallyEnqueued = Collections.emptyList();
          try {
              actuallyEnqueued = DBTasks.enqueueFeedItemsToDownload(getApplicationContext(), feedItems);
          } catch (InterruptedException | ExecutionException e) {
              e.printStackTrace();
          }
  
          for (DownloadRequest request : requests) {
              if (request.getFeedfileType() != FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
                  continue;
              }
              final long mediaId = request.getFeedfileId();
              for (FeedItem item : actuallyEnqueued) {
                  if (item.getMedia() != null && item.getMedia().getId() == mediaId) {
                      request.setMediaEnqueued(true);
                  }
              }
          }
      }
  
      private void enqueueAll(Intent intent) {
          boolean initiatedByUser = intent.getBooleanExtra(EXTRA_INITIATED_BY_USER, false);
          List<Feed> feeds = DBReader.getFeedList();
          int count = 0;
          for (Feed feed : feeds) {
              if (feed.getPreferences().getKeepUpdated()) {
                  if (++count > Feed.MAX_SUBSCRIBED_FEEDS_FOR_FREE && !GoRouter.getInstance().getService(PayService.class).isPurchase(null, false)) {
                      EventBus.getDefault().post(new SubscribedFeedLimitEvent());
                      Timber.i("reach feed limit , suspend refresh feeds");
                      break;
                  }
                  DownloadRequest.Builder builder = DownloadRequestCreator.create(feed);
                  builder.withInitiatedByUser(initiatedByUser);
                  if (feed.hasLastUpdateFailed()) {
                      builder.setForce(true);
                  }
                  addNewRequest(builder.build());
              }
          }
          postDownloaders();
          stopServiceIfEverythingDone();
      }
  
  
      synchronized private void addNewRequest(@NonNull DownloadRequest request) {
          if (isDownloadingFile(request.getSource())) {
              Log.d(TAG, "already in queue");
              return;
          } else if (downloadHandleExecutor.isShutdown()) {
              Log.d(TAG, "service is already shutting down.");
              return;
          }
          Log.d(TAG, "add new request -> " + request.getSource());
          if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) {
              Downloader downloader = new LocalFeedStubDownloader(request);
              downloads.add(downloader);
              downloadHandleExecutor.submit(() -> performLocalFeedRefresh(downloader, request));
          } else {
              writeFileUrl(request);
              Downloader downloader = downloaderFactory.create(request);
              if (downloader != null) {
                  downloads.add(downloader);
                  downloadHandleExecutor.submit(() -> performDownload(downloader));
              }
          }
      }
  
      @VisibleForTesting
      public static DownloaderFactory getDownloaderFactory() {
          return downloaderFactory;
      }
  
      // public scope rather than package private,
      // because androidTest put classes in the non-standard de.test.FocusPodcast hierarchy
      @VisibleForTesting
      public static void setDownloaderFactory(DownloaderFactory downloaderFactory) {
          DownloadService.downloaderFactory = downloaderFactory;
      }
  
      /**
       * Adds a new DownloadStatus object to the list of completed downloads and
       * saves it in the database
       *
       * @param status the download that is going to be saved
       */
      private void saveDownloadStatus(@NonNull DownloadStatus status) {
          reportQueue.add(status);
          DBWriter.addDownloadStatus(status);
      }
  
      /**
       * Check if there's something else to download, otherwise stop.
       */
      private void stopServiceIfEverythingDone() {
          Log.d(TAG, downloads.size() + " remain download");
          if (downloads.size() <= 0) {
              Log.d(TAG, "try to shutdown");
              shutdown();
          }
      }
  
      @Nullable
      private FeedItem getFeedItemFromId(long id) {
          FeedMedia media = DBReader.getFeedMedia(id);
          if (media != null) {
              return media.getItem();
          } else {
              return null;
          }
      }
  
      /**
       * Creates the destination file and writes FeedMedia File_url directly after starting download
       * to make it possible to resume download after the service was killed by the system.
       */
      private void writeFileUrl(DownloadRequest request) {
          if (request.getFeedfileType() != FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
              return;
          }
  
          File dest = new File(request.getDestination());
          if (!dest.exists()) {
              try {
                  dest.createNewFile();
              } catch (IOException e) {
                  Log.e(TAG, "unable to create file");
              }
          }
  
          if (dest.exists()) {
              Log.d(TAG, "save to file");
              FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId());
              if (media == null) {
                  Log.d(TAG, "no media");
                  return;
              }
              media.setFile_url(request.getDestination());
              try {
                  DBWriter.setFeedMedia(media).get();
              } catch (InterruptedException e) {
                  Log.e(TAG, "save file failed");
              } catch (ExecutionException e) {
                  Log.e(TAG, "save file failed " + e.getMessage());
              }
          }
      }
  
      /**
       * Schedules the notification updater task if it hasn't been scheduled yet.
       */
      private void setupNotificationUpdaterIfNecessary() {
          if (notificationUpdater == null) {
              notificationUpdater = new NotificationUpdater();
              notificationUpdaterFuture = notificationUpdateExecutor
                      .scheduleAtFixedRate(notificationUpdater, 1, 1, TimeUnit.SECONDS);
          }
      }
  
      private void cancelNotificationUpdater() {
          boolean result = false;
          if (notificationUpdaterFuture != null) {
              result = notificationUpdaterFuture.cancel(true);
          }
          notificationUpdater = null;
          notificationUpdaterFuture = null;
          Log.d(TAG, "NotificationUpdater cancelled. Result: " + result);
      }
  
      private class NotificationUpdater implements Runnable {
          public void run() {
              Notification n = notificationManager.updateNotifications(downloads);
              if (n != null) {
                  NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
                  nm.notify(R.id.notification_downloading, n);
              }
          }
      }
  
      private void postDownloaders() {
          new PostDownloaderTask(downloads).run();
  
          if (downloadPostFuture == null) {
              downloadPostFuture = notificationUpdateExecutor.scheduleAtFixedRate(
                      new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS);
          }
      }
  
      private void shutdown() {
          // If the service was run for a very short time, the system may delay closing
          // the notification. Set the notification text now so that a misleading message
          // is not left on the notification.
          if (notificationUpdater != null) {
              notificationUpdater.run();
          }
          cancelNotificationUpdater();
          ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
          stopSelf();
      }
  }
  