From 056d7db16b16f3ec22e6b455c9640a9e1418963c Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 29 Oct 2019 23:39:29 +0100 Subject: Extracted feed sync from DownloadService --- .../core/service/download/DownloadService.java | 531 +++------------------ .../download/handler/FailedDownloadHandler.java | 33 ++ .../service/download/handler/FeedParserTask.java | 129 +++++ .../service/download/handler/FeedSyncThread.java | 234 +++++++++ .../download/handler/MediaDownloadedHandler.java | 111 +++++ .../danoeh/antennapod/core/storage/DBReader.java | 15 +- 6 files changed, 574 insertions(+), 479 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java (limited to 'core/src') 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..703b9157f 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,74 +7,55 @@ 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 android.text.TextUtils; +import android.util.Log; +import android.webkit.URLUtil; 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 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.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.FeedSyncThread; +import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; +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 de.danoeh.antennapod.core.util.gui.NotificationUtils; import org.apache.commons.io.FileUtils; import org.greenrobot.eventbus.EventBus; -import org.xml.sax.SAXException; 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; - /** * 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_REQUEST field of @@ -171,9 +152,18 @@ public class DownloadService extends Service { final int type = status.getFeedfileType(); if (successful) { if (type == Feed.FEEDFILETYPE_FEED) { - handleCompletedFeedDownload(downloader.getDownloadRequest()); + Log.d(TAG, "Handling completed Feed Download"); + feedSyncThread.submitCompletedDownload(downloader.getDownloadRequest()); } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); + Log.d(TAG, "Handling completed FeedMedia Download"); + syncExecutor.execute(() -> { + MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, + status, downloader.getDownloadRequest()); + handler.run(); + saveDownloadStatus(handler.getUpdatedStatus()); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + }); } } else { numberOfDownloads.decrementAndGet(); @@ -189,7 +179,7 @@ public class DownloadService extends Service { } else { Log.e(TAG, "Download failed"); saveDownloadStatus(status); - handleFailedDownload(status, downloader.getDownloadRequest()); + syncExecutor.execute(new FailedDownloadHandler(downloader.getDownloadRequest())); if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { FeedItem item = getFeedItemFromId(status.getFeedfileId()); @@ -279,7 +269,23 @@ public class DownloadService extends Service { }, (r, executor) -> Log.w(TAG, "SchedEx rejected submission of new task") ); downloadCompletionThread.start(); - feedSyncThread = new FeedSyncThread(); + feedSyncThread = new FeedSyncThread(DownloadService.this, new FeedSyncThread.FeedSyncCallback() { + @Override + public void finishedSyncingFeeds(int numberOfCompletedFeeds) { + numberOfDownloads.addAndGet(-numberOfCompletedFeeds); + queryDownloadsAsync(); + } + + @Override + public void failedSyncingFeed() { + numberOfDownloads.decrementAndGet(); + } + + @Override + public void downloadStatusGenerated(DownloadStatus downloadStatus) { + saveDownloadStatus(downloadStatus); + } + }); feedSyncThread.start(); setupNotificationBuilders(); @@ -334,7 +340,7 @@ public class DownloadService extends Service { .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); + notificationCompatBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } Log.d(TAG, "Notification set up"); @@ -535,7 +541,7 @@ public class DownloadService extends Service { ) .setAutoCancel(true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.setVisibility(Notification.VISIBILITY_PUBLIC); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(REPORT_ID, builder.build()); @@ -583,34 +589,13 @@ public class DownloadService extends Service { .setAutoCancel(true) .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.setVisibility(Notification.VISIBILITY_PUBLIC); + builder.setVisibility(NotificationCompat.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); @@ -621,300 +606,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 completedRequests = new LinkedBlockingDeque<>(); - private final CompletionService> 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> collectCompletedRequests() { - List> 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 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> 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> 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> { - - private final DownloadRequest request; - - private FeedParserTask(DownloadRequest request) { - this.request = request; - } - - @Override - public Pair call() throws Exception { - return parseFeed(request); - } - } - - private Pair 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 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. @@ -951,120 +642,6 @@ public class DownloadService extends Service { } } - /** - * Handles failed downloads. - *

- * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - *

- * 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. */ 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. + *

+ * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location + * of the downloaded file. + *

