content: package de.danoeh.antennapod.net.download.service.episode;
  
  import android.Manifest;
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.net.wifi.WifiManager;
  import android.os.Build;
  import android.util.Log;
  import androidx.annotation.NonNull;
  import androidx.core.app.NotificationCompat;
  import androidx.core.content.ContextCompat;
  import androidx.work.Data;
  import androidx.work.ForegroundInfo;
  import androidx.work.Worker;
  import androidx.work.WorkerParameters;
  import com.google.common.util.concurrent.Futures;
  import com.google.common.util.concurrent.ListenableFuture;
  import de.danoeh.antennapod.net.download.service.R;
  import de.danoeh.antennapod.net.download.service.feed.remote.DefaultDownloaderFactory;
  import de.danoeh.antennapod.net.download.service.feed.remote.Downloader;
  import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequestCreator;
  import de.danoeh.antennapod.storage.database.DBReader;
  import de.danoeh.antennapod.storage.database.DBWriter;
  import de.danoeh.antennapod.event.MessageEvent;
  import de.danoeh.antennapod.model.download.DownloadError;
  import de.danoeh.antennapod.model.download.DownloadResult;
  import de.danoeh.antennapod.model.feed.FeedMedia;
  import de.danoeh.antennapod.model.download.DownloadRequest;
  import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
  import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
  import de.danoeh.antennapod.ui.notifications.NotificationUtils;
  import org.apache.commons.io.FileUtils;
  import org.greenrobot.eventbus.EventBus;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  import java.util.concurrent.ExecutionException;
  
  public class EpisodeDownloadWorker extends Worker {
      private static final String TAG = "EpisodeDownloadWorker";
      private static final Map<String, Integer> notificationProgress = new HashMap<>();
  
      private Downloader downloader = null;
  
      public EpisodeDownloadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
          super(context, params);
      }
  
      @Override
      @NonNull
      public Result doWork() {
          long mediaId = getInputData().getLong(DownloadServiceInterface.WORK_DATA_MEDIA_ID, 0);
          FeedMedia media = DBReader.getFeedMedia(mediaId);
          if (media == null) {
              return Result.failure();
          }
  
          DownloadRequest request = DownloadRequestCreator.create(media).build();
          Thread progressUpdaterThread = new Thread() {
              @Override
              public void run() {
                  while (true) {
                      try {
                          synchronized (notificationProgress) {
                              if (isInterrupted()) {
                                  return;
                              }
                              notificationProgress.put(media.getEpisodeTitle(), request.getProgressPercent());
                          }
                          setProgressAsync(
                                  new Data.Builder()
                                      .putInt(DownloadServiceInterface.WORK_DATA_PROGRESS, request.getProgressPercent())
                                      .build())
                                  .get();
                          NotificationManager nm = (NotificationManager) getApplicationContext()
                                  .getSystemService(Context.NOTIFICATION_SERVICE);
                          if (ContextCompat.checkSelfPermission(getApplicationContext(),
                                  Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
                              nm.notify(R.id.notification_downloading, generateProgressNotification());
                          }
                          Thread.sleep(1000);
                      } catch (InterruptedException | ExecutionException e) {
                          return;
                      }
                  }
              }
          };
          progressUpdaterThread.start();
          Result result;
          try {
              result = performDownload(media, request);
          } catch (Exception e) {
              e.printStackTrace();
              result = Result.failure();
          }
          if (result.equals(Result.failure()) && downloader != null) {
              FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
          }
          progressUpdaterThread.interrupt();
          try {
              progressUpdaterThread.join();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          synchronized (notificationProgress) {
              notificationProgress.remove(media.getEpisodeTitle());
              if (notificationProgress.isEmpty()) {
                  NotificationManager nm = (NotificationManager) getApplicationContext()
                          .getSystemService(Context.NOTIFICATION_SERVICE);
                  nm.cancel(R.id.notification_downloading);
              }
          }
          Log.d(TAG, "Worker for " + media.getDownloadUrl() + " returned.");
          return result;
      }
  
      @Override
      public void onStopped() {
          super.onStopped();
          if (downloader != null) {
              downloader.cancel();
          }
      }
  
      @NonNull
      @Override
      public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
          return Futures.immediateFuture(
                  new ForegroundInfo(R.id.notification_downloading, generateProgressNotification()));
      }
  
      private Result performDownload(FeedMedia media, DownloadRequest request) {
          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()) {
              media.setLocalFileUrl(request.getDestination());
              try {
                  DBWriter.setFeedMedia(media).get();
              } catch (Exception e) {
                  Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage());
              }
          }
  
          downloader = new DefaultDownloaderFactory().create(request);
          if (downloader == null) {
              Log.d(TAG, "Unable to create downloader");
              return Result.failure();
          }
  
          WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
          WifiManager.WifiLock wifiLock = null;
          if (wifiManager != null) {
              wifiLock = wifiManager.createWifiLock(TAG);
              wifiLock.acquire();
          }
  
          DownloadAnnouncer.announceStart(getApplicationContext(), request.getTitle());
          try {
              downloader.call();
          } catch (Exception e) {
              DBWriter.addDownloadStatus(downloader.getResult());
              sendErrorNotification(request.getTitle());
              return Result.failure();
          } finally {
              if (wifiLock != null) {
                  wifiLock.release();
              }
          }
  
          if (downloader.cancelled) {
              // This also happens when the worker was preempted, not just when the user cancelled it
              return Result.success();
          }
  
          DownloadResult status = downloader.getResult();
          if (status.isSuccessful()) {
              MediaDownloadedHandler handler = new MediaDownloadedHandler(
                      getApplicationContext(), downloader.getResult(), request);
              handler.run();
              DBWriter.addDownloadStatus(handler.getUpdatedStatus());
              DownloadAnnouncer.announceCompleted(getApplicationContext(), request.getTitle());
              return Result.success();
          }
  
          if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
                  && Integer.parseInt(status.getReasonDetailed()) == 416) {
              Log.d(TAG, "Requested invalid range, restarting download from the beginning");
              FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination()));
              sendMessage(request.getTitle(), false);
              return retry3times();
          }
  
          Log.e(TAG, "Download failed");
          DBWriter.addDownloadStatus(status);
          if (status.getReason() == DownloadError.ERROR_FORBIDDEN
                  || status.getReason() == DownloadError.ERROR_NOT_FOUND
                  || status.getReason() == DownloadError.ERROR_UNAUTHORIZED
                  || status.getReason() == DownloadError.ERROR_IO_BLOCKED) {
              // Fail fast, these are probably unrecoverable
              sendErrorNotification(request.getTitle());
              return Result.failure();
          }
          sendMessage(request.getTitle(), false);
          return retry3times();
      }
  
      private Result retry3times() {
          if (isLastRunAttempt()) {
              sendErrorNotification(downloader.getDownloadRequest().getTitle());
              return Result.failure();
          } else {
              return Result.retry();
          }
      }
  
      private boolean isLastRunAttempt() {
          return getRunAttemptCount() >= 2;
      }
  
      private void sendMessage(String episodeTitle, boolean isImmediateFail) {
          boolean retrying = !isLastRunAttempt() && !isImmediateFail;
          if (episodeTitle.length() > 20) {
              episodeTitle = episodeTitle.substring(0, 19) + "…";
          }
          EventBus.getDefault().post(new MessageEvent(
                      getApplicationContext().getString(
                              retrying ? R.string.download_error_retrying : R.string.download_error_not_retrying,
                              episodeTitle), (ctx) -> new MainActivityStarter(ctx).withDownloadLogsOpen().start(),
                  getApplicationContext().getString(R.string.download_error_details)));
      }
  
      private PendingIntent getDownloadLogsIntent(Context context) {
          Intent intent = new MainActivityStarter(context).withDownloadLogsOpen().getIntent();
          return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
                  PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
      }
  
      private PendingIntent getDownloadsIntent(Context context) {
          Intent intent = new MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent();
          return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
                  PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
      }
  
      private void sendErrorNotification(String title) {
          if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) {
              sendMessage(title, false);
              return;
          }
  
          NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(),
                  NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR);
          builder.setTicker(getApplicationContext().getString(R.string.episode_download_failed))
                  .setContentTitle(getApplicationContext().getString(R.string.episode_download_failed))
                  .setContentText(getApplicationContext().getString(R.string.download_error_tap_for_details))
                  .setSmallIcon(R.drawable.ic_notification_sync_error)
                  .setContentIntent(getDownloadLogsIntent(getApplicationContext()))
                  .setAutoCancel(true);
          builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
          NotificationManager nm = (NotificationManager) getApplicationContext()
                  .getSystemService(Context.NOTIFICATION_SERVICE);
          if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS)
                  == PackageManager.PERMISSION_GRANTED) {
              nm.notify(R.id.notification_download_report, builder.build());
          }
      }
  
      private Notification generateProgressNotification() {
          StringBuilder bigTextB = new StringBuilder();
          Map<String, Integer> progressCopy;
          synchronized (notificationProgress) {
              progressCopy = new HashMap<>(notificationProgress);
          }
          for (Map.Entry<String, Integer> entry : progressCopy.entrySet()) {
              bigTextB.append(String.format(Locale.getDefault(), "%s (%d%%)\n", entry.getKey(), entry.getValue()));
          }
          String bigText = bigTextB.toString().trim();
          String contentText;
          if (progressCopy.size() == 1) {
              contentText = bigText;
          } else {
              contentText = getApplicationContext().getResources().getQuantityString(R.plurals.downloads_left,
                      progressCopy.size(), progressCopy.size());
          }
          NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(),
                  NotificationUtils.CHANNEL_ID_DOWNLOADING);
          builder.setTicker(getApplicationContext().getString(R.string.download_notification_title_episodes))
                  .setContentTitle(getApplicationContext().getString(R.string.download_notification_title_episodes))
                  .setContentText(contentText)
                  .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText))
                  .setContentIntent(getDownloadsIntent(getApplicationContext()))
                  .setAutoCancel(false)
                  .setOngoing(true)
                  .setWhen(0)
                  .setOnlyAlertOnce(true)
                  .setShowWhen(false)
                  .setSmallIcon(R.drawable.ic_notification_sync)
                  .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
          return builder.build();
      }
  }
  