diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2023-05-05 23:09:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-05 23:09:03 +0200 |
commit | 6d7bfef8a5fe8180f13904739996bb2b8de8a0d4 (patch) | |
tree | 84f246b74fe7254678788e9f206d81d1a30ffa5e /core | |
parent | 4c286931cd2dbd9038022f808f9d8a73ccbb6759 (diff) | |
download | AntennaPod-6d7bfef8a5fe8180f13904739996bb2b8de8a0d4.zip |
Download Service Rewrite (#6420)
Diffstat (limited to 'core')
29 files changed, 409 insertions, 1349 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 6f5508f27..e186a856f 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -16,10 +16,6 @@ android:icon="@mipmap/ic_launcher" android:supportsRtl="true"> - <service - android:name=".service.download.DownloadService" - android:enabled="true" /> - <service android:name=".service.playback.PlaybackService" android:label="@string/app_name" android:enabled="true" diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java deleted file mode 100644 index efd53ab9d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.core.service.download.Downloader; - -public class DownloadEvent { - - public final DownloaderUpdate update; - - private DownloadEvent(DownloaderUpdate downloader) { - this.update = downloader; - } - - public static DownloadEvent refresh(List<Downloader> list) { - list = new ArrayList<>(list); - DownloaderUpdate update = new DownloaderUpdate(list); - return new DownloadEvent(update); - } - - @NonNull - @Override - public String toString() { - return "DownloadEvent{" + - "update=" + update + - '}'; - } - - public boolean hasChangedFeedUpdateStatus(boolean oldStatus) { - return oldStatus != update.feedIds.length > 0; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java deleted file mode 100644 index 1cab7e0f0..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.NonNull; - -import java.util.Arrays; -import java.util.List; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.util.LongList; - -public class DownloaderUpdate { - - /* Downloaders that are currently running */ - @NonNull - public final List<Downloader> downloaders; - - /** - * IDs of feeds that are currently being downloaded - * Often used to show some progress wheel in the action bar - */ - public final long[] feedIds; - - /** - * IDs of feed media that are currently being downloaded - * Can be used to show and update download progress bars - */ - public final long[] mediaIds; - - DownloaderUpdate(@NonNull List<Downloader> downloaders) { - this.downloaders = downloaders; - LongList feedIds1 = new LongList(); - LongList mediaIds1 = new LongList(); - for(Downloader d1 : downloaders) { - int type = d1.getDownloadRequest().getFeedfileType(); - long id = d1.getDownloadRequest().getFeedfileId(); - if(type == Feed.FEEDFILETYPE_FEED) { - feedIds1.add(id); - } else if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - mediaIds1.add(id); - } - } - - this.feedIds = feedIds1.toArray(); - this.mediaIds = mediaIds1.toArray(); - } - - @NonNull - @Override - public String toString() { - return "DownloaderUpdate{" + - "downloaders=" + downloaders + - ", feedIds=" + Arrays.toString(feedIds) + - ", mediaIds=" + Arrays.toString(mediaIds) + - '}'; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index d4d948b2a..03881ee4f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -28,7 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.documentfile.provider.DocumentFile; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.util.FastDocumentFile; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -246,8 +246,8 @@ public class LocalFeedUpdater { } private static void reportError(Feed feed, String reasonDetailed) { - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.ERROR_IO_ERROR, false, reasonDetailed, true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, reasonDetailed); DBWriter.addDownloadStatus(status); DBWriter.setFeedLastUpdateFailed(feed.getId(), true); } @@ -256,8 +256,7 @@ public class LocalFeedUpdater { * Reports a successful download status. */ private static void reportSuccess(Feed feed) { - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.SUCCESS, true, null, true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), DownloadError.SUCCESS, true, null); DBWriter.addDownloadStatus(status); DBWriter.setFeedLastUpdateFailed(feed.getId(), false); } @@ -266,21 +265,21 @@ public class LocalFeedUpdater { * Answers if reporting success is needed for the given feed. */ private static boolean mustReportDownloadSuccessful(Feed feed) { - List<DownloadStatus> downloadStatuses = DBReader.getFeedDownloadLog(feed.getId()); + List<DownloadResult> downloadResults = DBReader.getFeedDownloadLog(feed.getId()); - if (downloadStatuses.isEmpty()) { + if (downloadResults.isEmpty()) { // report success if never reported before return true; } - Collections.sort(downloadStatuses, (downloadStatus1, downloadStatus2) -> + Collections.sort(downloadResults, (downloadStatus1, downloadStatus2) -> downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())); - DownloadStatus lastDownloadStatus = downloadStatuses.get(downloadStatuses.size() - 1); + DownloadResult lastDownloadResult = downloadResults.get(downloadResults.size() - 1); // report success if the last update was not successful // (avoid logging success again if the last update was ok) - return !lastDownloadStatus.isSuccessful(); + return !lastDownloadResult.isSuccessful(); } @FunctionalInterface diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index 8d9f046e2..5f59f0c41 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -20,12 +20,13 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; @@ -80,6 +81,7 @@ public class FeedUpdateWorker extends Worker { refreshFeeds(toUpdate, true); } notificationManager.cancel(R.id.notification_updating_feeds); + DBTasks.autodownloadUndownloadedItems(getApplicationContext()); return Result.success(); } @@ -115,8 +117,8 @@ public class FeedUpdateWorker extends Worker { } } catch (Exception e) { DBWriter.setFeedLastUpdateFailed(feed.getId(), true); - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.ERROR_IO_ERROR, false, e.getMessage(), true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, e.getMessage()); DBWriter.addDownloadStatus(status); } toUpdate.remove(0); @@ -144,7 +146,7 @@ public class FeedUpdateWorker extends Worker { downloader.call(); if (!downloader.getResult().isSuccessful()) { - if (downloader.getResult().isCancelled()) { + if (downloader.cancelled) { return; } DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); @@ -165,7 +167,7 @@ public class FeedUpdateWorker extends Worker { 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()); + List<DownloadResult> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); if (log.size() > 0 && !log.get(0).isSuccessful()) { DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java index d6a4b8378..5ca904ff6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java @@ -21,9 +21,6 @@ public class DownloadRequestCreator { public static DownloadRequest.Builder create(Feed feed) { File dest = new File(getFeedfilePath(), getFeedfileName(feed)); - if (!isFilenameAvailable(dest.toString()) && !feed.isLocalFeed()) { - dest = findUnusedFile(dest); - } Log.d(TAG, "Requesting download of url " + feed.getDownload_url()); String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; @@ -45,7 +42,7 @@ public class DownloadRequestCreator { dest = new File(getMediafilePath(media), getMediafilename(media)); } - if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) { + if (dest.exists() && !partiallyDownloadedFileExists) { dest = findUnusedFile(dest); } Log.d(TAG, "Requesting download of url " + media.getDownload_url()); @@ -72,7 +69,7 @@ public class DownloadRequestCreator { + FilenameUtils.getExtension(dest.getName()); Log.d(TAG, "Testing filename " + newName); newDest = new File(dest.getParent(), newName); - if (!newDest.exists() && isFilenameAvailable(newDest.toString())) { + if (!newDest.exists()) { Log.d(TAG, "File doesn't exist yet. Using " + newName); break; } @@ -80,19 +77,6 @@ public class DownloadRequestCreator { return newDest; } - /** - * Returns true if a filename is available and false if it has already been - * taken by another requested download. - */ - private static boolean isFilenameAvailable(String path) { - for (Downloader downloader : DownloadService.downloads) { - if (downloader.request.getDestination().equals(path)) { - return false; - } - } - return true; - } - private static String getFeedfilePath() { return UserPreferences.getDataFolder(FEED_DOWNLOADPATH).toString() + "/"; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java deleted file mode 100644 index 9c238137e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ /dev/null @@ -1,536 +0,0 @@ -package de.danoeh.antennapod.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.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.NotificationManagerCompat; -import androidx.core.app.ServiceCompat; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; -import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; -import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; -import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -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; - -/** - * 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.de.danoeh.antennapod.core.service.cancelDownload"; - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAll"; - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - public static final String EXTRA_REQUESTS = "downloadRequests"; - 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 final List<DownloadRequest> failedRequestsForReport = new ArrayList<>(); - private DownloadServiceNotification notificationManager; - 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() { - - downloadEnqueueExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "EnqueueThread"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); - downloadHandleExecutor = Executors.newFixedThreadPool(UserPreferences.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, "Service started"); - isRunning = true; - notificationManager = new DownloadServiceNotification(this); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - - connectionMonitor = new ConnectionStateMonitor(); - connectionMonitor.enable(getApplicationContext()); - } - - 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); - NotificationManagerCompat.from(this).cancel(R.id.notification_download_report); - NotificationManagerCompat.from(this).cancel(R.id.notification_auto_download_report); - setupNotificationUpdaterIfNecessary(); - downloadEnqueueExecutor.execute(() -> onDownloadQueued(intent)); - } else if (downloads.size() == 0) { - shutdown(); - } else { - Log.d(TAG, "onStartCommand: Unknown intent"); - } - return Service.START_NOT_STICKY; - } - - @Override - public void onDestroy() { - Log.d(TAG, "Service shutting down"); - isRunning = false; - - boolean showAutoDownloadReport = UserPreferences.showAutoDownloadReport(); - if (UserPreferences.showDownloadReport() || showAutoDownloadReport) { - notificationManager.updateReport(reportQueue, showAutoDownloadReport, failedRequestsForReport); - reportQueue.clear(); - failedRequestsForReport.clear(); - } - - unregisterReceiver(cancelDownloadReceiver); - 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(); - }); - } - - private void handleSuccessfulDownload(Downloader downloader) { - DownloadRequest request = downloader.getDownloadRequest(); - DownloadStatus status = downloader.getResult(); - final int type = status.getFeedfileType(); - - if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "Handling completed FeedMedia Download"); - MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, status, request); - handler.run(); - saveDownloadStatus(handler.getUpdatedStatus(), downloader.getDownloadRequest()); - } - } - - 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, "Requested invalid range, restarting download from the beginning"); - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - DownloadServiceInterface.get().download(this, false, downloader.getDownloadRequest()); - } else { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status, downloader.getDownloadRequest()); - 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, "cancelDownloadReceiver: " + 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, "Cancelled all downloads"); - postDownloaders(); - stopServiceIfEverythingDone(); - }); - } - } - }; - - private void doCancel(String url) { - Log.d(TAG, "Cancelling download with 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, "Received enqueue request. #requests=" + 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 addNewRequest(@NonNull DownloadRequest request) { - if (isDownloadingFile(request.getSource())) { - Log.d(TAG, "Skipped enqueueing request. Already running."); - return; - } else if (downloadHandleExecutor.isShutdown()) { - Log.d(TAG, "Skipped enqueueing request. Service is already shutting down."); - return; - } - Log.d(TAG, "Add new request: " + request.getSource()); - 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.antennapod 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, @NonNull DownloadRequest request) { - reportQueue.add(status); - if (!status.isSuccessful() && !status.isCancelled()) { - failedRequestsForReport.add(request); - } - DBWriter.addDownloadStatus(status); - } - - /** - * Check if there's something else to download, otherwise stop. - */ - private void stopServiceIfEverythingDone() { - Log.d(TAG, downloads.size() + " downloads left"); - if (downloads.size() <= 0) { - Log.d(TAG, "Attempting 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, "Writing file url"); - 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, "writeFileUrl was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage()); - } - } - } - - /** - * Schedules the notification updater task if it hasn't been scheduled yet. - */ - private void setupNotificationUpdaterIfNecessary() { - if (notificationUpdater == null) { - Log.d(TAG, "Setting up notification updater"); - 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(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java index 976d8255f..87cbeda84 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java @@ -1,74 +1,72 @@ package de.danoeh.antennapod.core.service.download; import android.content.Context; -import android.content.Intent; -import androidx.core.content.ContextCompat; -import com.google.android.exoplayer2.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.WorkManager; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; -import java.util.ArrayList; - -import static de.danoeh.antennapod.core.service.download.DownloadService.isDownloadingFile; +import java.util.concurrent.TimeUnit; public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { - private static final String TAG = "DownloadServiceInterface"; - - public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { - Intent intent = makeDownloadIntent(context, cleanupMedia, requests); - if (intent != null) { - ContextCompat.startForegroundService(context, intent); + public void downloadNow(Context context, FeedItem item, boolean ignoreConstraints) { + OneTimeWorkRequest.Builder workRequest = getRequest(context, item); + workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST); + if (ignoreConstraints) { + workRequest.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()); + } else { + workRequest.setConstraints(getConstraints()); } + WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownload_url(), + ExistingWorkPolicy.KEEP, workRequest.build()); } - public Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests) { - ArrayList<DownloadRequest> requestsToSend = new ArrayList<>(); - for (DownloadRequest request : requests) { - if (!isDownloadingFile(request.getSource())) { - requestsToSend.add(request); - } - } - if (requestsToSend.isEmpty()) { - return null; - } else if (requestsToSend.size() > 100) { - if (BuildConfig.DEBUG) { - throw new IllegalArgumentException("Android silently drops intent payloads that are too large"); - } else { - Log.d(TAG, "Too many download requests. Dropping some to avoid Android dropping all."); - requestsToSend = new ArrayList<>(requestsToSend.subList(0, 100)); - } - } + public void download(Context context, FeedItem item) { + OneTimeWorkRequest.Builder workRequest = getRequest(context, item); + workRequest.setConstraints(getConstraints()); + WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownload_url(), + ExistingWorkPolicy.KEEP, workRequest.build()); + } - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putParcelableArrayListExtra(DownloadService.EXTRA_REQUESTS, requestsToSend); - if (cleanupMedia) { - launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, true); + private static OneTimeWorkRequest.Builder getRequest(Context context, FeedItem item) { + OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(EpisodeDownloadWorker.class) + .setInitialDelay(0L, TimeUnit.MILLISECONDS) + .addTag(DownloadServiceInterface.WORK_TAG) + .addTag(DownloadServiceInterface.WORK_TAG_EPISODE_URL + item.getMedia().getDownload_url()); + Data.Builder builder = new Data.Builder(); + builder.putLong(WORK_DATA_MEDIA_ID, item.getMedia().getId()); + if (!item.isTagged(FeedItem.TAG_QUEUE) && UserPreferences.enqueueDownloadedEpisodes()) { + DBWriter.addQueueItem(context, false, item.getId()); + builder.putBoolean(WORK_DATA_WAS_QUEUED, true); } - return launchIntent; + workRequest.setInputData(builder.build()); + return workRequest; } - public void refreshAllFeeds(Context context, boolean initiatedByUser) { - FeedUpdateManager.runOnce(context); + private static Constraints getConstraints() { + Constraints.Builder constraints = new Constraints.Builder(); + if (UserPreferences.isAllowMobileEpisodeDownload()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + return constraints.build(); } + @Override public void cancel(Context context, String url) { - if (!DownloadService.isRunning) { - return; - } - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); - cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, url); - cancelIntent.setPackage(context.getPackageName()); - context.sendBroadcast(cancelIntent); + WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG_EPISODE_URL + url); } + @Override public void cancelAll(Context context) { - if (!DownloadService.isRunning) { - return; - } - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); - cancelIntent.setPackage(context.getPackageName()); - context.sendBroadcast(cancelIntent); + WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java deleted file mode 100644 index b9846c06c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ /dev/null @@ -1,306 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.util.Log; -import androidx.core.app.NotificationCompat; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.ui.appstartintent.DownloadAuthenticationActivityStarter; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; - -import java.util.List; - -public class DownloadServiceNotification { - private static final String TAG = "DownloadSvcNotification"; - - private final Context context; - private NotificationCompat.Builder notificationCompatBuilder; - - public DownloadServiceNotification(Context context) { - this.context = context; - setupNotificationBuilders(); - } - - private void setupNotificationBuilders() { - notificationCompatBuilder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) - .setOngoing(false) - .setWhen(0) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setContentIntent(getNotificationContentIntent(context)) - .setSmallIcon(R.drawable.ic_notification_sync) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * after setupNotificationBuilders. - */ - public Notification updateNotifications(List<Downloader> downloads) { - if (notificationCompatBuilder == null) { - return null; - } - - String contentTitle; - if (typeIsOnly(downloads, Feed.FEEDFILETYPE_FEED)) { - contentTitle = context.getString(R.string.download_notification_title_feeds); - } else if (typeIsOnly(downloads, FeedMedia.FEEDFILETYPE_FEEDMEDIA)) { - contentTitle = context.getString(R.string.download_notification_title_episodes); - } else { - contentTitle = context.getString(R.string.download_notification_title); - } - - int numDownloads = getNumberOfRunningDownloads(downloads); - String contentText = context.getString(R.string.completing); - String bigText = context.getString(R.string.completing); - notificationCompatBuilder.clearActions(); - if (numDownloads > 0) { - bigText = compileNotificationString(downloads); - if (numDownloads == 1) { - contentText = bigText; - } else { - contentText = context.getResources().getQuantityString(R.plurals.downloads_left, - numDownloads, numDownloads); - } - - Intent cancelDownloadsIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadsIntent.setPackage(context.getPackageName()); - PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(context, - R.id.pending_intent_download_cancel_all, cancelDownloadsIntent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - notificationCompatBuilder.addAction(new NotificationCompat.Action( - R.drawable.ic_notification_cancel, context.getString(R.string.cancel_label), cancelPendingIntent)); - } - - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(contentText); - notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); - return notificationCompatBuilder.build(); - } - - private int getNumberOfRunningDownloads(List<Downloader> downloads) { - int running = 0; - for (Downloader downloader : downloads) { - if (!downloader.cancelled && !downloader.isFinished()) { - running++; - } - } - return running; - } - - private boolean typeIsOnly(List<Downloader> downloads, int feedFileType) { - for (Downloader downloader : downloads) { - if (downloader.cancelled) { - continue; - } - DownloadRequest request = downloader.getDownloadRequest(); - if (request.getFeedfileType() != feedFileType) { - return false; - } - } - return true; - } - - private static String compileNotificationString(List<Downloader> downloads) { - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < downloads.size(); i++) { - Downloader downloader = downloads.get(i); - if (downloader.cancelled) { - continue; - } - stringBuilder.append("• "); - DownloadRequest request = downloader.getDownloadRequest(); - if (request.getTitle() != null) { - stringBuilder.append(request.getTitle()); - } else { - stringBuilder.append(request.getSource()); - } - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - stringBuilder.append(" (").append(request.getProgressPercent()).append("%)"); - } else if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) { - stringBuilder.append(" (").append(request.getSoFar()) - .append("/").append(request.getSize()).append(")"); - } - if (i != downloads.size() - 1) { - stringBuilder.append("\n"); - } - } - return stringBuilder.toString(); - } - - private static String createAutoDownloadNotificationContent(List<DownloadStatus> statuses) { - int length = statuses.size(); - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) { - sb.append("• ").append(statuses.get(i).getTitle()); - if (i != length - 1) { - sb.append("\n"); - } - } - - return sb.toString(); - } - - private String createFailedDownloadNotificationContent(List<DownloadStatus> statuses) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < statuses.size(); i++) { - if (statuses.get(i) == null || statuses.get(i).isSuccessful()) { - continue; - } - sb.append("• ").append(statuses.get(i).getTitle()); - if (statuses.get(i).getReason() != null) { - sb.append(": ").append(context.getString(DownloadErrorLabel.from(statuses.get(i).getReason()))); - } - if (i != statuses.size() - 1) { - sb.append("\n"); - } - } - - return sb.toString(); - } - - /** - * Creates a notification at the end of the service lifecycle to notify the - * user about the number of completed downloads. A report will only be - * created if there is at least one failed download excluding images - */ - public void updateReport(List<DownloadStatus> reportQueue, boolean showAutoDownloadReport, - List<DownloadRequest> failedRequests) { - // check if report should be created - boolean createReport = false; - int failedDownloads = 0; - - // a download report is created if at least one download has failed - // (excluding failed image downloads) - for (DownloadStatus status : reportQueue) { - if (status == null || status.isCancelled()) { - continue; - } - if (status.isSuccessful()) { - createReport |= showAutoDownloadReport && !status.isInitiatedByUser() - && status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA; - } else { - failedDownloads++; - createReport = true; - } - } - - if (!createReport) { - Log.d(TAG, "No report is created"); - return; - } - Log.d(TAG, "Creating report"); - if (failedDownloads == 0) { - createAutoDownloadReportNotification(reportQueue); - } else { - createDownloadFailedNotification(reportQueue, failedRequests); - } - Log.d(TAG, "Download report notification was posted"); - } - - private void createAutoDownloadReportNotification(List<DownloadStatus> reportQueue) { - PendingIntent intent = getAutoDownloadReportNotificationContentIntent(context); - String content = createAutoDownloadNotificationContent(reportQueue); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, - NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD); - builder.setTicker(context.getString(R.string.auto_download_report_title)) - .setContentTitle(context.getString(R.string.auto_download_report_title)) - .setContentText(content) - .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) - .setSmallIcon(R.drawable.ic_notification_new) - .setContentIntent(intent) - .setAutoCancel(true) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(R.id.notification_auto_download_report, builder.build()); - } - - private void createDownloadFailedNotification(List<DownloadStatus> reportQueue, - List<DownloadRequest> failedRequests) { - Intent retryIntent = DownloadServiceInterface.get().makeDownloadIntent(context, - false, failedRequests.toArray(new DownloadRequest[0])); - PendingIntent retryPendingIntent = null; - if (retryIntent != null && Build.VERSION.SDK_INT >= 26) { - retryPendingIntent = PendingIntent.getForegroundService(context, R.id.pending_intent_download_service_retry, - retryIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else if (retryIntent != null) { - retryPendingIntent = PendingIntent.getService(context, - R.id.pending_intent_download_service_retry, retryIntent, - PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - PendingIntent intent = getReportNotificationContentIntent(context); - String content = createFailedDownloadNotificationContent(reportQueue); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, - NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); - builder.setTicker(context.getString(R.string.download_report_title)) - .setContentTitle(context.getString(R.string.download_report_title)) - .setContentText(content) - .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) - .setSmallIcon(R.drawable.ic_notification_sync_error) - .setContentIntent(intent) - .setAutoCancel(true); - if (retryPendingIntent != null) { - builder.addAction(new NotificationCompat.Action( - R.drawable.ic_notification_sync, context.getString(R.string.retry_label), retryPendingIntent)); - } - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(R.id.notification_download_report, builder.build()); - } - - public void postAuthenticationNotification(final DownloadRequest downloadRequest) { - final String resourceTitle = (downloadRequest.getTitle() != null) ? - downloadRequest.getTitle() : downloadRequest.getSource(); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_USER_ACTION); - builder.setTicker(context.getText(R.string.authentication_notification_title)) - .setContentTitle(context.getText(R.string.authentication_notification_title)) - .setContentText(context.getText(R.string.authentication_notification_msg)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getText(R.string.authentication_notification_msg) - + ": " + resourceTitle)) - .setSmallIcon(R.drawable.ic_notification_key) - .setAutoCancel(true) - .setContentIntent(new DownloadAuthenticationActivityStarter( - context, downloadRequest.getFeedfileId(), downloadRequest).getPendingIntent()); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), builder.build()); - } - - public PendingIntent getReportNotificationContentIntent(Context context) { - Intent intent = new MainActivityStarter(context) - .withFragmentLoaded("DownloadsFragment") - .withFragmentArgs("show_logs", true) - .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)); - } - - public PendingIntent getAutoDownloadReportNotificationContentIntent(Context context) { - Intent intent = new MainActivityStarter(context).withFragmentLoaded("QueueFragment").getIntent(); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - public PendingIntent getNotificationContentIntent(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)); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java index f7f5e8e9c..35247509d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -9,7 +9,7 @@ import java.util.concurrent.Callable; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; /** @@ -25,15 +25,15 @@ public abstract class Downloader implements Callable<Downloader> { @NonNull final DownloadRequest request; @NonNull - final DownloadStatus result; + final DownloadResult result; Downloader(@NonNull DownloadRequest request) { super(); this.request = request; this.request.setStatusMsg(R.string.download_pending); this.cancelled = false; - this.result = new DownloadStatus(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), - false, cancelled, false, null, new Date(), null, request.isInitiatedByUser()); + this.result = new DownloadResult(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), + false, null, new Date(), null); } protected abstract void download(); @@ -63,7 +63,7 @@ public abstract class Downloader implements Callable<Downloader> { } @NonNull - public DownloadStatus getResult() { + public DownloadResult getResult() { return result; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java new file mode 100644 index 000000000..c428bc861 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java @@ -0,0 +1,265 @@ +package de.danoeh.antennapod.core.service.download; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import de.danoeh.antennapod.core.ClientConfigurator; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; +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.net.download.serviceinterface.DownloadRequest; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +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() { + ClientConfigurator.initialize(getApplicationContext()); + 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 (!isInterrupted()) { + try { + Thread.sleep(1000); + notificationProgress.put(media.getEpisodeTitle(), request.getProgressPercent()); + setProgressAsync( + new Data.Builder() + .putInt(DownloadServiceInterface.WORK_DATA_PROGRESS, request.getProgressPercent()) + .build()) + .get(); + sendProgressNotification(); + } catch (InterruptedException | ExecutionException e) { + return; + } + } + } + }; + progressUpdaterThread.start(); + final Result result = performDownload(media, request); + progressUpdaterThread.interrupt(); + try { + progressUpdaterThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + 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.getDownload_url() + " returned."); + return result; + } + + @Override + public void onStopped() { + super.onStopped(); + if (downloader != null) { + downloader.cancel(); + } + } + + 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.setFile_url(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(); + } + + try { + downloader.call(); + } catch (Exception e) { + DBWriter.addDownloadStatus(downloader.getResult()); + if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { + sendMessage(request.getTitle(), false); + } else { + sendErrorNotification(); + } + return Result.failure(); + } + + if (downloader.cancelled) { + if (getInputData().getBoolean(DownloadServiceInterface.WORK_DATA_WAS_QUEUED, false)) { + try { + DBWriter.removeQueueItem(getApplicationContext(), false, media.getItem()).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + return Result.success(); + } + + DownloadResult status = downloader.getResult(); + if (status.isSuccessful()) { + MediaDownloadedHandler handler = new MediaDownloadedHandler( + getApplicationContext(), downloader.getResult(), request); + handler.run(); + DBWriter.addDownloadStatus(handler.getUpdatedStatus()); + 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(), true); + 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 + if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { + sendMessage(request.getTitle(), false); + } else { + sendErrorNotification(); + } + return Result.failure(); + } + sendMessage(request.getTitle(), true); + return retry3times(); + } + + private Result retry3times() { + if (getRunAttemptCount() < 2) { + return Result.retry(); + } else { + sendErrorNotification(); + return Result.failure(); + } + } + + private void sendMessage(String episodeTitle, boolean retrying) { + 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() { + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), + NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); + builder.setTicker(getApplicationContext().getString(R.string.download_report_title)) + .setContentTitle(getApplicationContext().getString(R.string.download_report_title)) + .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); + nm.notify(R.id.notification_download_report, builder.build()); + } + + private void sendProgressNotification() { + StringBuilder bigTextB = new StringBuilder(); + Map<String, Integer> 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 (notificationProgress.size() == 1) { + contentText = bigText; + } else { + contentText = getApplicationContext().getResources().getQuantityString(R.plurals.downloads_left, + notificationProgress.size(), notificationProgress.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); + NotificationManager nm = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(R.id.notification_downloading, builder.build()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index a0a0615cb..949f9966b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -5,7 +5,7 @@ import android.text.TextUtils; import android.util.Log; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import okhttp3.CacheControl; import okhttp3.internal.http.StatusLine; @@ -149,12 +149,12 @@ public class HttpDownloader extends Downloader { request.setSize(responseBody.contentLength() + request.getSoFar()); Log.d(TAG, "Size is " + request.getSize()); if (request.getSize() < 0) { - request.setSize(DownloadStatus.SIZE_UNKNOWN); + request.setSize(DownloadResult.SIZE_UNKNOWN); } long freeSpace = StorageUtils.getFreeSpaceAvailable(); Log.d(TAG, "Free space is " + freeSpace); - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSize() > freeSpace) { + if (request.getSize() != DownloadResult.SIZE_UNKNOWN && request.getSize() > freeSpace) { onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); return; } @@ -175,7 +175,7 @@ public class HttpDownloader extends Downloader { } else { // check if size specified in the response header is the same as the size of the // written file. This check cannot be made if compression was used - if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN + if (!isGzip && request.getSize() != DownloadResult.SIZE_UNKNOWN && request.getSoFar() != request.getSize()) { onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " + request.getSoFar() + " does not equal expected size " + request.getSize()); @@ -267,7 +267,8 @@ public class HttpDownloader extends Downloader { } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { error = DownloadError.ERROR_FORBIDDEN; details = String.valueOf(response.code()); - } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { + } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND + || response.code() == HttpURLConnection.HTTP_GONE) { error = DownloadError.ERROR_NOT_FOUND; details = String.valueOf(response.code()); } else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java deleted file mode 100644 index 750255958..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import androidx.annotation.NonNull; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; - -/** - * This does not actually download, but it keeps track of a local feed's refresh state. - */ -public class LocalFeedStubDownloader extends Downloader { - - public LocalFeedStubDownloader(@NonNull DownloadRequest request) { - super(request); - } - - @Override - protected void download() { - } -}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java deleted file mode 100644 index 937f051ec..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import android.util.Log; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.storage.DBWriter; - -/** - * Handles failed downloads. - * <p/> - * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - * <p/> - * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. - */ -public class FailedDownloadHandler implements Runnable { - private static final String TAG = "FailedDownloadHandler"; - private final DownloadRequest request; - - public FailedDownloadHandler(DownloadRequest request) { - this.request = request; - } - - @Override - public void run() { - if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - } else if (request.isDeleteOnFailure()) { - Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index 1118b93cd..5da250e15 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -8,7 +8,7 @@ import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.parser.feed.FeedHandler; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; @@ -25,15 +25,15 @@ import java.util.concurrent.Callable; public class FeedParserTask implements Callable<FeedHandlerResult> { private static final String TAG = "FeedParserTask"; private final DownloadRequest request; - private DownloadStatus downloadStatus; + private DownloadResult downloadResult; private boolean successful = true; public FeedParserTask(DownloadRequest request) { this.request = request; - downloadStatus = new DownloadStatus( + downloadResult = new DownloadResult( 0, request.getTitle(), 0, request.getFeedfileType(), false, - false, true, DownloadError.ERROR_REQUEST_ERROR, new Date(), - "Unknown error: Status not set", request.isInitiatedByUser()); + DownloadError.ERROR_REQUEST_ERROR, new Date(), + "Unknown error: Status not set"); } @Override @@ -87,12 +87,12 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { } if (successful) { - downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, - successful, reasonDetailed, request.isInitiatedByUser()); + downloadResult = new DownloadResult(feed, feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, + successful, reasonDetailed); return result; } else { - downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), reason, - successful, reasonDetailed, request.isInitiatedByUser()); + downloadResult = new DownloadResult(feed, feed.getHumanReadableIdentifier(), reason, + successful, reasonDetailed); return null; } } @@ -120,7 +120,7 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { } @NonNull - public DownloadStatus getDownloadStatus() { - return downloadStatus; + public DownloadResult getDownloadStatus() { + return downloadResult; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 9cb1166b4..3b72ed164 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.service.download.handler; import android.content.Context; import androidx.annotation.NonNull; import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; @@ -30,7 +30,7 @@ public class FeedSyncTask { } @NonNull - public DownloadStatus getDownloadStatus() { + public DownloadResult getDownloadStatus() { return task.getDownloadStatus(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index c632bf1e0..a46b4c6d0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -13,7 +13,7 @@ import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; @@ -30,9 +30,9 @@ public class MediaDownloadedHandler implements Runnable { private static final String TAG = "MediaDownloadedHandler"; private final DownloadRequest request; private final Context context; - private DownloadStatus updatedStatus; + private DownloadResult updatedStatus; - public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadStatus status, + public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadResult status, @NonNull DownloadRequest request) { this.request = request; this.context = context; @@ -94,8 +94,8 @@ public class MediaDownloadedHandler implements Runnable { Log.e(TAG, "MediaHandlerThread was interrupted"); } catch (ExecutionException e) { Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.getMessage()); - updatedStatus = new DownloadStatus(media, media.getEpisodeTitle(), - DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage(), request.isInitiatedByUser()); + updatedStatus = new DownloadResult(media, media.getEpisodeTitle(), + DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); } if (item != null) { @@ -107,7 +107,7 @@ public class MediaDownloadedHandler implements Runnable { } @NonNull - public DownloadStatus getUpdatedStatus() { + public DownloadResult getUpdatedStatus() { return updatedStatus; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java deleted file mode 100644 index 5d2c48679..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.service.download.Downloader; -import org.greenrobot.eventbus.EventBus; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class PostDownloaderTask implements Runnable { - private List<Downloader> downloads; - - public PostDownloaderTask(List<Downloader> downloads) { - this.downloads = downloads; - } - - @Override - public void run() { - List<Downloader> runningDownloads = new ArrayList<>(); - for (Downloader downloader : downloads) { - if (!downloader.cancelled) { - runningDownloads.add(downloader); - } - } - List<Downloader> list = Collections.unmodifiableList(runningDownloads); - EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 42631296b..6fc9035ca 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -824,7 +824,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { && SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) { setSleepTimer(SleepTimerPreferences.timerMillis()); EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label), - PlaybackService.this::disableSleepTimer)); + (ctx) -> disableSleepTimer(), getString(R.string.undo))); } loadQueueForMediaSession(); break; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 0f3121551..dbbfba379 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -9,12 +9,10 @@ import java.util.List; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.PowerUtils; @@ -97,13 +95,9 @@ public class AutomaticDownloadAlgorithm { if (itemsToDownload.size() > 0) { Log.d(TAG, "Enqueueing " + itemsToDownload.size() + " items for download"); - List<DownloadRequest> requests = new ArrayList<>(); for (FeedItem episode : itemsToDownload) { - DownloadRequest.Builder request = DownloadRequestCreator.create(episode.getMedia()); - request.withInitiatedByUser(false); - requests.add(request.build()); + DownloadServiceInterface.get().download(context, episode); } - DownloadServiceInterface.get().download(context, false, requests.toArray(new DownloadRequest[0])); } } }; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 94a7334f3..d83557b0c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -16,10 +16,9 @@ import java.util.List; import java.util.Map; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.core.util.comparator.DownloadResultComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; -import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; @@ -28,14 +27,15 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.feed.SubscriptionsFilter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.storage.database.PodDBAdapter; import de.danoeh.antennapod.storage.database.mapper.ChapterCursorMapper; -import de.danoeh.antennapod.storage.database.mapper.DownloadStatusCursorMapper; +import de.danoeh.antennapod.storage.database.mapper.DownloadResultCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedItemCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedMediaCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedPreferencesCursorMapper; -import de.danoeh.antennapod.storage.preferences.UserPreferences; /** * Provides methods for reading data from the AntennaPod database. @@ -394,17 +394,17 @@ public final class DBReader { * @return A list with DownloadStatus objects that represent the download log. * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. */ - public static List<DownloadStatus> getDownloadLog() { + public static List<DownloadResult> getDownloadLog() { Log.d(TAG, "getDownloadLog() called"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) { - List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); + List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { - downloadLog.add(DownloadStatusCursorMapper.convert(cursor)); + downloadLog.add(DownloadResultCursorMapper.convert(cursor)); } - Collections.sort(downloadLog, new DownloadStatusComparator()); + Collections.sort(downloadLog, new DownloadResultComparator()); return downloadLog; } finally { adapter.close(); @@ -418,17 +418,17 @@ public final class DBReader { * @return A list with DownloadStatus objects that represent the feed's download log, * newest events first. */ - public static List<DownloadStatus> getFeedDownloadLog(long feedId) { + public static List<DownloadResult> getFeedDownloadLog(long feedId) { Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feedId + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) { - List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); + List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { - downloadLog.add(DownloadStatusCursorMapper.convert(cursor)); + downloadLog.add(DownloadResultCursorMapper.convert(cursor)); } - Collections.sort(downloadLog, new DownloadStatusComparator()); + Collections.sort(downloadLog, new DownloadResultComparator()); return downloadLog; } finally { adapter.close(); @@ -717,10 +717,10 @@ public final class DBReader { } } - public static List<FeedItem> getFeedItemsWithMedia(Long[] mediaIds) { + public static List<FeedItem> getFeedItemsWithUrl(List<String> urls) { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor itemCursor = adapter.getFeedItemCursorByMediaIds(mediaIds)) { + try (Cursor itemCursor = adapter.getFeedItemCursorByUrl(urls)) { List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); loadAdditionalFeedItemListData(items); Collections.sort(items, new PlaybackCompletionDateComparator()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 8b79d594c..e3ac7a7e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -7,14 +7,13 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; @@ -114,21 +113,6 @@ public final class DBTasks { EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); } - public static List<FeedItem> enqueueFeedItemsToDownload(final Context context, - List<FeedItem> items) throws InterruptedException, ExecutionException { - List<FeedItem> itemsToEnqueue = new ArrayList<>(); - if (UserPreferences.enqueueDownloadedEpisodes()) { - LongList queueIDList = DBReader.getQueueIDList(); - for (FeedItem item : items) { - if (!queueIDList.contains(item.getId())) { - itemsToEnqueue.add(item); - } - } - DBWriter.addQueueItem(context, false, itemsToEnqueue.toArray(new FeedItem[0])).get(); - } - return itemsToEnqueue; - } - /** * Looks for non-downloaded episodes in the queue or list of unread items and request a download if * 1. Network is available @@ -267,13 +251,13 @@ public final class DBTasks { FeedItem possibleDuplicate = searchFeedItemGuessDuplicate(newFeed.getItems(), item); if (!newFeed.isLocalFeed() && possibleDuplicate != null && item != possibleDuplicate) { // Canonical episode is the first one returned (usually oldest) - DBWriter.addDownloadStatus(new DownloadStatus(savedFeed, + DBWriter.addDownloadStatus(new DownloadResult(savedFeed, item.getTitle(), DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, "The podcast host appears to have added the same episode twice. " + "AntennaPod still refreshed the feed and attempted to repair it." + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(item) + "\n\nSecond episode that is also in the feed:\n" - + duplicateEpisodeDetails(possibleDuplicate), false)); + + duplicateEpisodeDetails(possibleDuplicate))); continue; } @@ -282,13 +266,13 @@ public final class DBTasks { oldItem = searchFeedItemGuessDuplicate(savedFeed.getItems(), item); if (oldItem != null) { Log.d(TAG, "Repaired duplicate: " + oldItem + ", " + item); - DBWriter.addDownloadStatus(new DownloadStatus(savedFeed, + DBWriter.addDownloadStatus(new DownloadResult(savedFeed, item.getTitle(), DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, "The podcast host changed the ID of an existing episode instead of just " + "updating the episode itself. AntennaPod still refreshed the feed and " + "attempted to repair it." + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(oldItem) - + "\n\nNow the feed contains:\n" + duplicateEpisodeDetails(item), false)); + + "\n\nNow the feed contains:\n" + duplicateEpisodeDetails(item))); oldItem.setItemIdentifier(item.getItemIdentifier()); if (oldItem.isPlayed() && oldItem.getMedia() != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index dcee8a45a..4815737f4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -36,7 +36,7 @@ import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.IntentUtils; @@ -299,7 +299,7 @@ public class DBWriter { * * @param status The DownloadStatus object. */ - public static Future<?> addDownloadStatus(final DownloadStatus status) { + public static Future<?> addDownloadStatus(final DownloadResult status) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java index b81f281e8..4ed17c43f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java @@ -8,9 +8,9 @@ import androidx.annotation.Nullable; import java.util.List; import java.util.Random; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.model.playback.Playable; @@ -74,7 +74,7 @@ class ItemEnqueuePositionCalculator { } return curItem != null && curItem.getMedia() != null - && DownloadService.isDownloadingFile(curItem.getMedia().getDownload_url()); + && DownloadServiceInterface.get().isDownloadingEpisode(curItem.getMedia().getDownload_url()); } private static int getCurrentlyPlayingPosition(@NonNull List<FeedItem> curQueue, diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java index 50b4d411f..d848b5a30 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -24,10 +24,10 @@ public class FeedItemUtil { return -1; } - public static int indexOfItemWithMediaId(List<FeedItem> items, long mediaId) { - for(int i=0; i < items.size(); i++) { + public static int indexOfItemWithDownloadUrl(List<FeedItem> items, String downloadUrl) { + for (int i = 0; i < items.size(); i++) { FeedItem item = items.get(i); - if(item != null && item.getMedia() != null && item.getMedia().getId() == mediaId) { + if (item != null && item.getMedia() != null && item.getMedia().getDownload_url().equals(downloadUrl)) { return i; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java new file mode 100644 index 000000000..d1d50fc8a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.util.comparator; + +import java.util.Comparator; + +import de.danoeh.antennapod.model.download.DownloadResult; + +/** Compares the completion date of two DownloadResult objects. */ +public class DownloadResultComparator implements Comparator<DownloadResult> { + + @Override + public int compare(DownloadResult lhs, DownloadResult rhs) { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java deleted file mode 100644 index 68b38ec7f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.danoeh.antennapod.core.util.comparator; - -import java.util.Comparator; - -import de.danoeh.antennapod.model.download.DownloadStatus; - -/** Compares the completion date of two Downloadstatus objects. */ -public class DownloadStatusComparator implements Comparator<DownloadStatus> { - - @Override - public int compare(DownloadStatus lhs, DownloadStatus rhs) { - return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); - } - -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java index d66bd2360..6a7e51bac 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java @@ -13,7 +13,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -26,7 +25,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import static de.danoeh.antennapod.core.util.FeedItemUtil.getIdList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -255,63 +253,6 @@ public class DbTasksTest { } } - @Test - public void testAddQueueItemsInDownload_EnqueueEnabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(true); - UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - // the first item fis1.get(0) is already in the queue - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Arrays.asList(fis1.get(1), fis2.get(2), fis2.get(1)); - List<FeedItem> expectedQueue = new ArrayList<>(); - expectedQueue.addAll(DBReader.getQueue()); - expectedQueue.addAll(expectedEnqueued); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("Only items not in the queue are enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue()); - } - - @Test - public void testAddQueueItemsInDownload_EnqueueDisabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(false); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Collections.emptyList(); - List<FeedItem> expectedQueue = DBReader.getQueue(); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue()); - } - - private void assertEqualsByIds(String msg, List<? extends FeedItem> expected, List<? extends FeedItem> actual) { - // assert only the IDs, so that any differences are easily to spot. - List<Long> expectedIds = getIdList(expected); - List<Long> actualIds = getIdList(actual); - assertEquals(msg, expectedIds, actualIds); - } - private Feed createSavedFeed(String title, int numFeedItems) { final Feed feed = new Feed("url", null, title); diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java index 376e0e65c..2594fabf6 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -1,7 +1,8 @@ package de.danoeh.antennapod.core.storage; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -21,8 +22,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMother; import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.model.playback.Playable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import static de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation.AFTER_CURRENTLY_PLAYING; import static de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation.BACK; @@ -74,6 +73,7 @@ public class ItemEnqueuePositionCalculatorTest { */ @Test public void test() { + DownloadServiceInterface.setImpl(new DownloadServiceInterfaceStub()); ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); // shallow copy to which the test will add items @@ -128,95 +128,6 @@ public class ItemEnqueuePositionCalculatorTest { } - @RunWith(Parameterized.class) - public static class PreserveDownloadOrderTest { - /** - * The test covers the use case that when user initiates multiple downloads in succession, - * resulting in multiple addQueueItem() calls in succession. - * the items in the queue will be in the same order as the order user taps to download - */ - @Parameters(name = "{index}: case<{0}>") - public static Iterable<Object[]> data() { - // Attempts to make test more readable by showing the expected list of ids - // (rather than the expected positions) - return Arrays.asList(new Object[][] { - {"download order test, enqueue default", - concat(QUEUE_DEFAULT_IDS, 101L), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L)), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 103L)), - BACK, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL}, - {"download order test, enqueue at front (currently playing has no effect)", - concat(101L, QUEUE_DEFAULT_IDS), - concat(list(101L, 102L), QUEUE_DEFAULT_IDS), - concat(list(101L, 103L, 102L), QUEUE_DEFAULT_IDS), - // ^ 103 is put ahead of 102, after 102 failed. - // It is a limitation as the logic can't tell 102 download has failed - // (as opposed to simply being enqueued) - FRONT, QUEUE_DEFAULT, 11L}, // 11 is at the front, currently playing - {"download order test, enqueue after currently playing", - list(11L, 101L, 12L, 13L, 14L), - list(11L, 101L, 102L, 12L, 13L, 14L), - list(11L, 101L, 103L, 102L, 12L, 13L, 14L), - AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 11L} // 11 is at the front, currently playing - }); - } - - @Parameter - public String message; - - @Parameter(1) - public List<Long> idsExpectedAfter101; - - @Parameter(2) - public List<Long> idsExpectedAfter102; - - @Parameter(3) - public List<Long> idsExpectedAfter103; - - @Parameter(4) - public EnqueueLocation options; - - @Parameter(5) - public List<FeedItem> queueInitial; - - @Parameter(6) - public long idCurrentlyPlaying; - - @Test - public void testQueueOrderWhenDownloading2Items() { - ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); - try (MockedStatic<DownloadService> downloadServiceMock = Mockito.mockStatic(DownloadService.class)) { - List<FeedItem> queue = new ArrayList<>(queueInitial); - - // Test body - Playable currentlyPlaying = getCurrentlyPlaying(idCurrentlyPlaying); - // User clicks download on feed item 101 - FeedItem feedItem101 = createFeedItem(101); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem101.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message + " (1st download)", - calculator, feedItem101, queue, currentlyPlaying, idsExpectedAfter101); - // Then user clicks download on feed item 102 - FeedItem feedItem102 = createFeedItem(102); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem102.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message + " (2nd download, it should preserve order of download)", - calculator, feedItem102, queue, currentlyPlaying, idsExpectedAfter102); - // simulate download failure case for 102 - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem102.getMedia().getDownload_url())).thenReturn(false); - // Then user clicks download on feed item 103 - FeedItem feedItem103 = createFeedItem(103); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem103.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message - + " (3rd download, with 2nd download failed; " - + "it should be behind 1st download (unless enqueueLocation is BACK)", - calculator, feedItem103, queue, currentlyPlaying, idsExpectedAfter103); - } - } - } - static void doAddToQueueAndAssertResult(String message, ItemEnqueuePositionCalculator calculator, FeedItem itemToAdd, |