+ * 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..96119ad47 --- /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 { + 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() throws Exception { + 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/FeedSyncThread.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java new file mode 100644 index 000000000..21cab4ef6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java @@ -0,0 +1,234 @@ +package de.danoeh.antennapod.core.service.download.handler; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; +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.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +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; + +/** + * Takes a single Feed, parses the corresponding file and refreshes + * information in the manager. + */ +public class FeedSyncThread extends Thread { + private static final String TAG = "FeedSyncThread"; + private static final long WAIT_TIMEOUT = 3000; + + private final BlockingQueue completedRequests = new LinkedBlockingDeque<>(); + private final CompletionService> parserService = + new ExecutorCompletionService<>(Executors.newSingleThreadExecutor()); + private final ExecutorService dbService = Executors.newSingleThreadExecutor(); + private final Context context; + private Future dbUpdateFuture; + private final FeedSyncCallback feedSyncCallback; + private volatile boolean isActive = true; + private volatile boolean isCollectingRequests = false; + + public FeedSyncThread(Context context, FeedSyncCallback feedSyncCallback) { + super("FeedSyncThread"); + this.context = context; + this.feedSyncCallback = feedSyncCallback; + } + + /** + * 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> collectCompletedRequests() { + List> results = new LinkedList<>(); + DownloadRequester requester = DownloadRequester.getInstance(); + int tasks = 0; + + try { + DownloadRequest request = completedRequests.take(); + submitParseRequest(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 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 void submitParseRequest(DownloadRequest request) { + parserService.submit(() -> { + FeedParserTask task = new FeedParserTask(request); + FeedHandlerResult result = task.call(); + + if (task.isSuccessful()) { + // we create a 'successful' download log if the feed's last refresh failed + List log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + if (log.size() > 0 && !log.get(0).isSuccessful()) { + feedSyncCallback.downloadStatusGenerated(task.getDownloadStatus()); + } + return Pair.create(request, result); + } else { + feedSyncCallback.failedSyncingFeed(); + feedSyncCallback.downloadStatusGenerated(task.getDownloadStatus()); + return null; + } + }); + } + + private int pollCompletedDownloads() { + int tasks = 0; + while (!completedRequests.isEmpty()) { + DownloadRequest request = completedRequests.poll(); + submitParseRequest(request); + tasks++; + } + return tasks; + } + + @Override + public void run() { + while (isActive) { + final List> 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(context, 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(context, savedFeed, true); + } catch (DownloadRequestException e) { + Log.e(TAG, "Error trying to load next page", e); + } + } + + ClientConfig.downloadServiceCallbacks.onFeedParsed(context, savedFeed); + } + feedSyncCallback.finishedSyncingFeeds(savedFeeds.length); + }); + + } + + 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"); + } + + private Feed[] getFeeds(List> results) { + Feed[] feeds = new Feed[results.size()]; + for (int i = 0; i < results.size(); i++) { + feeds[i] = results.get(i).second.feed; + } + return feeds; + } + + public void shutdown() { + isActive = false; + if (isCollectingRequests) { + interrupt(); + } + } + + public void submitCompletedDownload(DownloadRequest request) { + completedRequests.offer(request); + if (isCollectingRequests) { + interrupt(); + } + } + + public interface FeedSyncCallback { + void finishedSyncingFeeds(int numberOfCompletedFeeds); + void failedSyncingFeed(); + void downloadStatusGenerated(DownloadStatus downloadStatus); + } + +} \ No newline at end of file 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/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 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 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 downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { downloadLog.add(DownloadStatus.fromCursor(cursor)); -- cgit v1.2.3 From 798868db9c31b018daaf276e2d9a314e1adf4cec Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 29 Oct 2019 23:58:44 +0100 Subject: Extracted notification from DownloadService --- .../core/service/download/DownloadService.java | 163 ++------------------ .../download/DownloadServiceNotification.java | 166 +++++++++++++++++++++ 2 files changed, 182 insertions(+), 147 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java (limited to 'core/src') 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 703b9157f..324e3e7b8 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 @@ -98,10 +98,8 @@ public class DownloadService extends Service { private DownloadRequester requester; - - private NotificationCompat.Builder notificationCompatBuilder; - private static final int NOTIFICATION_ID = 2; - private static final int REPORT_ID = 3; + private DownloadServiceNotification notificationManager; + public static final int NOTIFICATION_ID = 2; /** * Currently running downloads. @@ -169,7 +167,7 @@ public class DownloadService extends Service { numberOfDownloads.decrementAndGet(); if (!status.isCancelled()) { if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - postAuthenticationNotification(downloader.getDownloadRequest()); + notificationManager.postAuthenticationNotification(downloader.getDownloadRequest()); } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR && Integer.parseInt(status.getReasonDetailed()) == 416) { @@ -288,9 +286,11 @@ public class DownloadService extends Service { }); feedSyncThread.start(); - setupNotificationBuilders(); requester = DownloadRequester.getInstance(); - startForeground(NOTIFICATION_ID, updateNotifications()); + notificationManager = new DownloadServiceNotification(this); + Notification notification = notificationManager.updateNotifications( + requester.getNumberOfDownloads(), downloads); + startForeground(NOTIFICATION_ID, notification); } @Override @@ -303,9 +303,10 @@ 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); @@ -334,40 +335,6 @@ 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(NotificationCompat.VISIBILITY_PUBLIC); - } - - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * after setupNotificationBuilders. - */ - private Notification updateNotifications() { - if (notificationCompatBuilder == null) { - return null; - } - - 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(); - } - private Downloader getDownloader(String downloadUrl) { for (Downloader downloader : downloads) { if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { @@ -502,55 +469,6 @@ public class DownloadService extends Service { DBWriter.addDownloadStatus(status); } - /** - * 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(NotificationCompat.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. @@ -570,32 +488,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(NotificationCompat.VISIBILITY_PUBLIC); - } - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), builder.build()); - }); - } - @Nullable private FeedItem getFeedItemFromId(long id) { FeedMedia media = DBReader.getFeedMedia(id); @@ -667,7 +565,8 @@ public class DownloadService extends Service { private class NotificationUpdater implements Runnable { public void run() { handler.post(() -> { - Notification n = updateNotifications(); + Notification n = notificationManager.updateNotifications( + requester.getNumberOfDownloads(), downloads); if (n != null) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(NOTIFICATION_ID, n); @@ -703,34 +602,4 @@ public class DownloadService extends Service { lastPost = now; } } - - private static String compileNotificationString(List downloads) { - List 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); - } } 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 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 downloads) { + List 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 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()); + } +} -- cgit v1.2.3 From 74621170fe7431e5011f040d23fed7660a10e473 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 11:10:44 +0100 Subject: Converted our own FeedSyncThread to a task+executor --- .../core/service/download/DownloadService.java | 56 +++-- .../service/download/handler/FeedParserTask.java | 2 +- .../service/download/handler/FeedSyncTask.java | 57 +++++ .../service/download/handler/FeedSyncThread.java | 234 --------------------- 4 files changed, 82 insertions(+), 267 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java (limited to 'core/src') 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 324e3e7b8..1d8f2bad0 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 @@ -8,7 +8,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; -import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.text.TextUtils; @@ -17,9 +16,7 @@ import android.webkit.URLUtil; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.core.app.NotificationCompat; 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; @@ -29,17 +26,13 @@ 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.FeedSyncThread; +import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; 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 de.danoeh.antennapod.core.util.gui.NotificationUtils; -import org.apache.commons.io.FileUtils; -import org.greenrobot.eventbus.EventBus; - import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; @@ -55,6 +48,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +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. @@ -94,7 +89,6 @@ public class DownloadService extends Service { private ExecutorService syncExecutor; private CompletionService downloadExecutor; - private FeedSyncThread feedSyncThread; private DownloadRequester requester; @@ -145,18 +139,36 @@ public class DownloadService extends Service { Log.d(TAG, "Received 'Download Complete' - message."); removeDownload(downloader); DownloadStatus status = downloader.getResult(); + DownloadRequest request = downloader.getDownloadRequest(); boolean successful = status.isSuccessful(); - final int type = status.getFeedfileType(); + if (successful) { if (type == Feed.FEEDFILETYPE_FEED) { Log.d(TAG, "Handling completed Feed Download"); - feedSyncThread.submitCompletedDownload(downloader.getDownloadRequest()); + syncExecutor.execute(() -> { + 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 log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + if (log.size() > 0 && !log.get(0).isSuccessful()) { + saveDownloadStatus(task.getDownloadStatus()); + } + } else { + saveDownloadStatus(task.getDownloadStatus()); + } + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + + }); + } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { Log.d(TAG, "Handling completed FeedMedia Download"); syncExecutor.execute(() -> { MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, - status, downloader.getDownloadRequest()); + status, request); handler.run(); saveDownloadStatus(handler.getUpdatedStatus()); numberOfDownloads.decrementAndGet(); @@ -267,25 +279,6 @@ public class DownloadService extends Service { }, (r, executor) -> Log.w(TAG, "SchedEx rejected submission of new task") ); downloadCompletionThread.start(); - feedSyncThread = new FeedSyncThread(DownloadService.this, new FeedSyncThread.FeedSyncCallback() { - @Override - public void finishedSyncingFeeds(int numberOfCompletedFeeds) { - numberOfDownloads.addAndGet(-numberOfCompletedFeeds); - queryDownloadsAsync(); - } - - @Override - public void failedSyncingFeed() { - numberOfDownloads.decrementAndGet(); - } - - @Override - public void downloadStatusGenerated(DownloadStatus downloadStatus) { - saveDownloadStatus(downloadStatus); - } - }); - feedSyncThread.start(); - requester = DownloadRequester.getInstance(); notificationManager = new DownloadServiceNotification(this); Notification notification = notificationManager.updateNotifications( @@ -319,7 +312,6 @@ public class DownloadService extends Service { downloadCompletionThread.interrupt(); syncExecutor.shutdown(); schedExecutor.shutdown(); - feedSyncThread.shutdown(); cancelNotificationUpdater(); unregisterReceiver(cancelDownloadReceiver); 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 96119ad47..10d5bfa15 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 @@ -31,7 +31,7 @@ public class FeedParserTask implements Callable { } @Override - public FeedHandlerResult call() throws Exception { + public FeedHandlerResult call() { Feed feed = new Feed(request.getSource(), request.getLastModified()); feed.setFile_url(request.getDestination()); feed.setId(request.getFeedfileId()); 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/FeedSyncThread.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java deleted file mode 100644 index 21cab4ef6..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncThread.java +++ /dev/null @@ -1,234 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import android.content.Context; -import android.util.Log; -import android.util.Pair; -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.LinkedList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -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; - -/** - * Takes a single Feed, parses the corresponding file and refreshes - * information in the manager. - */ -public class FeedSyncThread extends Thread { - private static final String TAG = "FeedSyncThread"; - private static final long WAIT_TIMEOUT = 3000; - - private final BlockingQueue completedRequests = new LinkedBlockingDeque<>(); - private final CompletionService> parserService = - new ExecutorCompletionService<>(Executors.newSingleThreadExecutor()); - private final ExecutorService dbService = Executors.newSingleThreadExecutor(); - private final Context context; - private Future dbUpdateFuture; - private final FeedSyncCallback feedSyncCallback; - private volatile boolean isActive = true; - private volatile boolean isCollectingRequests = false; - - public FeedSyncThread(Context context, FeedSyncCallback feedSyncCallback) { - super("FeedSyncThread"); - this.context = context; - this.feedSyncCallback = feedSyncCallback; - } - - /** - * 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> collectCompletedRequests() { - List> results = new LinkedList<>(); - DownloadRequester requester = DownloadRequester.getInstance(); - int tasks = 0; - - try { - DownloadRequest request = completedRequests.take(); - submitParseRequest(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 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 void submitParseRequest(DownloadRequest request) { - parserService.submit(() -> { - FeedParserTask task = new FeedParserTask(request); - FeedHandlerResult result = task.call(); - - if (task.isSuccessful()) { - // we create a 'successful' download log if the feed's last refresh failed - List log = DBReader.getFeedDownloadLog(request.getFeedfileId()); - if (log.size() > 0 && !log.get(0).isSuccessful()) { - feedSyncCallback.downloadStatusGenerated(task.getDownloadStatus()); - } - return Pair.create(request, result); - } else { - feedSyncCallback.failedSyncingFeed(); - feedSyncCallback.downloadStatusGenerated(task.getDownloadStatus()); - return null; - } - }); - } - - private int pollCompletedDownloads() { - int tasks = 0; - while (!completedRequests.isEmpty()) { - DownloadRequest request = completedRequests.poll(); - submitParseRequest(request); - tasks++; - } - return tasks; - } - - @Override - public void run() { - while (isActive) { - final List> 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(context, 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(context, savedFeed, true); - } catch (DownloadRequestException e) { - Log.e(TAG, "Error trying to load next page", e); - } - } - - ClientConfig.downloadServiceCallbacks.onFeedParsed(context, savedFeed); - } - feedSyncCallback.finishedSyncingFeeds(savedFeeds.length); - }); - - } - - 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"); - } - - private Feed[] getFeeds(List> results) { - Feed[] feeds = new Feed[results.size()]; - for (int i = 0; i < results.size(); i++) { - feeds[i] = results.get(i).second.feed; - } - return feeds; - } - - public void shutdown() { - isActive = false; - if (isCollectingRequests) { - interrupt(); - } - } - - public void submitCompletedDownload(DownloadRequest request) { - completedRequests.offer(request); - if (isCollectingRequests) { - interrupt(); - } - } - - public interface FeedSyncCallback { - void finishedSyncingFeeds(int numberOfCompletedFeeds); - void failedSyncingFeed(); - void downloadStatusGenerated(DownloadStatus downloadStatus); - } - -} \ No newline at end of file -- cgit v1.2.3 From fe17f1bcb57968b9542d5ed482a73c17426a62b6 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 17:51:22 +0100 Subject: Extracted handling completed downloads to method --- .../core/service/download/DownloadService.java | 174 +++++++++++---------- 1 file changed, 92 insertions(+), 82 deletions(-) (limited to 'core/src') 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 1d8f2bad0..66b04f2a9 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 @@ -137,102 +137,112 @@ public class DownloadService extends Service { try { Downloader downloader = downloadExecutor.take().get(); Log.d(TAG, "Received 'Download Complete' - message."); - removeDownload(downloader); - DownloadStatus status = downloader.getResult(); - DownloadRequest request = downloader.getDownloadRequest(); - boolean successful = status.isSuccessful(); - final int type = status.getFeedfileType(); - - if (successful) { - if (type == Feed.FEEDFILETYPE_FEED) { - Log.d(TAG, "Handling completed Feed Download"); - syncExecutor.execute(() -> { - 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 log = DBReader.getFeedDownloadLog(request.getFeedfileId()); - if (log.size() > 0 && !log.get(0).isSuccessful()) { - saveDownloadStatus(task.getDownloadStatus()); - } - } else { - saveDownloadStatus(task.getDownloadStatus()); - } - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - - }); - - } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(() -> { - MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, - status, request); - handler.run(); - saveDownloadStatus(handler.getUpdatedStatus()); - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - }); - } + + if (downloader.getResult().isSuccessful()) { + syncExecutor.execute(() -> { + handleSuccessfulDownload(downloader); + removeDownload(downloader); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + + }); } else { + handleFailedDownload(downloader); + removeDownload(downloader); numberOfDownloads.decrementAndGet(); - 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) { - 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"); } }; + private void handleSuccessfulDownload(Downloader downloader) { + DownloadRequest request = downloader.getDownloadRequest(); + DownloadStatus status = downloader.getResult(); + final int type = status.getFeedfileType(); + + if (type == Feed.FEEDFILETYPE_FEED) { + Log.d(TAG, "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 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(); + + 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)); + } + } + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { -- cgit v1.2.3 From 366e60990db03c03be318ffab7055184f5a1b553 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 18:05:18 +0100 Subject: Posting downloaders on executor --- .../core/service/download/DownloadService.java | 114 +++++++++------------ .../core/service/download/Downloader.java | 2 +- .../download/handler/PostDownloaderTask.java | 29 ++++++ 3 files changed, 78 insertions(+), 67 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java (limited to 'core/src') 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 66b04f2a9..023069715 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 @@ -28,6 +28,7 @@ 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; @@ -82,23 +83,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 reportQueue; - - private ExecutorService syncExecutor; - private CompletionService downloadExecutor; - - private DownloadRequester requester; - - private DownloadServiceNotification notificationManager; - public static final int NOTIFICATION_ID = 2; + private final List reportQueue; + private final ExecutorService syncExecutor; + private final CompletionService downloadExecutor; + private final DownloadRequester requester; + private final DownloadServiceNotification notificationManager; /** * Currently running downloads. */ - private List downloads; + private final List downloads; /** * Number of running downloads. @@ -114,11 +113,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 final IBinder mBinder = new LocalBinder(); private class LocalBinder extends Binder { @@ -243,29 +241,13 @@ public class DownloadService extends Service { } } - @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(); + notificationManager = new DownloadServiceNotification(this); - 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); @@ -288,9 +270,31 @@ 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(); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + downloadCompletionThread.start(); - requester = DownloadRequester.getInstance(); - notificationManager = new DownloadServiceNotification(this); + Notification notification = notificationManager.updateNotifications( requester.getNumberOfDownloads(), downloads); startForeground(NOTIFICATION_ID, notification); @@ -312,7 +316,6 @@ public class DownloadService extends Service { reportQueue.clear(); } - postHandler.removeCallbacks(postDownloaderTask); EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); stopForeground(true); @@ -323,6 +326,7 @@ public class DownloadService extends Service { syncExecutor.shutdown(); schedExecutor.shutdown(); cancelNotificationUpdater(); + downloadPostFuture.cancel(true); unregisterReceiver(cancelDownloadReceiver); // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, @@ -566,42 +570,20 @@ public class DownloadService extends Service { private class NotificationUpdater implements Runnable { public void run() { - handler.post(() -> { - Notification n = notificationManager.updateNotifications( - requester.getNumberOfDownloads(), downloads); - 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 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 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(); + + if (downloadPostFuture == null) { + downloadPostFuture = schedExecutor.scheduleAtFixedRate( + new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS); } } } 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 { 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/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 downloads; + + public PostDownloaderTask(List downloads) { + this.downloads = downloads; + } + + @Override + public void run() { + List runningDownloads = new ArrayList<>(); + for (Downloader downloader : downloads) { + if (!downloader.cancelled) { + runningDownloads.add(downloader); + } + } + List list = Collections.unmodifiableList(runningDownloads); + EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); + } +} -- cgit v1.2.3 From 8f168948add874c7f55f2ad9fcfc6d15f7b17f9b Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 18:06:44 +0100 Subject: Reordered methods to have setup on top --- .../core/service/download/DownloadService.java | 200 ++++++++++----------- 1 file changed, 100 insertions(+), 100 deletions(-) (limited to 'core/src') 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 023069715..fbc2abb43 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 @@ -125,6 +125,106 @@ public class DownloadService extends Service { } } + public DownloadService() { + reportQueue = Collections.synchronizedList(new ArrayList<>()); + downloads = Collections.synchronizedList(new ArrayList<>()); + numberOfDownloads = new AtomicInteger(0); + requester = DownloadRequester.getInstance(); + notificationManager = new DownloadServiceNotification(this); + + syncExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + }); + Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); + downloadExecutor = new ExecutorCompletionService<>( + Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), + r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + ) + ); + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, + r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + 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(); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + + downloadCompletionThread.start(); + + Notification notification = notificationManager.updateNotifications( + requester.getNumberOfDownloads(), downloads); + startForeground(NOTIFICATION_ID, notification); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + Log.d(TAG, "Service shutting down"); + isRunning = false; + + if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() + && UserPreferences.showDownloadReport()) { + notificationManager.updateReport(reportQueue); + reportQueue.clear(); + } + + EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); + + stopForeground(true); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + + downloadCompletionThread.interrupt(); + syncExecutor.shutdown(); + schedExecutor.shutdown(); + cancelNotificationUpdater(); + downloadPostFuture.cancel(true); + unregisterReceiver(cancelDownloadReceiver); + + // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, + // it is now time to sync the episode actions + if (GpodnetPreferences.loggedIn() && + GpodnetPreferences.getLastSubscriptionSyncTimestamp() > 0 && + GpodnetPreferences.getLastEpisodeActionsSyncTimestamp() == 0) { + GpodnetSyncService.sendSyncActionsIntent(this); + } + + // start auto download in case anything new has shown up + DBTasks.autodownloadUndownloadedItems(getApplicationContext()); + } + private final Thread downloadCompletionThread = new Thread("DownloadCompletionThread") { private static final String TAG = "downloadCompletionThd"; @@ -241,106 +341,6 @@ public class DownloadService extends Service { } } - public DownloadService() { - reportQueue = Collections.synchronizedList(new ArrayList<>()); - downloads = Collections.synchronizedList(new ArrayList<>()); - numberOfDownloads = new AtomicInteger(0); - requester = DownloadRequester.getInstance(); - notificationManager = new DownloadServiceNotification(this); - - syncExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); - downloadExecutor = new ExecutorCompletionService<>( - Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), - r -> { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - ) - ); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - r -> { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - 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(); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - - downloadCompletionThread.start(); - - Notification notification = notificationManager.updateNotifications( - requester.getNumberOfDownloads(), downloads); - startForeground(NOTIFICATION_ID, notification); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onDestroy() { - Log.d(TAG, "Service shutting down"); - isRunning = false; - - if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() - && UserPreferences.showDownloadReport()) { - notificationManager.updateReport(reportQueue); - reportQueue.clear(); - } - - EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); - - stopForeground(true); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - - downloadCompletionThread.interrupt(); - syncExecutor.shutdown(); - schedExecutor.shutdown(); - cancelNotificationUpdater(); - downloadPostFuture.cancel(true); - unregisterReceiver(cancelDownloadReceiver); - - // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, - // it is now time to sync the episode actions - if (GpodnetPreferences.loggedIn() && - GpodnetPreferences.getLastSubscriptionSyncTimestamp() > 0 && - GpodnetPreferences.getLastEpisodeActionsSyncTimestamp() == 0) { - GpodnetSyncService.sendSyncActionsIntent(this); - } - - // start auto download in case anything new has shown up - DBTasks.autodownloadUndownloadedItems(getApplicationContext()); - } - private Downloader getDownloader(String downloadUrl) { for (Downloader downloader : downloads) { if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { -- cgit v1.2.3 From ea6b41116ff4894981182bd6305df2f5c094ab57 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 18:11:01 +0100 Subject: Extracted DownloaderFactory to new class --- .../service/download/DefaultDownloaderFactory.java | 20 +++++++++++++++ .../core/service/download/DownloadService.java | 29 ++-------------------- .../core/service/download/DownloaderFactory.java | 9 +++++++ 3 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DefaultDownloaderFactory.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderFactory.java (limited to 'core/src') 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 fbc2abb43..24d32e962 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 @@ -12,8 +12,6 @@ import android.os.Handler; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; -import android.webkit.URLUtil; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import de.danoeh.antennapod.core.ClientConfig; @@ -116,6 +114,7 @@ public class DownloadService extends Service { private ScheduledFuture downloadPostFuture; private static final int SCHED_EX_POOL_SIZE = 1; private ScheduledThreadPoolExecutor schedExecutor; + private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); private final IBinder mBinder = new LocalBinder(); @@ -397,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 @@ -414,26 +413,6 @@ public class DownloadService extends Service { queryDownloads(); } - @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; @@ -446,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. 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 -- cgit v1.2.3 From dd5ba4d38de2134e06927b1edad9a3c8feb43e9f Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 18:16:04 +0100 Subject: Do not access context in constructor --- .../de/danoeh/antennapod/core/service/download/DownloadService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'core/src') 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 24d32e962..80444a62b 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 @@ -90,7 +90,7 @@ public class DownloadService extends Service { private final ExecutorService syncExecutor; private final CompletionService downloadExecutor; private final DownloadRequester requester; - private final DownloadServiceNotification notificationManager; + private DownloadServiceNotification notificationManager; /** * Currently running downloads. @@ -129,7 +129,6 @@ public class DownloadService extends Service { downloads = Collections.synchronizedList(new ArrayList<>()); numberOfDownloads = new AtomicInteger(0); requester = DownloadRequester.getInstance(); - notificationManager = new DownloadServiceNotification(this); syncExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r); @@ -170,6 +169,7 @@ public class DownloadService extends Service { Log.d(TAG, "Service started"); isRunning = true; handler = new Handler(); + notificationManager = new DownloadServiceNotification(this); IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); -- cgit v1.2.3 From 6825cca32acbb615919e4c8b53b46a072226385d Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 18:19:36 +0100 Subject: Updating notification more often --- .../de/danoeh/antennapod/core/service/download/DownloadService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'core/src') 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 80444a62b..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 @@ -528,8 +528,7 @@ public class DownloadService extends Service { 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); } } -- cgit v1.2.3 From 69f3a1210fcbd199cd24c4060da4518e0dda2146 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 30 Oct 2019 19:27:38 +0100 Subject: Do not refresh feeds more often than necessary --- .../antennapod/core/event/FeedListUpdateEvent.java | 24 +++++++++++++++++++++- .../de/danoeh/antennapod/core/storage/DBTasks.java | 8 ++++---- .../danoeh/antennapod/core/storage/DBWriter.java | 6 +++--- 3 files changed, 30 insertions(+), 8 deletions(-) (limited to 'core/src') 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 feeds = new ArrayList<>(); + + public FeedListUpdateEvent(List 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/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)); }); } -- cgit v1.2.3