diff options
author | H. Lehmann <ByteHamster@users.noreply.github.com> | 2019-10-31 09:23:47 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-31 09:23:47 +0100 |
commit | 2a2ced1631ebd9dc84f3e91339d2706b30a850b5 (patch) | |
tree | 23937c7821f3d551d684804259072fdebe3b1d55 | |
parent | e7367e218d868967f7c1c45438c85c5c0958e56b (diff) | |
parent | 69f3a1210fcbd199cd24c4060da4518e0dda2146 (diff) | |
download | AntennaPod-2a2ced1631ebd9dc84f3e91339d2706b30a850b5.zip |
Merge pull request #3572 from ByteHamster/downloadservice-refactoring
DownloadService refactoring
18 files changed, 796 insertions, 801 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java index cb099cc85..3b5b35946 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java @@ -5,6 +5,7 @@ import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import de.danoeh.antennapod.core.service.download.DownloaderFactory; import org.awaitility.Awaitility; import org.awaitility.core.ConditionTimeoutException; import org.junit.After; @@ -47,7 +48,7 @@ public class DownloadServiceTest { private Feed testFeed = null; private FeedMedia testMedia11 = null; - private DownloadService.DownloaderFactory origFactory = null; + private DownloaderFactory origFactory = null; @Before public void setUp() throws Exception { @@ -106,7 +107,7 @@ public class DownloadServiceTest { }); } - private static class StubDownloaderFactory implements DownloadService.DownloaderFactory { + private static class StubDownloaderFactory implements DownloaderFactory { private final long downloadTime; @NonNull diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java index 35a4014ba..f25159046 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -201,7 +201,7 @@ public class UITestUtils { adapter.setCompleteFeed(hostedFeeds.toArray(new Feed[hostedFeeds.size()])); adapter.setQueue(queue); adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(hostedFeeds)); EventBus.getDefault().post(QueueEvent.setQueue(queue)); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 48c84344c..63851acd4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -409,7 +409,9 @@ public class FeedItemlistFragment extends ListFragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onFeedListChanged(FeedListUpdateEvent event) { - updateUi(); + if (event.contains(feed)) { + updateUi(); + } } private void updateProgressBarVisibility() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index 528fa7c32..7e8823c27 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -25,6 +25,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.view.EmptyViewHandler; +import org.greenrobot.eventbus.ThreadMode; /** * Displays all running downloads and provides actions to cancel them @@ -75,7 +76,7 @@ public class RunningDownloadsFragment extends ListFragment { setListAdapter(null); } - @Subscribe(sticky = true) + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEvent(DownloadEvent event) { Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java index 6073eb3bc..ca8db3cc9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java @@ -1,6 +1,28 @@ package de.danoeh.antennapod.core.event; +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.ArrayList; +import java.util.List; + public class FeedListUpdateEvent { - public FeedListUpdateEvent() { + private final List<Long> feeds = new ArrayList<>(); + + public FeedListUpdateEvent(List<Feed> feeds) { + for (Feed feed : feeds) { + this.feeds.add(feed.getId()); + } + } + + public FeedListUpdateEvent(Feed feed) { + feeds.add(feed.getId()); + } + + public FeedListUpdateEvent(long feedId) { + feeds.add(feedId); + } + + public boolean contains(Feed feed) { + return feeds.contains(feed.getId()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java new file mode 100644 index 000000000..c0de6c825 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.service.download; + +import android.util.Log; +import android.webkit.URLUtil; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class DefaultDownloaderFactory implements DownloaderFactory { + private static final String TAG = "DefaultDwnldrFactory"; + + @Nullable + @Override + public Downloader create(@NonNull DownloadRequest request) { + if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) { + Log.e(TAG, "Could not find appropriate downloader for " + request.getSource()); + return null; + } + return new HttpDownloader(request); + } +}
\ No newline at end of file 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 index 296046031..95f78366f 100644 --- 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 @@ -7,73 +7,48 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.media.MediaMetadataRetriever; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.core.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; -import android.webkit.URLUtil; - -import org.apache.commons.io.FileUtils; -import org.greenrobot.eventbus.EventBus; -import org.xml.sax.SAXException; - +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.GpodnetSyncService; +import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; +import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; +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.DownloadRequester; +import de.danoeh.antennapod.core.util.DownloadError; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; -import java.util.LinkedList; import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - -import javax.xml.parsers.ParserConfigurationException; - -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.FeedPreferences; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.GpodnetSyncService; -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.DownloadRequestException; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.InvalidFeedException; -import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import org.apache.commons.io.FileUtils; +import org.greenrobot.eventbus.EventBus; /** * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent. @@ -106,26 +81,21 @@ public class DownloadService extends Service { */ public static final String EXTRA_REQUEST = "request"; + public static final int NOTIFICATION_ID = 2; + /** * Contains all completed downloads that have not been included in the report yet. */ - private List<DownloadStatus> reportQueue; - - private ExecutorService syncExecutor; - private CompletionService<Downloader> downloadExecutor; - private FeedSyncThread feedSyncThread; - - private DownloadRequester requester; - - - private NotificationCompat.Builder notificationCompatBuilder; - private static final int NOTIFICATION_ID = 2; - private static final int REPORT_ID = 3; + private final List<DownloadStatus> reportQueue; + private final ExecutorService syncExecutor; + private final CompletionService<Downloader> downloadExecutor; + private final DownloadRequester requester; + private DownloadServiceNotification notificationManager; /** * Currently running downloads. */ - private List<Downloader> downloads; + private final List<Downloader> downloads; /** * Number of running downloads. @@ -141,10 +111,10 @@ public class DownloadService extends Service { private NotificationUpdater notificationUpdater; private ScheduledFuture<?> notificationUpdaterFuture; + private ScheduledFuture<?> downloadPostFuture; private static final int SCHED_EX_POOL_SIZE = 1; private ScheduledThreadPoolExecutor schedExecutor; - - private final Handler postHandler = new Handler(); + private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); private final IBinder mBinder = new LocalBinder(); @@ -154,108 +124,12 @@ public class DownloadService extends Service { } } - private final Thread downloadCompletionThread = new Thread("DownloadCompletionThread") { - private static final String TAG = "downloadCompletionThd"; - - @Override - public void run() { - Log.d(TAG, "downloadCompletionThread was started"); - while (!isInterrupted()) { - try { - Downloader downloader = downloadExecutor.take().get(); - Log.d(TAG, "Received 'Download Complete' - message."); - removeDownload(downloader); - DownloadStatus status = downloader.getResult(); - boolean successful = status.isSuccessful(); - - final int type = status.getFeedfileType(); - if (successful) { - if (type == Feed.FEEDFILETYPE_FEED) { - handleCompletedFeedDownload(downloader.getDownloadRequest()); - } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); - } - } else { - numberOfDownloads.decrementAndGet(); - if (!status.isCancelled()) { - if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - 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())); - DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest()); - } else { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); - handleFailedDownload(status, downloader.getDownloadRequest()); - - if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedItem item = getFeedItemFromId(status.getFeedfileId()); - if (item == null) { - return; - } - boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed()); - boolean forbidden = status.getReason() == DownloadError.ERROR_FORBIDDEN - && String.valueOf(HttpURLConnection.HTTP_FORBIDDEN).equals(status.getReasonDetailed()); - boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE; - boolean wrongFileType = status.getReason() == DownloadError.ERROR_FILE_TYPE; - if (httpNotFound || forbidden || notEnoughSpace || wrongFileType) { - DBWriter.saveFeedItemAutoDownloadFailed(item).get(); - } - // 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)); - } - } - queryDownloadsAsync(); - } - } catch (InterruptedException e) { - Log.e(TAG, "DownloadCompletionThread was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in DownloadCompletionThread: " + e.getMessage()); - numberOfDownloads.decrementAndGet(); - } - } - Log.d(TAG, "End of downloadCompletionThread"); - } - }; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { - onDownloadQueued(intent); - } else if (numberOfDownloads.get() == 0) { - stopSelf(); - } - return Service.START_NOT_STICKY; - } - - @Override - public void onCreate() { - Log.d(TAG, "Service started"); - isRunning = true; - handler = new Handler(); + public DownloadService() { reportQueue = Collections.synchronizedList(new ArrayList<>()); downloads = Collections.synchronizedList(new ArrayList<>()); numberOfDownloads = new AtomicInteger(0); + requester = DownloadRequester.getInstance(); - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); syncExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r); t.setPriority(Thread.MIN_PRIORITY); @@ -278,13 +152,35 @@ public class DownloadService extends Service { return t; }, (r, executor) -> Log.w(TAG, "SchedEx rejected submission of new task") ); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { + onDownloadQueued(intent); + } else if (numberOfDownloads.get() == 0) { + stopSelf(); + } + return Service.START_NOT_STICKY; + } + + @Override + public void onCreate() { + Log.d(TAG, "Service started"); + isRunning = true; + handler = new Handler(); + notificationManager = new DownloadServiceNotification(this); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + downloadCompletionThread.start(); - feedSyncThread = new FeedSyncThread(); - feedSyncThread.start(); - setupNotificationBuilders(); - requester = DownloadRequester.getInstance(); - startForeground(NOTIFICATION_ID, updateNotifications()); + Notification notification = notificationManager.updateNotifications( + requester.getNumberOfDownloads(), downloads); + startForeground(NOTIFICATION_ID, notification); } @Override @@ -297,12 +193,12 @@ public class DownloadService extends Service { Log.d(TAG, "Service shutting down"); isRunning = false; - if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() && - UserPreferences.showDownloadReport()) { - updateReport(); + if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() + && UserPreferences.showDownloadReport()) { + notificationManager.updateReport(reportQueue); + reportQueue.clear(); } - postHandler.removeCallbacks(postDownloaderTask); EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); stopForeground(true); @@ -312,8 +208,8 @@ public class DownloadService extends Service { downloadCompletionThread.interrupt(); syncExecutor.shutdown(); schedExecutor.shutdown(); - feedSyncThread.shutdown(); cancelNotificationUpdater(); + downloadPostFuture.cancel(true); unregisterReceiver(cancelDownloadReceiver); // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, @@ -328,38 +224,120 @@ public class DownloadService extends Service { DBTasks.autodownloadUndownloadedItems(getApplicationContext()); } - private void setupNotificationBuilders() { - notificationCompatBuilder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOADING) - .setOngoing(true) - .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)) - .setSmallIcon(R.drawable.stat_notify_sync); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - notificationCompatBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); + private final Thread downloadCompletionThread = new Thread("DownloadCompletionThread") { + private static final String TAG = "downloadCompletionThd"; + + @Override + public void run() { + Log.d(TAG, "downloadCompletionThread was started"); + while (!isInterrupted()) { + try { + Downloader downloader = downloadExecutor.take().get(); + Log.d(TAG, "Received 'Download Complete' - message."); + + if (downloader.getResult().isSuccessful()) { + syncExecutor.execute(() -> { + handleSuccessfulDownload(downloader); + removeDownload(downloader); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + + }); + } else { + handleFailedDownload(downloader); + removeDownload(downloader); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } catch (InterruptedException e) { + Log.e(TAG, "DownloadCompletionThread was interrupted"); + } catch (ExecutionException e) { + Log.e(TAG, "ExecutionException in DownloadCompletionThread: " + e.getMessage()); + } + } + Log.d(TAG, "End of downloadCompletionThread"); } + }; - Log.d(TAG, "Notification set up"); - } + private void handleSuccessfulDownload(Downloader downloader) { + DownloadRequest request = downloader.getDownloadRequest(); + DownloadStatus status = downloader.getResult(); + final int type = status.getFeedfileType(); - /** - * Updates the contents of the service's notifications. Should be called - * after setupNotificationBuilders. - */ - private Notification updateNotifications() { - if (notificationCompatBuilder == null) { - return null; + if (type == Feed.FEEDFILETYPE_FEED) { + Log.d(TAG, "Handling completed Feed Download"); + FeedSyncTask task = new FeedSyncTask(DownloadService.this, request); + boolean success = task.run(); + + if (success) { + // we create a 'successful' download log if the feed's last refresh failed + List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + if (log.size() > 0 && !log.get(0).isSuccessful()) { + saveDownloadStatus(task.getDownloadStatus()); + } + } else { + saveDownloadStatus(task.getDownloadStatus()); + } + } else 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()); } + } + + private void handleFailedDownload(Downloader downloader) { + DownloadStatus status = downloader.getResult(); + final int type = status.getFeedfileType(); - String contentTitle = getString(R.string.download_notification_title); - int numDownloads = requester.getNumberOfDownloads(); - String downloadsLeft = (numDownloads > 0) ? - getResources().getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads) : - getString(R.string.downloads_processing); - String bigText = compileNotificationString(downloads); - - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(downloadsLeft); - notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); - return notificationCompatBuilder.build(); + 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())); + DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest()); + } else { + Log.e(TAG, "Download failed"); + saveDownloadStatus(status); + syncExecutor.execute(new FailedDownloadHandler(downloader.getDownloadRequest())); + + if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedItem item = getFeedItemFromId(status.getFeedfileId()); + if (item == null) { + return; + } + boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR + && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed()); + boolean forbidden = status.getReason() == DownloadError.ERROR_FORBIDDEN + && String.valueOf(HttpURLConnection.HTTP_FORBIDDEN).equals(status.getReasonDetailed()); + boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE; + boolean wrongFileType = status.getReason() == DownloadError.ERROR_FILE_TYPE; + if (httpNotFound || forbidden || notEnoughSpace || wrongFileType) { + try { + DBWriter.saveFeedItemAutoDownloadFailed(item).get(); + } catch (ExecutionException | InterruptedException e) { + Log.d(TAG, "Ignoring exception while setting item download status"); + e.printStackTrace(); + } + } + // 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 Downloader getDownloader(String downloadUrl) { @@ -418,7 +396,7 @@ public class DownloadService extends Service { writeFileUrl(request); - Downloader downloader = getDownloader(request); + Downloader downloader = downloaderFactory.create(request); if (downloader != null) { numberOfDownloads.incrementAndGet(); // smaller rss feeds before bigger media files @@ -436,26 +414,6 @@ public class DownloadService extends Service { } @VisibleForTesting - public interface DownloaderFactory { - @Nullable - Downloader create(@NonNull DownloadRequest request); - } - - private static class DefaultDownloaderFactory implements DownloaderFactory { - @Nullable - @Override - public Downloader create(@NonNull DownloadRequest request) { - if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) { - Log.e(TAG, "Could not find appropriate downloader for " + request.getSource()); - return null; - } - return new HttpDownloader(request); - } - } - - private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); - - @VisibleForTesting public static DownloaderFactory getDownloaderFactory() { return downloaderFactory; } @@ -467,10 +425,6 @@ public class DownloadService extends Service { DownloadService.downloaderFactory = downloaderFactory; } - private Downloader getDownloader(@NonNull DownloadRequest request) { - return downloaderFactory.create(request); - } - /** * Remove download from the DownloadRequester list and from the * DownloadService list. @@ -497,55 +451,6 @@ public class DownloadService extends Service { } /** - * 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 - */ - private void updateReport() { - // check if report should be created - boolean createReport = false; - int successfulDownloads = 0; - 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.isSuccessful()) { - successfulDownloads++; - } else if (!status.isCancelled()) { - createReport = true; - failedDownloads++; - } - } - - if (createReport) { - Log.d(TAG, "Creating report"); - // create notification object - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_ERROR) - .setTicker(getString(R.string.download_report_title)) - .setContentTitle(getString(R.string.download_report_content_title)) - .setContentText( - String.format( - getString(R.string.download_report_content), - successfulDownloads, failedDownloads) - ) - .setSmallIcon(R.drawable.stat_notify_sync_error) - .setContentIntent( - ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) - ) - .setAutoCancel(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.setVisibility(Notification.VISIBILITY_PUBLIC); - } - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(REPORT_ID, builder.build()); - } else { - Log.d(TAG, "No report is created"); - } - reportQueue.clear(); - } - - /** * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is * used from a thread other than the main thread. */ @@ -564,53 +469,12 @@ public class DownloadService extends Service { stopSelf(); } else { setupNotificationUpdater(); - startForeground(NOTIFICATION_ID, updateNotifications()); + Notification notification = notificationManager.updateNotifications( + requester.getNumberOfDownloads(), downloads); + startForeground(NOTIFICATION_ID, notification); } } - private void postAuthenticationNotification(final DownloadRequest downloadRequest) { - handler.post(() -> { - final String resourceTitle = (downloadRequest.getTitle() != null) ? - downloadRequest.getTitle() : downloadRequest.getSource(); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this, NotificationUtils.CHANNEL_ID_USER_ACTION); - builder.setTicker(getText(R.string.authentication_notification_title)) - .setContentTitle(getText(R.string.authentication_notification_title)) - .setContentText(getText(R.string.authentication_notification_msg)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) - + ": " + resourceTitle)) - .setSmallIcon(R.drawable.ic_notification_key) - .setAutoCancel(true) - .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.setVisibility(Notification.VISIBILITY_PUBLIC); - } - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), builder.build()); - }); - } - - /** - * Is called whenever a Feed is downloaded - */ - private void handleCompletedFeedDownload(DownloadRequest request) { - Log.d(TAG, "Handling completed Feed Download"); - feedSyncThread.submitCompletedDownload(request); - } - - /** - * Is called whenever a FeedMedia is downloaded. - */ - private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(new MediaHandlerThread(status, request)); - } - - private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { - Log.d(TAG, "Handling failed download"); - syncExecutor.execute(new FailedDownloadHandler(status, request)); - } - @Nullable private FeedItem getFeedItemFromId(long id) { FeedMedia media = DBReader.getFeedMedia(id); @@ -622,300 +486,6 @@ public class DownloadService extends Service { } /** - * Takes a single Feed, parses the corresponding file and refreshes - * information in the manager - */ - private class FeedSyncThread extends Thread { - private static final String TAG = "FeedSyncThread"; - - private final BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<>(); - private final CompletionService<Pair<DownloadRequest, FeedHandlerResult>> parserService = new ExecutorCompletionService<>(Executors.newSingleThreadExecutor()); - private final ExecutorService dbService = Executors.newSingleThreadExecutor(); - private Future<?> dbUpdateFuture; - private volatile boolean isActive = true; - private volatile boolean isCollectingRequests = false; - - private static final long WAIT_TIMEOUT = 3000; - - FeedSyncThread() { - super("FeedSyncThread"); - } - - /** - * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to - * collect more completed requests. - * - * @return Collected feeds or null if the method has been interrupted during the first waiting period. - */ - private List<Pair<DownloadRequest, FeedHandlerResult>> collectCompletedRequests() { - List<Pair<DownloadRequest, FeedHandlerResult>> results = new LinkedList<>(); - DownloadRequester requester = DownloadRequester.getInstance(); - int tasks = 0; - - try { - DownloadRequest request = completedRequests.take(); - parserService.submit(new FeedParserTask(request)); - tasks++; - } catch (InterruptedException e) { - Log.e(TAG, "FeedSyncThread was interrupted"); - return null; - } - - tasks += pollCompletedDownloads(); - - isCollectingRequests = true; - - if (requester.isDownloadingFeeds()) { - // wait for completion of more downloads - long startTime = System.currentTimeMillis(); - long currentTime = startTime; - while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { - try { - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); - sleep(startTime + WAIT_TIMEOUT - currentTime); - } catch (InterruptedException e) { - Log.d(TAG, "interrupted while waiting for more downloads"); - tasks += pollCompletedDownloads(); - } finally { - currentTime = System.currentTimeMillis(); - } - } - - tasks += pollCompletedDownloads(); - - } - - isCollectingRequests = false; - - for (int i = 0; i < tasks; i++) { - try { - Pair<DownloadRequest, FeedHandlerResult> result = parserService.take().get(); - if (result != null) { - results.add(result); - } - } catch (InterruptedException e) { - Log.e(TAG, "FeedSyncThread was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage()); - e.printStackTrace(); - } - } - - return results; - } - - private int pollCompletedDownloads() { - int tasks = 0; - while (!completedRequests.isEmpty()) { - parserService.submit(new FeedParserTask(completedRequests.poll())); - tasks++; - } - return tasks; - } - - @Override - public void run() { - while (isActive) { - final List<Pair<DownloadRequest, FeedHandlerResult>> results = collectCompletedRequests(); - - if (results == null) { - continue; - } - - Log.d(TAG, "Bundling " + results.size() + " feeds"); - - // Save information of feed in DB - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - Log.e(TAG, "FeedSyncThread was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage()); - e.printStackTrace(); - } - } - - dbUpdateFuture = dbService.submit(() -> { - Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, getFeeds(results)); - - for (int i = 0; i < savedFeeds.length; i++) { - Feed savedFeed = savedFeeds[i]; - - // If loadAllPages=true, check if another page is available and queue it for download - final boolean loadAllPages = results.get(i).first.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES); - final Feed feed = results.get(i).second.feed; - if (loadAllPages && feed.getNextPageLink() != null) { - try { - feed.setId(savedFeed.getId()); - DBTasks.loadNextPageOfFeed(DownloadService.this, savedFeed, true); - } catch (DownloadRequestException e) { - Log.e(TAG, "Error trying to load next page", e); - } - } - - ClientConfig.downloadServiceCallbacks.onFeedParsed(DownloadService.this, - savedFeed); - - numberOfDownloads.decrementAndGet(); - } - - queryDownloadsAsync(); - }); - - } - - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - Log.e(TAG, "interrupted while updating the db"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException while updating the db: " + e.getMessage()); - } - } - - Log.d(TAG, "Shutting down"); - } - - /** - * Helper method - */ - private Feed[] getFeeds(List<Pair<DownloadRequest, FeedHandlerResult>> results) { - Feed[] feeds = new Feed[results.size()]; - for (int i = 0; i < results.size(); i++) { - feeds[i] = results.get(i).second.feed; - } - return feeds; - } - - private class FeedParserTask implements Callable<Pair<DownloadRequest, FeedHandlerResult>> { - - private final DownloadRequest request; - - private FeedParserTask(DownloadRequest request) { - this.request = request; - } - - @Override - public Pair<DownloadRequest, FeedHandlerResult> call() throws Exception { - return parseFeed(request); - } - } - - private Pair<DownloadRequest, FeedHandlerResult> parseFeed(DownloadRequest request) { - Feed feed = new Feed(request.getSource(), request.getLastModified()); - feed.setFile_url(request.getDestination()); - feed.setId(request.getFeedfileId()); - feed.setDownloaded(true); - feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, - request.getUsername(), request.getPassword())); - feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0)); - - DownloadError reason = null; - String reasonDetailed = null; - boolean successful = true; - FeedHandler feedHandler = new FeedHandler(); - - FeedHandlerResult result = null; - try { - result = feedHandler.parseFeed(feed); - Log.d(TAG, feed.getTitle() + " parsed"); - if (!checkFeedData(feed)) { - throw new InvalidFeedException(); - } - - } catch (SAXException | IOException | ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } finally { - File feedFile = new File(request.getDestination()); - if (feedFile.exists()) { - boolean deleted = feedFile.delete(); - Log.d(TAG, "Deletion of file '" + feedFile.getAbsolutePath() + "' " + (deleted ? "successful" : "FAILED")); - } - } - - if (successful) { - // we create a 'successful' download log if the feed's last refresh failed - List<DownloadStatus> log = DBReader.getFeedDownloadLog(feed); - if (log.size() > 0 && !log.get(0).isSuccessful()) { - saveDownloadStatus( - new DownloadStatus(feed, feed.getHumanReadableIdentifier(), - DownloadError.SUCCESS, successful, reasonDetailed)); - } - return Pair.create(request, result); - } else { - numberOfDownloads.decrementAndGet(); - saveDownloadStatus( - new DownloadStatus(feed, feed.getHumanReadableIdentifier(), reason, - successful, reasonDetailed)); - return null; - } - } - - - /** - * Checks if the feed was parsed correctly. - */ - private boolean checkFeedData(Feed feed) { - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title."); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - return true; - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - public void shutdown() { - isActive = false; - if (isCollectingRequests) { - interrupt(); - } - } - - void submitCompletedDownload(DownloadRequest request) { - completedRequests.offer(request); - if (isCollectingRequests) { - interrupt(); - } - } - - } - - /** * 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. */ @@ -952,128 +522,13 @@ public class DownloadService extends Service { } /** - * 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. - */ - private static class FailedDownloadHandler implements Runnable { - - private final DownloadRequest request; - private final DownloadStatus status; - - FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { - this.request = request; - this.status = status; - } - - @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"); - } - } - } - - /** - * Handles a completed media download. - */ - private class MediaHandlerThread implements Runnable { - - private final DownloadRequest request; - private DownloadStatus status; - - MediaHandlerThread(@NonNull DownloadStatus status, - @NonNull DownloadRequest request) { - this.status = status; - this.request = request; - } - - @Override - public void run() { - FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); - if (media == null) { - Log.e(TAG, "Could not find downloaded media object in database"); - return; - } - media.setDownloaded(true); - media.setFile_url(request.getDestination()); - media.checkEmbeddedPicture(); // enforce check - - // check if file has chapters - if(media.getItem() != null && !media.getItem().hasChapters()) { - ChapterUtils.loadChaptersFromFileUrl(media); - } - - // Get duration - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - String durationStr = null; - try { - mmr.setDataSource(media.getFile_url()); - durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - media.setDuration(Integer.parseInt(durationStr)); - Log.d(TAG, "Duration of file is " + media.getDuration()); - } catch (NumberFormatException e) { - Log.d(TAG, "Invalid file duration: " + durationStr); - } catch (Exception e) { - Log.e(TAG, "Get duration failed", e); - } finally { - mmr.release(); - } - - final FeedItem item = media.getItem(); - - try { - DBWriter.setFeedMedia(media).get(); - - // we've received the media, we don't want to autodownload it again - if (item != null) { - item.setAutoDownload(false); - // setFeedItem() signals (via EventBus) that the item has been updated, - // so we do it after the enclosing media has been updated above, - // to ensure subscribers will get the updated FeedMedia as well - DBWriter.setFeedItem(item).get(); - } - - if (item != null && UserPreferences.enqueueDownloadedEpisodes() && - !DBTasks.isInQueue(DownloadService.this, item.getId())) { - DBWriter.addQueueItem(DownloadService.this, item).get(); - } - } catch (InterruptedException e) { - Log.e(TAG, "MediaHandlerThread was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.getMessage()); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } - - saveDownloadStatus(status); - - if (GpodnetPreferences.loggedIn() && item != null) { - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD) - .currentDeviceId() - .currentTimestamp() - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** * Schedules the notification updater task if it hasn't been scheduled yet. */ private void setupNotificationUpdater() { Log.d(TAG, "Setting up notification updater"); if (notificationUpdater == null) { notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( - notificationUpdater, 5L, 5L, TimeUnit.SECONDS); + notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate(notificationUpdater, 1, 1, TimeUnit.SECONDS); } } @@ -1089,71 +544,20 @@ public class DownloadService extends Service { private class NotificationUpdater implements Runnable { public void run() { - handler.post(() -> { - Notification n = updateNotifications(); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, n); - } - }); - } - - } - - - private long lastPost = 0; - - private final Runnable postDownloaderTask = new Runnable() { - @Override - public void run() { - List<Downloader> runningDownloads = new ArrayList<>(); - for (Downloader downloader : downloads) { - if (!downloader.cancelled) { - runningDownloads.add(downloader); - } + Notification n = notificationManager.updateNotifications(requester.getNumberOfDownloads(), downloads); + if (n != null) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, n); } - List<Downloader> list = Collections.unmodifiableList(runningDownloads); - EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); - postHandler.postDelayed(postDownloaderTask, 1500); } - }; + } private void postDownloaders() { - long now = System.currentTimeMillis(); - if (now - lastPost >= 250) { - postHandler.removeCallbacks(postDownloaderTask); - postDownloaderTask.run(); - lastPost = now; - } - } + new PostDownloaderTask(downloads).run(); - private static String compileNotificationString(List<Downloader> downloads) { - List<String> lines = new ArrayList<>(downloads.size()); - for (Downloader downloader : downloads) { - if (downloader.cancelled) { - continue; - } - StringBuilder line = new StringBuilder("• "); - DownloadRequest request = downloader.getDownloadRequest(); - switch (request.getFeedfileType()) { - case Feed.FEEDFILETYPE_FEED: - if (request.getTitle() != null) { - line.append(request.getTitle()); - } - break; - case FeedMedia.FEEDFILETYPE_FEEDMEDIA: - if (request.getTitle() != null) { - line.append(request.getTitle()) - .append(" (") - .append(request.getProgressPercent()) - .append("%)"); - } - break; - default: - line.append("Unknown: ").append(request.getFeedfileType()); - } - lines.add(line.toString()); + if (downloadPostFuture == null) { + downloadPostFuture = schedExecutor.scheduleAtFixedRate( + new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS); } - return TextUtils.join("\n", lines); } } 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 new file mode 100644 index 000000000..431eccc8c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -0,0 +1,166 @@ +package de.danoeh.antennapod.core.service.download; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import androidx.core.app.NotificationCompat; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class DownloadServiceNotification { + private static final String TAG = "DownloadSvcNotification"; + private static final int REPORT_ID = 3; + + 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(true) + .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(context)) + .setSmallIcon(R.drawable.stat_notify_sync); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + notificationCompatBuilder.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(int numDownloads, List<Downloader> downloads) { + if (notificationCompatBuilder == null) { + return null; + } + + String contentTitle = context.getString(R.string.download_notification_title); + String downloadsLeft = (numDownloads > 0) + ? context.getResources().getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads) + : context.getString(R.string.downloads_processing); + String bigText = compileNotificationString(downloads); + + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); + return notificationCompatBuilder.build(); + } + + private static String compileNotificationString(List<Downloader> downloads) { + List<String> lines = new ArrayList<>(downloads.size()); + for (Downloader downloader : downloads) { + if (downloader.cancelled) { + continue; + } + StringBuilder line = new StringBuilder("• "); + DownloadRequest request = downloader.getDownloadRequest(); + switch (request.getFeedfileType()) { + case Feed.FEEDFILETYPE_FEED: + if (request.getTitle() != null) { + line.append(request.getTitle()); + } + break; + case FeedMedia.FEEDFILETYPE_FEEDMEDIA: + if (request.getTitle() != null) { + line.append(request.getTitle()) + .append(" (") + .append(request.getProgressPercent()) + .append("%)"); + } + break; + default: + line.append("Unknown: ").append(request.getFeedfileType()); + } + lines.add(line.toString()); + } + return TextUtils.join("\n", lines); + } + + /** + * 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) { + // check if report should be created + boolean createReport = false; + int successfulDownloads = 0; + 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.isSuccessful()) { + successfulDownloads++; + } else if (!status.isCancelled()) { + createReport = true; + failedDownloads++; + } + } + + if (createReport) { + Log.d(TAG, "Creating report"); + // create notification object + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, + NotificationUtils.CHANNEL_ID_ERROR) + .setTicker(context.getString(R.string.download_report_title)) + .setContentTitle(context.getString(R.string.download_report_content_title)) + .setContentText( + String.format( + context.getString(R.string.download_report_content), + successfulDownloads, failedDownloads) + ) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setContentIntent( + ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context) + ) + .setAutoCancel(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + } + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(REPORT_ID, builder.build()); + } else { + Log.d(TAG, "No report is created"); + } + } + + 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(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(context, downloadRequest)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + } + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(downloadRequest.getSource().hashCode(), builder.build()); + } +} 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 02dc17301..2a0989d23 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 @@ -17,7 +17,7 @@ public abstract class Downloader implements Callable<Downloader> { private volatile boolean finished; - volatile boolean cancelled; + public volatile boolean cancelled; @NonNull final DownloadRequest request; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java new file mode 100644 index 000000000..d96210a6e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java @@ -0,0 +1,9 @@ +package de.danoeh.antennapod.core.service.download; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface DownloaderFactory { + @Nullable + Downloader create(@NonNull DownloadRequest request); +}
\ 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 new file mode 100644 index 000000000..041d26bd4 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.core.service.download.handler; + +import android.util.Log; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +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 new file mode 100644 index 000000000..10d5bfa15 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -0,0 +1,129 @@ +package de.danoeh.antennapod.core.service.download.handler; + +import android.util.Log; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; +import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.InvalidFeedException; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.util.Date; +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 boolean successful = true; + + public FeedParserTask(DownloadRequest request) { + this.request = request; + } + + @Override + public FeedHandlerResult call() { + Feed feed = new Feed(request.getSource(), request.getLastModified()); + feed.setFile_url(request.getDestination()); + feed.setId(request.getFeedfileId()); + feed.setDownloaded(true); + feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, + request.getUsername(), request.getPassword())); + feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0)); + + DownloadError reason = null; + String reasonDetailed = null; + FeedHandler feedHandler = new FeedHandler(); + + FeedHandlerResult result = null; + try { + result = feedHandler.parseFeed(feed); + Log.d(TAG, feed.getTitle() + " parsed"); + if (!checkFeedData(feed)) { + throw new InvalidFeedException(); + } + + } catch (SAXException | IOException | ParserConfigurationException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + reasonDetailed = e.getMessage(); + } catch (InvalidFeedException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } finally { + File feedFile = new File(request.getDestination()); + if (feedFile.exists()) { + boolean deleted = feedFile.delete(); + Log.d(TAG, "Deletion of file '" + feedFile.getAbsolutePath() + "' " + + (deleted ? "successful" : "FAILED")); + } + } + + if (successful) { + downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), + DownloadError.SUCCESS, successful, reasonDetailed); + return result; + } else { + downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), + reason, successful, reasonDetailed); + return null; + } + } + + public boolean isSuccessful() { + return successful; + } + + /** + * Checks if the feed was parsed correctly. + */ + private boolean checkFeedData(Feed feed) { + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title."); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + return true; + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItems()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + if (item.getPubDate() == null) { + Log.e(TAG, "Item has no pubDate. Using current time as pubDate"); + if (item.getTitle() != null) { + Log.e(TAG, "Title of invalid item: " + item.getTitle()); + } + item.setPubDate(new Date()); + } + } + return true; + } + + public DownloadStatus getDownloadStatus() { + return downloadStatus; + } +} 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 new file mode 100644 index 000000000..718faaa0a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.core.service.download.handler; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; +import java.util.List; + +public class FeedSyncTask { + private static final String TAG = "FeedParserTask"; + private final DownloadRequest request; + private final Context context; + private DownloadStatus downloadStatus; + + public FeedSyncTask(Context context, DownloadRequest request) { + this.request = request; + this.context = context; + } + + public boolean run() { + FeedParserTask task = new FeedParserTask(request); + FeedHandlerResult result = task.call(); + downloadStatus = task.getDownloadStatus(); + + if (!task.isSuccessful()) { + return false; + } + + Feed[] savedFeeds = DBTasks.updateFeed(context, result.feed); + Feed savedFeed = savedFeeds[0]; + // If loadAllPages=true, check if another page is available and queue it for download + final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES); + final Feed feed = result.feed; + if (loadAllPages && feed.getNextPageLink() != null) { + try { + feed.setId(savedFeed.getId()); + DBTasks.loadNextPageOfFeed(context, savedFeed, true); + } catch (DownloadRequestException e) { + Log.e(TAG, "Error trying to load next page", e); + } + } + + ClientConfig.downloadServiceCallbacks.onFeedParsed(context, savedFeed); + return true; + } + + public DownloadStatus getDownloadStatus() { + return downloadStatus; + } +} 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 new file mode 100644 index 000000000..cf5a84eea --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.core.service.download.handler; + +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.util.Log; +import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +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.ChapterUtils; +import de.danoeh.antennapod.core.util.DownloadError; + +import java.util.concurrent.ExecutionException; + +/** + * Handles a completed media download. + */ +public class MediaDownloadedHandler implements Runnable { + private static final String TAG = "MediaDownloadedHandler"; + private final DownloadRequest request; + private final DownloadStatus status; + private final Context context; + private DownloadStatus updatedStatus; + + public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadStatus status, + @NonNull DownloadRequest request) { + this.status = status; + this.request = request; + this.context = context; + } + + @Override + public void run() { + updatedStatus = status; + FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); + if (media == null) { + Log.e(TAG, "Could not find downloaded media object in database"); + return; + } + media.setDownloaded(true); + media.setFile_url(request.getDestination()); + media.checkEmbeddedPicture(); // enforce check + + // check if file has chapters + if (media.getItem() != null && !media.getItem().hasChapters()) { + ChapterUtils.loadChaptersFromFileUrl(media); + } + + // Get duration + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + String durationStr = null; + try { + mmr.setDataSource(media.getFile_url()); + durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + media.setDuration(Integer.parseInt(durationStr)); + Log.d(TAG, "Duration of file is " + media.getDuration()); + } catch (NumberFormatException e) { + Log.d(TAG, "Invalid file duration: " + durationStr); + } catch (Exception e) { + Log.e(TAG, "Get duration failed", e); + } finally { + mmr.release(); + } + + final FeedItem item = media.getItem(); + + try { + DBWriter.setFeedMedia(media).get(); + + // we've received the media, we don't want to autodownload it again + if (item != null) { + item.setAutoDownload(false); + // setFeedItem() signals (via EventBus) that the item has been updated, + // so we do it after the enclosing media has been updated above, + // to ensure subscribers will get the updated FeedMedia as well + DBWriter.setFeedItem(item).get(); + } + + if (item != null && UserPreferences.enqueueDownloadedEpisodes() + && !DBTasks.isInQueue(context, item.getId())) { + DBWriter.addQueueItem(context, item).get(); + } + } catch (InterruptedException e) { + 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()); + } + + + if (GpodnetPreferences.loggedIn() && item != null) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DOWNLOAD) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + } + + public DownloadStatus 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 new file mode 100644 index 000000000..5d2c48679 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java @@ -0,0 +1,29 @@ +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/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index d5245cc10..8b87d7c54 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 @@ -515,13 +515,24 @@ public final class DBReader { * newest events first. */ public static List<DownloadStatus> getFeedDownloadLog(Feed feed) { - Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feed + "]"); + return getFeedDownloadLog(feed.getId()); + } + + /** + * Loads the download log for a particular feed from the database. + * + * @param feedId Feed id for which the download log is loaded + * @return A list with DownloadStatus objects that represent the feed's download log, + * newest events first. + */ + public static List<DownloadStatus> getFeedDownloadLog(long feedId) { + Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feedId + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor cursor = null; try { - cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId()); + cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId); List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { downloadLog.add(DownloadStatus.fromCursor(cursor)); 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 84f57e87a..9d37a5f2a 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 @@ -299,7 +299,7 @@ public final class DBTasks { media.setDownloaded(false); media.setFile_url(null); DBWriter.setFeedMedia(media); - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(media.getItem().getFeed())); } /** @@ -558,13 +558,13 @@ public final class DBTasks { adapter.close(); try { - DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get(); - DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); + DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[0])).get(); + DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[0])).get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(updatedFeedsList)); return resultFeeds; } 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 919123950..8f0626c5c 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 @@ -173,7 +173,7 @@ public class DBWriter { if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); } - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(feed)); // we assume we also removed download log entries for the feed or its media files. // especially important if download or refresh failed, as the user should not be able @@ -803,7 +803,7 @@ public class DBWriter { adapter.open(); adapter.setFeedPreferences(preferences); adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(preferences.getFeedID())); }); } @@ -842,7 +842,7 @@ public class DBWriter { adapter.open(); adapter.setFeedCustomTitle(feed.getId(), feed.getCustomTitle()); adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent()); + EventBus.getDefault().post(new FeedListUpdateEvent(feed)); }); } |