diff options
Diffstat (limited to 'core/src/main')
13 files changed, 351 insertions, 422 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 9046b7165..79c6dd6bc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -8,9 +8,8 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import org.apache.commons.io.IOUtils; import org.xmlpull.v1.XmlPullParserException; @@ -30,6 +29,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import de.danoeh.antennapod.core.export.opml.OpmlElement; import de.danoeh.antennapod.core.export.opml.OpmlReader; @@ -144,9 +144,10 @@ public class OpmlBackupAgent extends BackupAgentHelper { mChecksum = digester == null ? null : digester.digest(); for (OpmlElement opmlElem : opmlElements) { Feed feed = new Feed(opmlElem.getXmlUrl(), null, opmlElem.getText()); - DownloadRequest request = DownloadRequestCreator.create(feed).build(); - DownloadServiceInterface.get().download(mContext, false, request); + feed.setItems(Collections.emptyList()); + DBTasks.updateFeed(mContext, feed, false); } + FeedUpdateManager.runOnce(mContext); } catch (XmlPullParserException e) { Log.e(TAG, "Error while parsing the OPML file", e); } catch (IOException e) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index 9ce89ebe2..e30b49280 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; /** * Refreshes all feeds when it receives an intent @@ -20,7 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { Log.d(TAG, "Received intent"); ClientConfigurator.initialize(context); - AutoUpdateManager.runOnce(context); + FeedUpdateManager.runOnce(context); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index 49c5211b0..bbf0dc357 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -1,47 +1,177 @@ package de.danoeh.antennapod.core.service; +import android.app.Notification; import android.content.Context; -import androidx.annotation.NonNull; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.ForegroundInfo; +import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; - +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.service.download.DefaultDownloaderFactory; +import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; +import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -public class FeedUpdateWorker extends Worker { +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +public class FeedUpdateWorker extends Worker { private static final String TAG = "FeedUpdateWorker"; - public static final String PARAM_RUN_ONCE = "runOnce"; + private final NewEpisodesNotification newEpisodesNotification; public FeedUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); + newEpisodesNotification = new NewEpisodesNotification(); } @Override @NonNull public Result doWork() { - final boolean isRunOnce = getInputData().getBoolean(PARAM_RUN_ONCE, false); - Log.d(TAG, "doWork() : isRunOnce = " + isRunOnce); ClientConfigurator.initialize(getApplicationContext()); + newEpisodesNotification.loadCountersBeforeRefresh(); - if (NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed()) { - DBTasks.refreshAllFeeds(getApplicationContext(), false); - } else { + if (!NetworkUtils.networkAvailable() || !NetworkUtils.isFeedRefreshAllowed()) { Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); + return Result.retry(); } - if (!isRunOnce && UserPreferences.isAutoUpdateTimeOfDay()) { - // WorkManager does not allow to set specific time for repeated tasks. - // We repeatedly schedule a OneTimeWorkRequest instead. - AutoUpdateManager.restartUpdateAlarm(getApplicationContext()); + List<Feed> toUpdate; + long feedId = getInputData().getLong(FeedUpdateManager.EXTRA_FEED_ID, -1); + if (feedId == -1) { // Update all + toUpdate = DBReader.getFeedList(); + Iterator<Feed> itr = toUpdate.iterator(); + while (itr.hasNext()) { + Feed feed = itr.next(); + if (!feed.getPreferences().getKeepUpdated()) { + itr.remove(); + } + } + Collections.shuffle(toUpdate); // If the worker gets cancelled early, every feed has a chance to be updated + refreshFeeds(toUpdate, false); + } else { + toUpdate = new ArrayList<>(); + Feed feed = DBReader.getFeed(feedId); + if (feed == null) { + return Result.success(); + } + toUpdate.add(feed); + refreshFeeds(toUpdate, true); } - return Result.success(); } + + @NonNull + private ForegroundInfo createForegroundInfo(List<Feed> toUpdate) { + Context context = getApplicationContext(); + String contentText = context.getResources().getQuantityString(R.plurals.downloads_left, + toUpdate.size(), toUpdate.size()); + String bigText = Stream.of(toUpdate).map(feed -> "• " + feed.getTitle()).collect(Collectors.joining("\n")); + Notification notification = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) + .setContentTitle(context.getString(R.string.download_notification_title_feeds)) + .setContentText(contentText) + .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) + .setSmallIcon(R.drawable.ic_notification_sync) + .setOngoing(true) + .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), + WorkManager.getInstance(context).createCancelPendingIntent(getId())) + .build(); + return new ForegroundInfo(R.id.notification_updating_feeds, notification); + } + + private void refreshFeeds(List<Feed> toUpdate, boolean force) { + while (!toUpdate.isEmpty()) { + if (isStopped()) { + return; + } + setForegroundAsync(createForegroundInfo(toUpdate)); + Feed feed = toUpdate.get(0); + try { + if (feed.isLocalFeed()) { + LocalFeedUpdater.updateFeed(feed, getApplicationContext(), null); + } else { + refreshFeed(feed, force); + } + } catch (Exception e) { + DBWriter.setFeedLastUpdateFailed(feed.getId(), true); + DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, e.getMessage(), true); + DBWriter.addDownloadStatus(status); + } + toUpdate.remove(0); + } + } + + void refreshFeed(Feed feed, boolean force) throws Exception { + boolean nextPage = getInputData().getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false) + && feed.getNextPageLink() != null; + if (nextPage) { + feed.setPageNr(feed.getPageNr() + 1); + } + DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); + builder.setForce(force || feed.hasLastUpdateFailed()); + if (nextPage) { + builder.setSource(feed.getNextPageLink()); + } + DownloadRequest request = builder.build(); + + Downloader downloader = new DefaultDownloaderFactory().create(request); + if (downloader == null) { + throw new Exception("Unable to create downloader"); + } + + downloader.call(); + + if (!downloader.getResult().isSuccessful()) { + if (downloader.getResult().isCancelled()) { + return; + } + DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); + DBWriter.addDownloadStatus(downloader.getResult()); + return; + } + + FeedSyncTask feedSyncTask = new FeedSyncTask(getApplicationContext(), request); + boolean success = feedSyncTask.run(); + + if (!success) { + DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); + DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); + return; + } + + if (request.getFeedfileId() == 0) { + return; // No download logs for new subscriptions + } + // we create a 'successful' download log if the feed's last refresh failed + List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + if (log.size() > 0 && !log.get(0).isSuccessful()) { + DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); + } + newEpisodesNotification.showIfNeeded(getApplicationContext(), feedSyncTask.getSavedFeed()); + if (downloader.permanentRedirectUrl != null) { + DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl); + } else if (feedSyncTask.getRedirectUrl() != null) { + DBWriter.updateFeedDownloadURL(request.getSource(), feedSyncTask.getRedirectUrl()); + } + } } 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 0e55e9a36..9c238137e 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 @@ -10,19 +10,29 @@ import android.content.IntentFilter; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.ServiceCompat; - import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; +import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; +import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; +import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.apache.commons.io.FileUtils; import org.greenrobot.eventbus.EventBus; @@ -39,22 +49,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; -import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; -import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; -import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.model.download.DownloadError; - /** * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent. * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUESTS field of @@ -69,7 +63,6 @@ public class DownloadService extends Service { public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAll"; public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; public static final String EXTRA_REQUESTS = "downloadRequests"; - public static final String EXTRA_REFRESH_ALL = "refreshAll"; public static final String EXTRA_INITIATED_BY_USER = "initiatedByUser"; public static final String EXTRA_CLEANUP_MEDIA = "cleanupMedia"; @@ -85,7 +78,6 @@ public class DownloadService extends Service { private final List<DownloadStatus> reportQueue = new ArrayList<>(); private final List<DownloadRequest> failedRequestsForReport = new ArrayList<>(); private DownloadServiceNotification notificationManager; - private final NewEpisodesNotification newEpisodesNotification; private NotificationUpdater notificationUpdater; private ScheduledFuture<?> notificationUpdaterFuture; private ScheduledFuture<?> downloadPostFuture; @@ -99,16 +91,12 @@ public class DownloadService extends Service { } public DownloadService() { - newEpisodesNotification = new NewEpisodesNotification(); downloadEnqueueExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "EnqueueThread"); t.setPriority(Thread.MIN_PRIORITY); return t; }); - // Must be the first runnable in syncExecutor - downloadEnqueueExecutor.execute(newEpisodesNotification::loadCountersBeforeRefresh); - Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); downloadHandleExecutor = Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), r -> { @@ -140,18 +128,6 @@ public class DownloadService extends Service { connectionMonitor.enable(getApplicationContext()); } - public static boolean isDownloadingFeeds() { - if (!isRunning) { - return false; - } - for (Downloader downloader : downloads) { - if (downloader.request.getFeedfileType() == Feed.FEEDFILETYPE_FEED && !downloader.cancelled) { - return true; - } - } - return false; - } - public static boolean isDownloadingFile(String downloadUrl) { if (!isRunning) { return false; @@ -182,13 +158,6 @@ public class DownloadService extends Service { NotificationManagerCompat.from(this).cancel(R.id.notification_auto_download_report); setupNotificationUpdaterIfNecessary(); downloadEnqueueExecutor.execute(() -> onDownloadQueued(intent)); - } else if (intent != null && intent.getBooleanExtra(EXTRA_REFRESH_ALL, false)) { - Notification notification = notificationManager.updateNotifications(downloads); - startForeground(R.id.notification_downloading, notification); - NotificationManagerCompat.from(this).cancel(R.id.notification_download_report); - NotificationManagerCompat.from(this).cancel(R.id.notification_auto_download_report); - setupNotificationUpdaterIfNecessary(); - downloadEnqueueExecutor.execute(() -> enqueueAll(intent)); } else if (downloads.size() == 0) { shutdown(); } else { @@ -251,61 +220,12 @@ public class DownloadService extends Service { }); } - /** - * This method MUST NOT, in any case, throw an exception. - * Otherwise, it hangs up the refresh thread pool. - */ - private void performLocalFeedRefresh(Downloader downloader, DownloadRequest request) { - try { - Feed feed = DBReader.getFeed(request.getFeedfileId()); - LocalFeedUpdater.updateFeed(feed, DownloadService.this, (scanned, totalFiles) -> { - request.setSize(totalFiles); - request.setSoFar(scanned); - request.setProgressPercent((int) (100.0 * scanned / totalFiles)); - }); - } catch (Exception e) { - e.printStackTrace(); - } - downloadEnqueueExecutor.submit(() -> { - downloads.remove(downloader); - stopServiceIfEverythingDone(); - }); - } - - private void handleSuccessfulDownload(Downloader downloader) { DownloadRequest request = downloader.getDownloadRequest(); DownloadStatus status = downloader.getResult(); final int type = status.getFeedfileType(); - if (type == Feed.FEEDFILETYPE_FEED) { - Log.d(TAG, "Handling completed Feed Download"); - FeedSyncTask feedSyncTask = new FeedSyncTask(DownloadService.this, request); - boolean success = feedSyncTask.run(); - - if (success) { - if (request.getFeedfileId() == 0) { - return; // No download logs for new subscriptions - } - // we create a 'successful' download log if the feed's last refresh failed - List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); - if (log.size() > 0 && !log.get(0).isSuccessful()) { - saveDownloadStatus(feedSyncTask.getDownloadStatus(), downloader.getDownloadRequest()); - } - if (!request.isInitiatedByUser()) { - // Was stored in the database before and not initiated manually - newEpisodesNotification.showIfNeeded(DownloadService.this, feedSyncTask.getSavedFeed()); - } - if (downloader.permanentRedirectUrl != null) { - DBWriter.updateFeedDownloadURL(request.getSource(), downloader.permanentRedirectUrl); - } else if (feedSyncTask.getRedirectUrl() != null) { - DBWriter.updateFeedDownloadURL(request.getSource(), feedSyncTask.getRedirectUrl()); - } - } else { - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - saveDownloadStatus(feedSyncTask.getDownloadStatus(), downloader.getDownloadRequest()); - } - } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { Log.d(TAG, "Handling completed FeedMedia Download"); MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, status, request); handler.run(); @@ -461,23 +381,6 @@ public class DownloadService extends Service { } } - private void enqueueAll(Intent intent) { - boolean initiatedByUser = intent.getBooleanExtra(EXTRA_INITIATED_BY_USER, false); - List<Feed> feeds = DBReader.getFeedList(); - for (Feed feed : feeds) { - if (feed.getPreferences().getKeepUpdated()) { - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - builder.withInitiatedByUser(initiatedByUser); - if (feed.hasLastUpdateFailed()) { - builder.setForce(true); - } - addNewRequest(builder.build()); - } - } - postDownloaders(); - stopServiceIfEverythingDone(); - } - private void addNewRequest(@NonNull DownloadRequest request) { if (isDownloadingFile(request.getSource())) { Log.d(TAG, "Skipped enqueueing request. Already running."); @@ -487,17 +390,11 @@ public class DownloadService extends Service { return; } Log.d(TAG, "Add new request: " + request.getSource()); - if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) { - Downloader downloader = new LocalFeedStubDownloader(request); + writeFileUrl(request); + Downloader downloader = downloaderFactory.create(request); + if (downloader != null) { downloads.add(downloader); - downloadHandleExecutor.submit(() -> performLocalFeedRefresh(downloader, request)); - } else { - writeFileUrl(request); - Downloader downloader = downloaderFactory.create(request); - if (downloader != null) { - downloads.add(downloader); - downloadHandleExecutor.submit(() -> performDownload(downloader)); - } + downloadHandleExecutor.submit(() -> performDownload(downloader)); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java index 7b7e52e0e..976d8255f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java @@ -5,6 +5,7 @@ import android.content.Intent; import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.util.Log; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; @@ -49,10 +50,7 @@ public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { } public void refreshAllFeeds(Context context, boolean initiatedByUser) { - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REFRESH_ALL, true); - launchIntent.putExtra(DownloadService.EXTRA_INITIATED_BY_USER, initiatedByUser); - ContextCompat.startForegroundService(context, launchIntent); + FeedUpdateManager.runOnce(context); } public void cancel(Context context, String url) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index e3010fe24..9cb1166b4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -1,23 +1,20 @@ package de.danoeh.antennapod.core.service.download.handler; import android.content.Context; - import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; public class FeedSyncTask { - private final DownloadRequest request; private final Context context; private Feed savedFeed; private final FeedParserTask task; private FeedHandlerResult feedHandlerResult; public FeedSyncTask(Context context, DownloadRequest request) { - this.request = request; this.context = context; this.task = new FeedParserTask(request); } @@ -29,13 +26,6 @@ public class FeedSyncTask { } savedFeed = DBTasks.updateFeed(context, feedHandlerResult.feed, false); - // If loadAllPages=true, check if another page is available and queue it for download - final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequest.REQUEST_ARG_LOAD_ALL_PAGES); - final Feed feed = feedHandlerResult.feed; - if (loadAllPages && feed.getNextPageLink() != null) { - feed.setId(savedFeed.getId()); - DBTasks.loadNextPageOfFeed(context, feed, true); - } return true; } 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 18f157908..8b79d594c 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 @@ -1,20 +1,28 @@ package de.danoeh.antennapod.core.storage; -import static android.content.Context.MODE_PRIVATE; - import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.VisibleForTesting; - -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.storage.database.PodDBAdapter; import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; @@ -29,31 +37,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - /** * Provides methods for doing common tasks that use DBReader and DBWriter. */ public final class DBTasks { private static final String TAG = "DBTasks"; - private static final String PREF_NAME = "dbtasks"; - private static final String PREF_LAST_REFRESH = "last_refresh"; - /** * Executor service used by the autodownloadUndownloadedEpisodes method. */ @@ -104,68 +93,12 @@ public final class DBTasks { } } - /** - * Refreshes all feeds. - * It must not be from the main thread. - * This method might ignore subsequent calls if it is still - * enqueuing Feeds for download from a previous call - * - * @param context Might be used for accessing the database - * @param initiatedByUser a boolean indicating if the refresh was triggered by user action. - */ - public static void refreshAllFeeds(final Context context, boolean initiatedByUser) { - DownloadServiceInterface.get().refreshAllFeeds(context, initiatedByUser); - - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); - prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); - - SynchronizationQueueSink.syncNow(); - // Note: automatic download of episodes will be done but not here. - // Instead it is done after all feeds have been refreshed (asynchronously), - // in DownloadService.onDestroy() - // See Issue #2577 for the details of the rationale - } - - - - /** - * Queues the next page of this Feed for download. The given Feed has to be a paged - * Feed (isPaged()=true) and must contain a nextPageLink. - * - * @param context Used for requesting the download. - * @param feed The feed whose next page should be loaded. - * @param loadAllPages True if any subsequent pages should also be loaded, false otherwise. - */ - public static void loadNextPageOfFeed(final Context context, Feed feed, boolean loadAllPages) { - if (feed.isPaged() && feed.getNextPageLink() != null) { - int pageNr = feed.getPageNr() + 1; - Feed nextFeed = new Feed(feed.getNextPageLink(), null, feed.getTitle() + "(" + pageNr + ")"); - nextFeed.setPageNr(pageNr); - nextFeed.setPaged(true); - nextFeed.setId(feed.getId()); - - DownloadRequest.Builder builder = DownloadRequestCreator.create(nextFeed); - builder.loadAllPages(loadAllPages); - DownloadServiceInterface.get().download(context, false, builder.build()); - } else { - Log.e(TAG, "loadNextPageOfFeed: Feed was either not paged or contained no nextPageLink"); - } - } - public static void forceRefreshFeed(Context context, Feed feed, boolean initiatedByUser) { forceRefreshFeed(context, feed, false, initiatedByUser); } - public static void forceRefreshCompleteFeed(final Context context, final Feed feed) { - forceRefreshFeed(context, feed, true, true); - } - private static void forceRefreshFeed(Context context, Feed feed, boolean loadAllPages, boolean initiatedByUser) { - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - builder.withInitiatedByUser(initiatedByUser); - builder.setForce(true); - builder.loadAllPages(loadAllPages); - DownloadServiceInterface.get().download(context, false, builder.build()); + FeedUpdateManager.runOnce(context, feed); } /** 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 9b4146f15..dcee8a45a 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 @@ -665,6 +665,15 @@ public class DBWriter { adapter.close(); } + public static Future<?> resetPagedFeedPage(Feed feed) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.resetPagedFeedPage(feed); + adapter.close(); + }); + } + /* * Sets the 'read'-attribute of all specified FeedItems * @@ -698,7 +707,6 @@ public class DBWriter { }); } - /** * Sets the 'read'-attribute of a FeedItem to the specified value. * diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 73f467154..2fd492cbd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -20,16 +20,15 @@ import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; +import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -153,9 +152,10 @@ public class SyncService extends Worker { continue; } if (!UrlChecker.containsUrl(localSubscriptions, downloadUrl) && !queuedRemovedFeeds.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, null); - DownloadRequest.Builder builder = DownloadRequestCreator.create(feed); - DownloadServiceInterface.get().download(getApplicationContext(), false, builder.build()); + Feed feed = new Feed(downloadUrl, null, "Unknown podcast"); + feed.setItems(Collections.emptyList()); + Feed newFeed = DBTasks.updateFeed(getApplicationContext(), feed, false); + FeedUpdateManager.runOnce(getApplicationContext(), newFeed); } } @@ -193,9 +193,13 @@ public class SyncService extends Worker { private void waitForDownloadServiceCompleted() { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_wait_for_downloads)); try { - while (DownloadService.isRunning) { + while (true) { //noinspection BusyWait Thread.sleep(1000); + FeedUpdateRunningEvent event = EventBus.getDefault().getStickyEvent(FeedUpdateRunningEvent.class); + if (event == null || !event.isFeedUpdateRunning) { + return; + } } } catch (InterruptedException e) { e.printStackTrace(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java deleted file mode 100644 index 0602fc4fe..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java +++ /dev/null @@ -1,167 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.FeedUpdateWorker; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.util.NetworkUtils; - -public class AutoUpdateManager { - private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; - private static final String WORK_ID_FEED_UPDATE_ONCE = WORK_ID_FEED_UPDATE + "Once"; - private static final String TAG = "AutoUpdateManager"; - - private AutoUpdateManager() { - - } - - /** - * Start / restart periodic auto feed refresh - * @param context Context - */ - public static void restartUpdateAlarm(Context context) { - if (UserPreferences.isAutoUpdateDisabled()) { - disableAutoUpdate(context); - } else if (UserPreferences.isAutoUpdateTimeOfDay()) { - int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); - restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1], context); - } else { - long milliseconds = UserPreferences.getUpdateInterval(); - restartUpdateIntervalAlarm(milliseconds, context); - } - } - - /** - * Sets the interval in which the feeds are refreshed automatically - */ - private static void restartUpdateIntervalAlarm(long intervalMillis, Context context) { - Log.d(TAG, "Restarting update alarm."); - - PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(FeedUpdateWorker.class, - intervalMillis, TimeUnit.MILLISECONDS) - .setConstraints(getConstraints()) - .build(); - - WorkManager.getInstance(context).enqueueUniquePeriodicWork( - WORK_ID_FEED_UPDATE, ExistingPeriodicWorkPolicy.REPLACE, workRequest); - } - - /** - * Sets time of day the feeds are refreshed automatically - */ - private static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute, Context context) { - Log.d(TAG, "Restarting update alarm."); - - Calendar now = Calendar.getInstance(); - Calendar alarm = (Calendar) now.clone(); - alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); - alarm.set(Calendar.MINUTE, minute); - if (alarm.before(now) || alarm.equals(now)) { - alarm.add(Calendar.DATE, 1); - } - long triggerAtMillis = alarm.getTimeInMillis() - now.getTimeInMillis(); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) - .setConstraints(getConstraints()) - .setInitialDelay(triggerAtMillis, TimeUnit.MILLISECONDS) - .build(); - - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE, - ExistingWorkPolicy.REPLACE, workRequest); - } - - /** - * Run auto feed refresh once in background, as soon as what OS scheduling allows. - * - * Callers from UI should use {@link #runImmediate(Context)}, as it will guarantee - * the refresh be run immediately. - * @param context Context - */ - public static void runOnce(Context context) { - Log.d(TAG, "Run auto update once, as soon as OS allows."); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) - .setConstraints(getConstraints()) - .setInitialDelay(0L, TimeUnit.MILLISECONDS) - .setInputData(new Data.Builder() - .putBoolean(FeedUpdateWorker.PARAM_RUN_ONCE, true) - .build() - ) - .build(); - - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_ONCE, - ExistingWorkPolicy.REPLACE, workRequest); - - } - - /** - /** - * Run auto feed refresh once in background immediately, using its own thread. - * - * Callers where the additional threads is not suitable should use {@link #runOnce(Context)} - */ - public static void runImmediate(@NonNull Context context) { - Log.d(TAG, "Run auto update immediately in background."); - if (!NetworkUtils.networkAvailable()) { - Log.d(TAG, "Ignoring: No network connection."); - } else if (NetworkUtils.isFeedRefreshAllowed()) { - startRefreshAllFeeds(context); - } else { - confirmMobileAllFeedsRefresh(context); - } - } - - private static void confirmMobileAllFeedsRefresh(final Context context) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.feed_refresh_title) - .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message) - .setPositiveButton(R.string.confirm_mobile_streaming_button_once, - (dialog, which) -> startRefreshAllFeeds(context)) - .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { - UserPreferences.setAllowMobileFeedRefresh(true); - startRefreshAllFeeds(context); - }) - .setNegativeButton(R.string.no, null); - builder.show(); - } - - private static void startRefreshAllFeeds(final Context context) { - new Thread(() -> DBTasks.refreshAllFeeds( - context.getApplicationContext(), true), "ManualRefreshAllFeeds").start(); - } - - public static void disableAutoUpdate(Context context) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); - } - - private static Constraints getConstraints() { - Constraints.Builder constraints = new Constraints.Builder(); - - if (UserPreferences.isAllowMobileFeedRefresh()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - return constraints.build(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java new file mode 100644 index 000000000..d1a273d4e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.core.util.download; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.FeedUpdateWorker; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.storage.preferences.UserPreferences; + +import java.util.concurrent.TimeUnit; + +public class FeedUpdateManager { + public static final String WORK_TAG_FEED_UPDATE = "feedUpdate"; + private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; + private static final String WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"; + public static final String EXTRA_FEED_ID = "feed_id"; + public static final String EXTRA_NEXT_PAGE = "next_page"; + private static final String TAG = "AutoUpdateManager"; + + private FeedUpdateManager() { + + } + + /** + * Start / restart periodic auto feed refresh + * @param context Context + */ + public static void restartUpdateAlarm(Context context, boolean replace) { + if (UserPreferences.isAutoUpdateDisabled()) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); + } else { + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder( + FeedUpdateWorker.class, UserPreferences.getUpdateInterval(), TimeUnit.HOURS) + .setConstraints(getConstraints()) + .build(); + WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, + replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest); + } + } + + public static void runOnce(Context context) { + runOnce(context, null, false); + } + + public static void runOnce(Context context, Feed feed) { + runOnce(context, feed, false); + } + + public static void runOnce(Context context, Feed feed, boolean nextPage) { + OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) + .setInitialDelay(0L, TimeUnit.MILLISECONDS) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(WORK_TAG_FEED_UPDATE); + if (feed != null) { + Data.Builder builder = new Data.Builder(); + builder.putLong(EXTRA_FEED_ID, feed.getId()); + builder.putBoolean(EXTRA_NEXT_PAGE, nextPage); + workRequest.setInputData(builder.build()); + } + WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, + ExistingWorkPolicy.REPLACE, workRequest.build()); + } + + public static void runOnceOrAsk(@NonNull Context context) { + Log.d(TAG, "Run auto update immediately in background."); + if (!NetworkUtils.networkAvailable()) { + Log.d(TAG, "Ignoring: No network connection."); + } else if (NetworkUtils.isFeedRefreshAllowed()) { + runOnce(context); + } else { + confirmMobileAllFeedsRefresh(context); + } + } + + private static void confirmMobileAllFeedsRefresh(final Context context) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setTitle(R.string.feed_refresh_title) + .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message) + .setPositiveButton(R.string.confirm_mobile_streaming_button_once, + (dialog, which) -> runOnce(context)) + .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { + UserPreferences.setAllowMobileFeedRefresh(true); + runOnce(context); + }) + .setNegativeButton(R.string.no, null); + builder.show(); + } + + private static Constraints getConstraints() { + Constraints.Builder constraints = new Constraints.Builder(); + + if (UserPreferences.isAllowMobileFeedRefresh()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + return constraints.build(); + } + +} diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 39f62a5d7..f3c0a0a3c 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -25,6 +25,28 @@ <item>heavy</item> </string-array> + <string-array name="feed_refresh_interval_entries"> + <item>@string/feed_refresh_never</item> + <item>@string/feed_every_hour</item> + <item>@string/feed_every_2_hours</item> + <item>@string/feed_every_4_hours</item> + <item>@string/feed_every_8_hours</item> + <item>@string/feed_every_12_hours</item> + <item>@string/feed_every_24_hours</item> + <item>@string/feed_every_72_hours</item> + </string-array> + + <string-array name="feed_refresh_interval_values"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>4</item> + <item>8</item> + <item>12</item> + <item>24</item> + <item>72</item> + </string-array> + <string-array name="globalNewEpisodesActionItems"> <item>@string/feed_new_episodes_action_add_to_inbox</item> <item>@string/feed_new_episodes_action_nothing</item> diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 87046cc0f..90d143d38 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -19,6 +19,7 @@ <item name="notification_gpodnet_sync_error" type="id"/> <item name="notification_gpodnet_sync_autherror" type="id"/> <item name="notification_downloading" type="id"/> + <item name="notification_updating_feeds" type="id"/> <item name="notification_download_report" type="id"/> <item name="notification_auto_download_report" type="id"/> <item name="notification_playing" type="id"/> |