diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2023-05-05 23:09:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-05 23:09:03 +0200 |
commit | 6d7bfef8a5fe8180f13904739996bb2b8de8a0d4 (patch) | |
tree | 84f246b74fe7254678788e9f206d81d1a30ffa5e | |
parent | 4c286931cd2dbd9038022f808f9d8a73ccbb6759 (diff) | |
download | AntennaPod-6d7bfef8a5fe8180f13904739996bb2b8de8a0d4.zip |
Download Service Rewrite (#6420)
78 files changed, 879 insertions, 2477 deletions
diff --git a/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java b/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java index 302301c85..835af1f95 100644 --- a/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java +++ b/app/src/androidTest/java/de/danoeh/antennapod/core/service/download/StubDownloader.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.service.download; import androidx.annotation.NonNull; import androidx.core.util.Consumer; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; public class StubDownloader extends Downloader { @@ -10,9 +10,10 @@ public class StubDownloader extends Downloader { private final long downloadTime; @NonNull - private final Consumer<DownloadStatus> onDownloadComplete; + private final Consumer<DownloadResult> onDownloadComplete; - public StubDownloader(@NonNull DownloadRequest request, long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) { + public StubDownloader(@NonNull DownloadRequest request, long downloadTime, + @NonNull Consumer<DownloadResult> onDownloadComplete) { super(request); this.downloadTime = downloadTime; this.onDownloadComplete = onDownloadComplete; @@ -36,7 +37,7 @@ public class StubDownloader extends Downloader { @NonNull @Override - public DownloadStatus getResult() { + public DownloadResult getResult() { return super.getResult(); } diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index d65289638..a7575862b 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -24,7 +24,6 @@ import junit.framework.AssertionFailedError; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.NavDrawerFragment; @@ -224,21 +223,6 @@ public class EspressoTestUtils { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } - public static void tryKillDownloadService() { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - context.stopService(new Intent(context, DownloadService.class)); - try { - // Android has no reliable way to stop a service instantly. - // Calling stopSelf marks allows the system to destroy the service but the actual call - // to onDestroy takes until the next GC of the system, which we can not influence. - // Try to wait for the service at least a bit. - Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> !DownloadService.isRunning); - } catch (ConditionTimeoutException e) { - e.printStackTrace(); - } - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - } - public static Matcher<View> actionBarOverflow() { return allOf(isDisplayed(), withContentDescription("More options")); } diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java deleted file mode 100644 index 97499b24a..000000000 --- a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java +++ /dev/null @@ -1,217 +0,0 @@ -package de.test.antennapod.service.download; - -import android.content.Context; -import android.content.Intent; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Consumer; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.test.antennapod.EspressoTestUtils; -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionTimeoutException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.TimeUnit; - -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.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.service.download.DownloaderFactory; -import de.danoeh.antennapod.core.service.download.StubDownloader; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; - -import static de.test.antennapod.util.event.DownloadEventListener.withDownloadEventListener; -import static de.test.antennapod.util.event.FeedItemEventListener.withFeedItemEventListener; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * @see HttpDownloaderTest for the test of actual download (and saving the file). - */ -@RunWith(AndroidJUnit4.class) -public class DownloadServiceTest { - private FeedMedia testMedia11 = null; - - private DownloaderFactory origFactory = null; - - @Before - public void setUp() throws Exception { - EspressoTestUtils.clearDatabase(); - EspressoTestUtils.clearPreferences(); - origFactory = DownloadService.getDownloaderFactory(); - Feed testFeed = setUpTestFeeds(); - testMedia11 = testFeed.getItemAtIndex(0).getMedia(); - } - - private Feed setUpTestFeeds() throws Exception { - // To avoid complication in case of test failures, leaving behind orphaned - // media files: add a timestamp so that each test run will have its own directory for media files. - Feed feed = new Feed("url", null, "Test Feed title 1 " + System.currentTimeMillis()); - List<FeedItem> items = new ArrayList<>(); - feed.setItems(items); - FeedItem item1 = new FeedItem(0, "Item 1-1", "Item 1-1", "url", new Date(), FeedItem.NEW, feed); - items.add(item1); - FeedMedia media1 = new FeedMedia(0, item1, 123, 1, 1, "audio/mp3", null, "http://example.com/episode.mp3", false, null, 0, 0); - item1.setMedia(media1); - - DBWriter.setFeedItem(item1).get(); - return feed; - } - - - @After - public void tearDown() throws Exception { - DownloadService.setDownloaderFactory(origFactory); - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - DownloadServiceInterface.get().cancelAll(context); - context.stopService(new Intent(context, DownloadService.class)); - EspressoTestUtils.tryKillDownloadService(); - } - - @Test - public void testEventsGeneratedCaseMediaDownloadSuccess_noEnqueue() throws Exception { - doTestEventsGeneratedCaseMediaDownloadSuccess(false, 1); - } - - @Test - public void testEventsGeneratedCaseMediaDownloadSuccess_withEnqueue() throws Exception { - // enqueue itself generates additional FeedItem event - doTestEventsGeneratedCaseMediaDownloadSuccess(true, 2); - } - - private void doTestEventsGeneratedCaseMediaDownloadSuccess(boolean enqueueDownloaded, - int numEventsExpected) - throws Exception { - // create a stub download that returns successful - // - // OPEN: Ideally, I'd like the download time long enough so that multiple in-progress DownloadEvents - // are generated (to simulate typical download), but it'll make download time quite long (1-2 seconds) - // to do so - DownloadService.setDownloaderFactory(new StubDownloaderFactory(50, DownloadStatus::setSuccessful)); - - UserPreferences.setEnqueueDownloadedEpisodes(enqueueDownloaded); - withFeedItemEventListener(feedItemEventListener -> { - try { - assertEquals(0, feedItemEventListener.getEvents().size()); - assertFalse("The media in test should not yet been downloaded", - DBReader.getFeedMedia(testMedia11.getId()).isDownloaded()); - - DownloadServiceInterface.get() - .download(InstrumentationRegistry.getInstrumentation().getTargetContext(), false, - DownloadRequestCreator.create(testMedia11).build()); - Awaitility.await() - .atMost(5000, TimeUnit.MILLISECONDS) - .until(() -> feedItemEventListener.getEvents().size() >= numEventsExpected); - assertTrue("After media download has completed, FeedMedia object in db should indicate so.", - DBReader.getFeedMedia(testMedia11.getId()).isDownloaded()); - assertEquals("The FeedItem should have been " + (enqueueDownloaded ? "" : "not ") + "enqueued", - enqueueDownloaded, - DBReader.getQueueIDList().contains(testMedia11.getItem().getId())); - } catch (ConditionTimeoutException cte) { - fail("The expected FeedItemEvent (for media download complete) has not been posted. " - + cte.getMessage()); - } - }); - } - - @Test - public void testCancelDownload_UndoEnqueue_Normal() throws Exception { - doTestCancelDownload_UndoEnqueue(false); - } - - @Test - public void testCancelDownload_UndoEnqueue_AlreadyInQueue() throws Exception { - doTestCancelDownload_UndoEnqueue(true); - } - - private void doTestCancelDownload_UndoEnqueue(boolean itemAlreadyInQueue) throws Exception { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - // let download take longer to ensure the test can cancel the download in time - DownloadService.setDownloaderFactory( - new StubDownloaderFactory(30000, DownloadStatus::setSuccessful)); - UserPreferences.setEnqueueDownloadedEpisodes(true); - UserPreferences.setEnableAutodownload(false); - - final long item1Id = testMedia11.getItem().getId(); - if (itemAlreadyInQueue) { - // simulate item already in queue condition - DBWriter.addQueueItem(context, false, item1Id).get(); - assertTrue(DBReader.getQueueIDList().contains(item1Id)); - } else { - assertFalse(DBReader.getQueueIDList().contains(item1Id)); - } - - withFeedItemEventListener(feedItemEventListener -> { - DownloadServiceInterface.get() - .download(InstrumentationRegistry.getInstrumentation().getTargetContext(), false, - DownloadRequestCreator.create(testMedia11).build()); - withDownloadEventListener(downloadEventListener -> - Awaitility.await("download is actually running") - .atMost(5000, TimeUnit.MILLISECONDS) - .until(() -> downloadEventListener.getLatestEvent() != null - && downloadEventListener.getLatestEvent().update.mediaIds.length > 0 - && downloadEventListener.getLatestEvent().update.mediaIds[0] == testMedia11.getId())); - - if (itemAlreadyInQueue) { - assertEquals("download service receives the request - no event is expected before cancel is issued", - 0, feedItemEventListener.getEvents().size()); - } else { - Awaitility.await("item enqueue event") - .atMost(2000, TimeUnit.MILLISECONDS) - .until(() -> feedItemEventListener.getEvents().size() >= 1); - } - DownloadServiceInterface.get().cancel(context, testMedia11.getDownload_url()); - final int totalNumEventsExpected = itemAlreadyInQueue ? 1 : 3; - Awaitility.await("item dequeue event + download termination event") - .atMost(2000, TimeUnit.MILLISECONDS) - .until(() -> feedItemEventListener.getEvents().size() >= totalNumEventsExpected); - assertFalse("The download should have been canceled", - DBReader.getFeedMedia(testMedia11.getId()).isDownloaded()); - if (itemAlreadyInQueue) { - assertTrue("The FeedItem should still be in the queue after the download is cancelled." - + " It's there before download.", - DBReader.getQueueIDList().contains(item1Id)); - } else { - assertFalse("The FeedItem should not be in the queue after the download is cancelled.", - DBReader.getQueueIDList().contains(item1Id)); - } - }); - } - - private static class StubDownloaderFactory implements DownloaderFactory { - private final long downloadTime; - - @NonNull - private final Consumer<DownloadStatus> onDownloadComplete; - - StubDownloaderFactory(long downloadTime, @NonNull Consumer<DownloadStatus> onDownloadComplete) { - this.downloadTime = downloadTime; - this.onDownloadComplete = onDownloadComplete; - } - - @Nullable - @Override - public Downloader create(@NonNull DownloadRequest request) { - return new StubDownloader(request, downloadTime, onDownloadComplete); - } - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java index f276434f6..76cba4706 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -10,7 +10,7 @@ import java.io.IOException; import de.danoeh.antennapod.model.feed.FeedFile; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.model.download.DownloadError; @@ -80,10 +80,9 @@ public class HttpDownloaderTest { DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt(), username, password, deleteOnFail, null, false); Downloader downloader = new HttpDownloader(request); downloader.call(); - DownloadStatus status = downloader.getResult(); + DownloadResult status = downloader.getResult(); assertNotNull(status); assertEquals(expectedResult, status.isSuccessful()); - assertTrue(status.isDone()); // the file should not exist if the download has failed and deleteExisting was true assertTrue(!deleteExisting || new File(feedFile.getFile_url()).exists() == expectedResult); return downloader; @@ -127,10 +126,8 @@ public class HttpDownloaderTest { } catch (InterruptedException e) { e.printStackTrace(); } - DownloadStatus result = downloader.getResult(); - assertTrue(result.isDone()); + DownloadResult result = downloader.getResult(); assertFalse(result.isSuccessful()); - assertTrue(result.isCancelled()); assertFalse(new File(feedFile.getFile_url()).exists()); } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 5940d511b..1f387b24b 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -27,8 +27,6 @@ import java.util.Arrays; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; -import static androidx.test.espresso.action.ViewActions.replaceText; import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.action.ViewActions.swipeDown; import static androidx.test.espresso.action.ViewActions.swipeUp; @@ -37,13 +35,11 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isChecked; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; -import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.waitForView; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertTrue; @@ -224,40 +220,6 @@ public class PreferencesTest { } @Test - public void testSetSequentialDownload() { - clickPreference(R.string.downloads_pref); - clickPreference(R.string.pref_parallel_downloads_title); - onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); - onView(withClassName(endsWith("EditText"))).perform(replaceText("1")); - onView(withText(android.R.string.ok)).perform(click()); - Awaitility.await().atMost(1000, MILLISECONDS) - .until(() -> UserPreferences.getParallelDownloads() == 1); - } - - @Test - public void testSetParallelDownloads() { - clickPreference(R.string.downloads_pref); - clickPreference(R.string.pref_parallel_downloads_title); - onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); - onView(withClassName(endsWith("EditText"))).perform(replaceText("10")); - onView(withClassName(endsWith("EditText"))).perform(closeSoftKeyboard()); - onView(withText(android.R.string.ok)).perform(click()); - Awaitility.await().atMost(1000, MILLISECONDS) - .until(() -> UserPreferences.getParallelDownloads() == 10); - } - - @Test - public void testSetParallelDownloadsInvalidInput() { - clickPreference(R.string.downloads_pref); - clickPreference(R.string.pref_parallel_downloads_title); - onView(isRoot()).perform(waitForView(withClassName(endsWith("EditText")), 1000)); - onView(withClassName(endsWith("EditText"))).perform(replaceText("0")); - onView(withClassName(endsWith("EditText"))).check(matches(withText(""))); - onView(withClassName(endsWith("EditText"))).perform(replaceText("100")); - onView(withClassName(endsWith("EditText"))).check(matches(withText(""))); - } - - @Test public void testSetEpisodeCache() { String[] entries = res.getStringArray(R.array.episode_cache_size_entries); String[] values = res.getStringArray(R.array.episode_cache_size_values); diff --git a/app/src/androidTest/java/de/test/antennapod/util/event/DownloadEventListener.java b/app/src/androidTest/java/de/test/antennapod/util/event/DownloadEventListener.java deleted file mode 100644 index d322c1cbf..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/event/DownloadEventListener.java +++ /dev/null @@ -1,45 +0,0 @@ -package de.test.antennapod.util.event; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import de.danoeh.antennapod.core.event.DownloadEvent; -import io.reactivex.functions.Consumer; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; - -import java.util.ArrayList; -import java.util.List; - -/** - * Test helper to listen to {@link DownloadEvent} and handle them accordingly. - */ -public class DownloadEventListener { - private final List<DownloadEvent> events = new ArrayList<>(); - - /** - * Provides an listener subscribing to {@link DownloadEvent} that the callers can use. - * Note: it uses RxJava's version of {@link Consumer} because it allows exceptions to be thrown. - */ - public static void withDownloadEventListener(@NonNull Consumer<DownloadEventListener> consumer) throws Exception { - DownloadEventListener feedItemEventListener = new DownloadEventListener(); - try { - EventBus.getDefault().register(feedItemEventListener); - consumer.accept(feedItemEventListener); - } finally { - EventBus.getDefault().unregister(feedItemEventListener); - } - } - - @Subscribe - public void onEvent(DownloadEvent event) { - events.add(event); - } - - @Nullable - public DownloadEvent getLatestEvent() { - if (events.size() == 0) { - return null; - } - return events.get(events.size() - 1); - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c779db26..95b0c1614 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -130,17 +130,6 @@ </activity> <activity - android:name=".activity.DownloadAuthenticationActivity" - android:theme="@style/Theme.AntennaPod.Dark.Translucent" - android:exported="false" - android:launchMode="singleInstance"> - <intent-filter> - <action android:name="de.danoeh.antennapod.intents.DOWNLOAD_AUTH_ACTIVITY" /> - <category android:name="android.intent.category.DEFAULT" /> - </intent-filter> - </activity> - - <activity android:name=".activity.PreferenceActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:exported="false" diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java deleted file mode 100644 index 176c3c990..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/DownloadAuthenticationActivity.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.danoeh.antennapod.activity; - -import android.os.Bundle; -import android.text.TextUtils; -import androidx.appcompat.app.AppCompatActivity; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.ThemeSwitcher; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.ui.appstartintent.DownloadAuthenticationActivityStarter; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - - -/** - * Shows a username and a password text field. - * The activity MUST be started with the ARG_DOWNlOAD_REQUEST argument set to a non-null value. - */ -public class DownloadAuthenticationActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeSwitcher.getTranslucentTheme(this)); - super.onCreate(savedInstanceState); - - DownloadRequest request = getIntent().getParcelableExtra( - DownloadAuthenticationActivityStarter.EXTRA_DOWNLOAD_REQUEST); - - new AuthenticationDialog(this, R.string.authentication_label, true, "", "") { - @Override - protected void onConfirmed(String username, String password) { - Completable.fromAction( - () -> { - request.setUsername(username); - request.setPassword(password); - - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - long mediaId = request.getFeedfileId(); - FeedMedia media = DBReader.getFeedMedia(mediaId); - if (media != null) { - FeedPreferences preferences = media.getItem().getFeed().getPreferences(); - if (TextUtils.isEmpty(preferences.getPassword()) - || TextUtils.isEmpty(preferences.getUsername())) { - preferences.setUsername(username); - preferences.setPassword(password); - DBWriter.setFeedPreferences(preferences); - } - } - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { - DownloadServiceInterface.get() - .download(DownloadAuthenticationActivity.this, false, request); - finish(); - }); - } - - @Override - protected void onCancelled() { - finish(); - } - }.show(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index bd467076a..a55bfade3 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -39,12 +39,14 @@ import de.danoeh.antennapod.core.preferences.ThemeSwitcher; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.AllEpisodesFragment; import de.danoeh.antennapod.fragment.AudioPlayerFragment; import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; +import de.danoeh.antennapod.fragment.DownloadLogFragment; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.InboxFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; @@ -53,6 +55,8 @@ import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.playback.cast.CastEnabledActivity; import de.danoeh.antennapod.preferences.PreferenceUpgrader; import de.danoeh.antennapod.storage.preferences.UserPreferences; @@ -66,6 +70,9 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.HashMap; +import java.util.Map; + /** * The activity that is shown when the user launches the app. */ @@ -173,6 +180,39 @@ public class MainActivity extends CastEnabledActivity { } EventBus.getDefault().postSticky(new FeedUpdateRunningEvent(isRefreshingFeeds)); }); + WorkManager.getInstance(this) + .getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG) + .observe(this, workInfos -> { + Map<String, DownloadStatus> updatedEpisodes = new HashMap<>(); + for (WorkInfo workInfo : workInfos) { + String downloadUrl = null; + for (String tag : workInfo.getTags()) { + if (tag.startsWith(DownloadServiceInterface.WORK_TAG_EPISODE_URL)) { + downloadUrl = tag.substring(DownloadServiceInterface.WORK_TAG_EPISODE_URL.length()); + } + } + if (downloadUrl == null) { + continue; + } + int status; + if (workInfo.getState() == WorkInfo.State.RUNNING) { + status = DownloadStatus.STATE_RUNNING; + } else if (workInfo.getState() == WorkInfo.State.ENQUEUED + || workInfo.getState() == WorkInfo.State.BLOCKED) { + status = DownloadStatus.STATE_QUEUED; + } else { + status = DownloadStatus.STATE_COMPLETED; + } + int progress = workInfo.getProgress().getInt(DownloadServiceInterface.WORK_DATA_PROGRESS, -1); + if (progress == -1 && status != DownloadStatus.STATE_COMPLETED) { + status = DownloadStatus.STATE_QUEUED; + progress = 0; + } + updatedEpisodes.put(downloadUrl, new DownloadStatus(status, progress)); + } + DownloadServiceInterface.get().setCurrentDownloads(updatedEpisodes); + EventBus.getDefault().postSticky(new EpisodeDownloadEvent(updatedEpisodes)); + }); } @Override @@ -531,9 +571,9 @@ public class MainActivity extends CastEnabledActivity { public void onEventMainThread(MessageEvent event) { Log.d(TAG, "onEvent(" + event + ")"); - Snackbar snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_SHORT); + Snackbar snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG); if (event.action != null) { - snackbar.setAction(getString(R.string.undo), v -> event.action.run()); + snackbar.setAction(event.actionText, v -> event.action.accept(this)); } } @@ -570,6 +610,9 @@ public class MainActivity extends CastEnabledActivity { if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false) && drawerLayout != null) { drawerLayout.open(); } + if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) { + new DownloadLogFragment().show(getSupportFragmentManager(), null); + } if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) { FeedUpdateManager.runOnceOrAsk(this); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index f5f3d28f6..3812339bd 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -31,20 +31,20 @@ import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.preferences.ThemeSwitcher; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; import de.danoeh.antennapod.core.feed.FeedUrlNotFoundException; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; import de.danoeh.antennapod.core.util.DownloadErrorLabel; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.storage.DBReader; @@ -307,10 +307,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { error -> Log.e(TAG, Log.getStackTraceString(error))); } - private void checkDownloadResult(@NonNull DownloadStatus status, String destination) { - if (status.isCancelled()) { - return; - } + private void checkDownloadResult(@NonNull DownloadResult status, String destination) { if (status.isSuccessful()) { parseFeed(destination); } else if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { @@ -341,9 +338,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity { ); } - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(EpisodeDownloadEvent event) { handleUpdatedFeedStatus(); } @@ -535,7 +531,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } private void handleUpdatedFeedStatus() { - if (DownloadService.isDownloadingFile(selectedDownloadUrl)) { + if (DownloadServiceInterface.get().isDownloadingEpisode(selectedDownloadUrl)) { viewBinding.subscribeButton.setEnabled(false); viewBinding.subscribeButton.setText(R.string.subscribing_label); } else if (feedInFeedlist()) { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 8df3e3968..0fb6a7e7a 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.adapter; import android.app.Activity; import android.text.format.DateUtils; -import android.text.format.Formatter; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -10,18 +9,13 @@ import android.widget.BaseAdapter; import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -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.service.download.Downloader; +import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; 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.DownloadErrorLabel; import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.view.viewholder.DownloadLogItemViewHolder; @@ -36,24 +30,18 @@ public class DownloadLogAdapter extends BaseAdapter { private static final String TAG = "DownloadLogAdapter"; private final Activity context; - private List<DownloadStatus> downloadLog = new ArrayList<>(); - private List<Downloader> runningDownloads = new ArrayList<>(); + private List<DownloadResult> downloadLog = new ArrayList<>(); public DownloadLogAdapter(Activity context) { super(); this.context = context; } - public void setDownloadLog(List<DownloadStatus> downloadLog) { + public void setDownloadLog(List<DownloadResult> downloadLog) { this.downloadLog = downloadLog; notifyDataSetChanged(); } - public void setRunningDownloads(List<Downloader> runningDownloads) { - this.runningDownloads = runningDownloads; - notifyDataSetChanged(); - } - @Override public View getView(int position, View convertView, ViewGroup parent) { DownloadLogItemViewHolder holder; @@ -63,17 +51,11 @@ public class DownloadLogAdapter extends BaseAdapter { } else { holder = (DownloadLogItemViewHolder) convertView.getTag(); } - - Object item = getItem(position); - if (item instanceof DownloadStatus) { - bind(holder, (DownloadStatus) item, position); - } else if (item instanceof Downloader) { - bind(holder, (Downloader) item, position); - } + bind(holder, getItem(position), position); return holder.itemView; } - private void bind(DownloadLogItemViewHolder holder, DownloadStatus status, int position) { + private void bind(DownloadLogItemViewHolder holder, DownloadResult status, int position) { String statusText = ""; if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { statusText += context.getString(R.string.download_type_feed); @@ -111,8 +93,7 @@ public class DownloadLogAdapter extends BaseAdapter { holder.reason.setVisibility(View.VISIBLE); holder.tapForDetails.setVisibility(View.VISIBLE); - if (newerWasSuccessful(position - runningDownloads.size(), - status.getFeedfileType(), status.getFeedfileId())) { + if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { holder.secondaryActionButton.setVisibility(View.INVISIBLE); holder.secondaryActionButton.setOnClickListener(null); holder.secondaryActionButton.setTag(null); @@ -138,8 +119,7 @@ public class DownloadLogAdapter extends BaseAdapter { Log.e(TAG, "Could not find feed media for feed id: " + status.getFeedfileId()); return; } - DownloadServiceInterface.get() - .download(context, true, DownloadRequestCreator.create(media).build()); + new DownloadActionButton(media.getItem()).onClick(context); ((MainActivity) context).showSnackbarAbovePlayer( R.string.status_downloading_label, Toast.LENGTH_SHORT); }); @@ -148,56 +128,9 @@ public class DownloadLogAdapter extends BaseAdapter { } } - private void bind(DownloadLogItemViewHolder holder, Downloader downloader, int position) { - DownloadRequest request = downloader.getDownloadRequest(); - holder.title.setText(request.getTitle()); - holder.secondaryActionIcon.setImageResource(R.drawable.ic_cancel); - holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); - holder.secondaryActionButton.setVisibility(View.VISIBLE); - holder.secondaryActionButton.setTag(downloader); - holder.secondaryActionButton.setOnClickListener(v -> { - DownloadServiceInterface.get().cancel(context, request.getSource()); - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); - FeedItem feedItem = media.getItem(); - feedItem.disableAutoDownload(); - DBWriter.setFeedItem(feedItem); - } - }); - holder.reason.setVisibility(View.GONE); - holder.tapForDetails.setVisibility(View.GONE); - holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.colorPrimary)); - holder.icon.setText("{fa-arrow-circle-down}"); - holder.icon.setContentDescription(context.getString(R.string.status_downloading_label)); - - boolean percentageWasSet = false; - String status = ""; - if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - status += context.getString(R.string.download_type_feed); - } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - status += context.getString(R.string.download_type_media); - } - status += " · "; - if (request.getSoFar() <= 0) { - status += context.getString(R.string.download_pending); - } else { - status += Formatter.formatShortFileSize(context, request.getSoFar()); - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) { - status += " / " + Formatter.formatShortFileSize(context, request.getSize()); - holder.secondaryActionProgress.setPercentage( - 0.01f * Math.max(1, request.getProgressPercent()), request); - percentageWasSet = true; - } - } - if (!percentageWasSet) { - holder.secondaryActionProgress.setPercentage(0, request); - } - holder.status.setText(status); - } - private boolean newerWasSuccessful(int downloadStatusIndex, int feedTypeId, long id) { for (int i = 0; i < downloadStatusIndex; i++) { - DownloadStatus status = downloadLog.get(i); + DownloadResult status = downloadLog.get(i); if (status.getFeedfileType() == feedTypeId && status.getFeedfileId() == id && status.isSuccessful()) { return true; } @@ -207,15 +140,13 @@ public class DownloadLogAdapter extends BaseAdapter { @Override public int getCount() { - return downloadLog.size() + runningDownloads.size(); + return downloadLog.size(); } @Override - public Object getItem(int position) { - if (position < runningDownloads.size()) { - return runningDownloads.get(position); - } else if (position - runningDownloads.size() < downloadLog.size()) { - return downloadLog.get(position - runningDownloads.size()); + public DownloadResult getItem(int position) { + if (position < downloadLog.size()) { + return downloadLog.get(position); } return null; } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java index 3b54efc03..86a8047a9 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java @@ -2,20 +2,17 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; import android.view.View; -import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UsageStatistics; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.NetworkUtils; public class DownloadActionButton extends ItemActionButton { @@ -50,19 +47,23 @@ public class DownloadActionButton extends ItemActionButton { UsageStatistics.logAction(UsageStatistics.ACTION_DOWNLOAD); - if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) { - DownloadServiceInterface.get() - .download(context, false, DownloadRequestCreator.create(item.getMedia()).build()); - } else if (MobileDownloadHelper.userChoseAddToQueue() && !item.isTagged(FeedItem.TAG_QUEUE)) { - DBWriter.addQueueItem(context, item); - Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); + if (NetworkUtils.isEpisodeDownloadAllowed()) { + DownloadServiceInterface.get().downloadNow(context, item, false); } else { - MobileDownloadHelper.confirmMobileDownload(context, item); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setTitle(R.string.confirm_mobile_download_dialog_title) + .setMessage(R.string.confirm_mobile_download_dialog_message) + .setPositiveButton(R.string.confirm_mobile_download_dialog_download_later, + (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, false)) + .setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time, + (d, w) -> DownloadServiceInterface.get().downloadNow(context, item, true)) + .setNegativeButton(R.string.cancel_label, null); + builder.show(); } } private boolean shouldNotDownload(@NonNull FeedMedia media) { - boolean isDownloading = DownloadService.isDownloadingFile(media.getDownload_url()); + boolean isDownloading = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownload_url()); return isDownloading || media.isDownloaded(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java index e7a95c404..613dd32f0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java @@ -7,10 +7,10 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; import android.view.View; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; public abstract class ItemActionButton { @@ -39,7 +39,7 @@ public abstract class ItemActionButton { return new MarkAsPlayedActionButton(item); } - final boolean isDownloadingMedia = DownloadService.isDownloadingFile(media.getDownload_url()); + final boolean isDownloadingMedia = DownloadServiceInterface.get().isDownloadingEpisode(media.getDownload_url()); if (PlaybackStatus.isCurrentlyPlaying(media)) { return new PauseActionButton(item); } else if (item.getFeed().isLocalFeed()) { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java deleted file mode 100644 index 015f46318..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.danoeh.antennapod.adapter.actionbutton; - -import android.content.Context; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; - -class MobileDownloadHelper { - private static long addToQueueTimestamp; - private static long allowMobileDownloadTimestamp; - private static final int TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000; - - static boolean userChoseAddToQueue() { - return System.currentTimeMillis() - addToQueueTimestamp < TEN_MINUTES_IN_MILLIS; - } - - static boolean userAllowedMobileDownloads() { - return System.currentTimeMillis() - allowMobileDownloadTimestamp < TEN_MINUTES_IN_MILLIS; - } - - static void confirmMobileDownload(final Context context, final FeedItem item) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) - .setTitle(R.string.confirm_mobile_download_dialog_title) - .setMessage(R.string.confirm_mobile_download_dialog_message) - .setPositiveButton(context.getText(R.string.confirm_mobile_download_dialog_enable_temporarily), - (dialog, which) -> downloadFeedItems(context, item)); - if (!DBReader.getQueueIDList().contains(item.getId())) { - builder.setMessage(R.string.confirm_mobile_download_dialog_message_not_in_queue) - .setNeutralButton(R.string.confirm_mobile_download_dialog_only_add_to_queue, - (dialog, which) -> addToQueue(context, item)); - } - builder.show(); - } - - private static void addToQueue(Context context, FeedItem item) { - addToQueueTimestamp = System.currentTimeMillis(); - DBWriter.addQueueItem(context, item); - } - - private static void downloadFeedItems(Context context, FeedItem item) { - allowMobileDownloadTimestamp = System.currentTimeMillis(); - DownloadServiceInterface.get().download(context, true, DownloadRequestCreator.create(item.getMedia()).build()); - } -}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java index 5e28639dd..00936a38a 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/DownloadLogDetailsDialog.java @@ -9,7 +9,7 @@ import androidx.appcompat.app.AlertDialog; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.feed.Feed; @@ -18,7 +18,7 @@ import org.greenrobot.eventbus.EventBus; public class DownloadLogDetailsDialog extends MaterialAlertDialogBuilder { - public DownloadLogDetailsDialog(@NonNull Context context, DownloadStatus status) { + public DownloadLogDetailsDialog(@NonNull Context context, DownloadResult status) { super(context); String url = "unknown"; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index 737975389..ba328adba 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -20,12 +20,12 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton; -import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloadLogEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; @@ -36,6 +36,7 @@ import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.view.EmptyViewHandler; import de.danoeh.antennapod.view.EpisodeItemListRecyclerView; @@ -50,9 +51,10 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Displays all completed downloads and provides a button to delete them. @@ -63,7 +65,7 @@ public class CompletedDownloadsFragment extends Fragment public static final String ARG_SHOW_LOGS = "show_logs"; private static final String KEY_UP_ARROW = "up_arrow"; - private long[] runningDownloads = new long[0]; + private Set<String> runningDownloads = new HashSet<>(); private List<FeedItem> items = new ArrayList<>(); private CompletedDownloadsListAdapter adapter; private EpisodeItemListRecyclerView recyclerView; @@ -217,19 +219,22 @@ public class CompletedDownloadsFragment extends Fragment } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (!Arrays.equals(event.update.mediaIds, runningDownloads)) { - runningDownloads = event.update.mediaIds; + public void onEventMainThread(EpisodeDownloadEvent event) { + Set<String> newRunningDownloads = new HashSet<>(); + for (String url : event.getUrls()) { + if (DownloadServiceInterface.get().isDownloadingEpisode(url)) { + newRunningDownloads.add(url); + } + } + if (!newRunningDownloads.equals(runningDownloads)) { + runningDownloads = newRunningDownloads; loadItems(); return; // Refreshed anyway } - if (event.update.mediaIds.length > 0) { - for (long mediaId : event.update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(items, mediaId); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); } } } @@ -318,17 +323,17 @@ public class CompletedDownloadsFragment extends Fragment List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, new FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder); - List<Long> mediaIds = new ArrayList<>(); + List<String> mediaUrls = new ArrayList<>(); if (runningDownloads == null) { return downloadedItems; } - for (long id : runningDownloads) { - if (FeedItemUtil.indexOfItemWithMediaId(downloadedItems, id) != -1) { + for (String url : runningDownloads) { + if (FeedItemUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) { continue; // Already in list } - mediaIds.add(id); + mediaUrls.add(url); } - List<FeedItem> currentDownloads = DBReader.getFeedItemsWithMedia(mediaIds.toArray(new Long[0])); + List<FeedItem> currentDownloads = DBReader.getFeedItemsWithUrl(mediaUrls); currentDownloads.addAll(downloadedItems); return currentDownloads; }) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index c27d8c058..03913ff12 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -14,15 +14,12 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadLogAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloadLogEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; -import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.databinding.DownloadLogFragmentBinding; import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -30,7 +27,6 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; import java.util.ArrayList; import java.util.List; @@ -42,8 +38,7 @@ public class DownloadLogFragment extends BottomSheetDialogFragment implements AdapterView.OnItemClickListener, MaterialToolbar.OnMenuItemClickListener { private static final String TAG = "DownloadLogFragment"; - private List<DownloadStatus> downloadLog = new ArrayList<>(); - private List<Downloader> runningDownloads = new ArrayList<>(); + private List<DownloadResult> downloadLog = new ArrayList<>(); private DownloadLogAdapter adapter; private Disposable disposable; private DownloadLogFragmentBinding viewBinding; @@ -93,8 +88,8 @@ public class DownloadLogFragment extends BottomSheetDialogFragment @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Object item = adapter.getItem(position); - if (item instanceof DownloadStatus) { - new DownloadLogDetailsDialog(getContext(), (DownloadStatus) item).show(); + if (item instanceof DownloadResult) { + new DownloadLogDetailsDialog(getContext(), (DownloadResult) item).show(); } } @@ -119,14 +114,6 @@ public class DownloadLogFragment extends BottomSheetDialogFragment return false; } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(DownloadEvent event) { - Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - runningDownloads = update.downloaders; - adapter.setRunningDownloads(runningDownloads); - } - private void loadDownloadLog() { if (disposable != null) { disposable.dispose(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index ef784e9ee..00d671d36 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -27,11 +27,10 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; @@ -387,16 +386,11 @@ public abstract class EpisodesListFragment extends Fragment } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - updateToolbar(); - if (update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 61883afe7..f59d5dbd4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -29,8 +29,6 @@ import com.leinardi.android.speeddial.SpeedDialView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; @@ -45,6 +43,7 @@ import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog; import de.danoeh.antennapod.dialog.FeedItemFilterDialog; import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.RenameItemDialog; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FavoritesEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; @@ -57,7 +56,7 @@ import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler; import de.danoeh.antennapod.fragment.swipeactions.SwipeActions; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; @@ -330,16 +329,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - updateToolbar(); - if (update.mediaIds.length > 0 && feed != null) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(feed.getItems(), mediaId); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + if (feed == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(feed.getItems(), downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); } } } @@ -479,7 +476,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private void showErrorDetails() { Maybe.fromCallable( () -> { - List<DownloadStatus> feedDownloadLog = DBReader.getFeedDownloadLog(feedID); + List<DownloadResult> feedDownloadLog = DBReader.getFeedDownloadLog(feedID); if (feedDownloadLog.size() == 0 || feedDownloadLog.get(0).isSuccessful()) { return null; } @@ -490,9 +487,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem .subscribe( downloadStatus -> new DownloadLogDetailsDialog(getContext(), downloadStatus).show(), error -> error.printStackTrace(), - () -> { - ((MainActivity) getActivity()).loadChildFragment(new DownloadLogFragment()); - }); + () -> new DownloadLogFragment().show(getChildFragmentManager(), null)); } private void showFeedInfo() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index df48c9a98..94877811e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -37,10 +37,7 @@ import de.danoeh.antennapod.adapter.actionbutton.PlayActionButton; import de.danoeh.antennapod.adapter.actionbutton.PlayLocalActionButton; import de.danoeh.antennapod.adapter.actionbutton.StreamActionButton; import de.danoeh.antennapod.adapter.actionbutton.VisitWebsiteActionButton; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; @@ -49,8 +46,8 @@ import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.preferences.UsageStatistics; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateFormatter; @@ -63,12 +60,10 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.ArrayUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -import java.util.List; import java.util.Locale; import java.util.Objects; @@ -98,7 +93,6 @@ public class ItemFragment extends Fragment { private long itemId; private FeedItem item; private String webviewData; - private List<Downloader> downloaderList; private ViewGroup root; private ShownotesWebView webvDescription; @@ -309,14 +303,13 @@ public class ItemFragment extends Fragment { private void updateButtons() { progbarDownload.setVisibility(View.GONE); - if (item.hasMedia() && downloaderList != null) { - for (Downloader downloader : downloaderList) { - DownloadRequest request = downloader.getDownloadRequest(); - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && request.getFeedfileId() == item.getMedia().getId()) { - progbarDownload.setVisibility(View.VISIBLE); - progbarDownload.setPercentage(0.01f * Math.max(1, request.getProgressPercent()), request); - } + if (item.hasMedia()) { + if (DownloadServiceInterface.get().isDownloadingEpisode(item.getMedia().getDownload_url())) { + progbarDownload.setVisibility(View.VISIBLE); + progbarDownload.setPercentage(0.01f * Math.max(1, + DownloadServiceInterface.get().getProgress(item.getMedia().getDownload_url())), item); + progbarDownload.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(item.getMedia().getDownload_url())); } } @@ -341,7 +334,7 @@ public class ItemFragment extends Fragment { } else { actionButton1 = new StreamActionButton(item); } - if (DownloadService.isDownloadingFile(media.getDownload_url())) { + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownload_url())) { actionButton2 = new CancelDownloadActionButton(item); } else if (!media.isDownloaded()) { actionButton2 = new DownloadActionButton(item); @@ -386,18 +379,15 @@ public class ItemFragment extends Fragment { } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - downloaderList = update.downloaders; + public void onEventMainThread(EpisodeDownloadEvent event) { if (item == null || item.getMedia() == null) { return; } - long mediaId = item.getMedia().getId(); - if (ArrayUtils.contains(update.mediaIds, mediaId)) { - if (itemsLoaded && getActivity() != null) { - updateButtons(); - } + if (!event.getUrls().contains(item.getMedia().getDownload_url())) { + return; + } + if (itemsLoaded && getActivity() != null) { + updateButtons(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 6681df4c1..a1aa7226d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -32,8 +32,6 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; @@ -41,6 +39,7 @@ import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; @@ -186,16 +185,14 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with DownloadEvent"); - DownloaderUpdate update = event.update; - refreshToolbarState(); - if (recyclerAdapter != null && update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(queue, mediaId); - if (pos >= 0) { - recyclerAdapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + if (queue == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); + if (pos >= 0) { + recyclerAdapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index d2aa35549..b85e34e7d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -28,9 +28,8 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; @@ -295,15 +294,14 @@ public class SearchFragment extends Fragment { } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - DownloaderUpdate update = event.update; - if (adapter != null && update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(results, mediaId); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + if (results == null) { + return; + } + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(results, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index 490c79d47..a6daec2e7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -36,7 +36,6 @@ import java.util.Locale; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.NavDrawerData; @@ -362,12 +361,6 @@ public class SubscriptionFragment extends Fragment loadSubscriptions(); } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - refreshToolbarState(); - } - @Override public void onEndSelectMode() { speedDialView.close(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java index e076d35b6..a14bfcd16 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java @@ -6,13 +6,10 @@ import androidx.annotation.PluralsRes; import com.google.android.material.snackbar.Snackbar; -import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -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.DBWriter; import de.danoeh.antennapod.core.util.LongList; @@ -93,14 +90,12 @@ public class EpisodeMultiSelectActionHandler { private void downloadChecked(List<FeedItem> items) { // download the check episodes in the same order as they are currently displayed - List<DownloadRequest> requests = new ArrayList<>(); for (FeedItem episode : items) { if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) { - requests.add(DownloadRequestCreator.create(episode.getMedia()).build()); + DownloadServiceInterface.get().download(activity, episode); } } - DownloadServiceInterface.get().download(activity, true, requests.toArray(new DownloadRequest[0])); - showMessage(R.plurals.downloading_batch_label, requests.size()); + showMessage(R.plurals.downloading_batch_label, items.size()); } private void deleteChecked(List<FeedItem> items) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java index c486089fc..7b0c3efdf 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.fragment.preferences; import android.content.SharedPreferences; -import android.content.res.Resources; import android.os.Bundle; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; @@ -43,7 +42,6 @@ public class DownloadsPreferencesFragment extends PreferenceFragmentCompat @Override public void onResume() { super.onResume(); - setParallelDownloadsText(UserPreferences.getParallelDownloads()); setDataFolderText(); } @@ -52,12 +50,6 @@ public class DownloadsPreferencesFragment extends PreferenceFragmentCompat ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); return true; }); - findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setOnPreferenceChangeListener((preference, o) -> { - if (o instanceof Integer) { - setParallelDownloadsText((Integer) o); - } - return true; - }); // validate and set correct value: number of downloads between 1 and 50 (inclusive) findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { ProxyDialog dialog = new ProxyDialog(getActivity()); @@ -73,12 +65,6 @@ public class DownloadsPreferencesFragment extends PreferenceFragmentCompat }); } - private void setParallelDownloadsText(int downloads) { - final Resources res = getActivity().getResources(); - String s = res.getString(R.string.parallel_downloads, downloads); - findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); - } - private void setDataFolderText() { File f = UserPreferences.getDataFolder(null); if (f != null) { diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/NumberPickerPreference.java b/app/src/main/java/de/danoeh/antennapod/preferences/NumberPickerPreference.java deleted file mode 100644 index a15c6d6b2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/NumberPickerPreference.java +++ /dev/null @@ -1,108 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.content.Context; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import androidx.preference.Preference; -import android.text.InputFilter; -import android.util.AttributeSet; -import android.view.View; -import android.view.WindowManager; -import android.widget.EditText; - -import de.danoeh.antennapod.R; - -public class NumberPickerPreference extends Preference { - private Context context; - private int defaultValue = 0; - private int minValue = 0; - private int maxValue = Integer.MAX_VALUE; - - public NumberPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(context, attrs); - } - - public NumberPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs); - } - - public NumberPickerPreference(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public NumberPickerPreference(Context context) { - super(context); - this.context = context; - } - - private void init(Context context, AttributeSet attrs) { - this.context = context; - - for (int i = 0; i < attrs.getAttributeCount(); i++) { - String name = attrs.getAttributeName(i); - String value = attrs.getAttributeValue(i); - switch (name) { - case "defaultValue": - defaultValue = Integer.parseInt(value); - break; - case "minValue": - minValue = Integer.parseInt(value); - break; - case "maxValue": - maxValue = Integer.parseInt(value); - break; - } - } - } - - @Override - protected void onClick() { - super.onClick(); - - View view = View.inflate(context, R.layout.numberpicker, null); - EditText number = view.findViewById(R.id.number); - number.setText(getSharedPreferences().getString(getKey(), ""+defaultValue)); - number.setFilters(new InputFilter[]{(source, start, end, dest, dstart, dend) -> { - try { - String newVal = dest.toString().substring(0, dstart) + dest.toString().substring(dend); - newVal = newVal.substring(0, dstart) + source.toString() + newVal.substring(dstart); - int input = Integer.parseInt(newVal); - if (input >= minValue && input <= maxValue) { - return null; - } - } catch (NumberFormatException nfe) { - nfe.printStackTrace(); - } - return ""; - }}); - - AlertDialog dialog = new MaterialAlertDialogBuilder(context) - .setTitle(getTitle()) - .setView(view) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - try { - String numberString = number.getText().toString(); - int value = Integer.parseInt(numberString); - - if (value < minValue || value > maxValue) { - return; - } - - getSharedPreferences().edit().putString(getKey(), "" + value).apply(); - - if (getOnPreferenceChangeListener() != null) { - getOnPreferenceChangeListener().onPreferenceChange(this, value); - } - } catch (NumberFormatException e) { - // Do not set value - } - }) - .create(); - dialog.show(); - dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java index 7e2c855e9..298bd05c5 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java @@ -13,11 +13,10 @@ import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; @@ -32,6 +31,7 @@ import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -41,7 +41,7 @@ public class EpisodesSurpriseSection extends HomeSection { private static int seed = 0; private HorizontalItemListAdapter listAdapter; private Disposable disposable; - private List<FeedItem> episodes; + private List<FeedItem> episodes = new ArrayList<>(); @Nullable @Override @@ -103,9 +103,6 @@ public class EpisodesSurpriseSection extends HomeSection { @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if (episodes == null) { - return; - } for (int i = 0, size = event.items.size(); i < size; i++) { FeedItem item = event.items.get(i); int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); @@ -118,15 +115,11 @@ public class EpisodesSurpriseSection extends HomeSection { } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with DownloadEvent"); - DownloaderUpdate update = event.update; - if (listAdapter != null && update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java index 8d343a16a..fe9d15dc1 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java @@ -14,11 +14,10 @@ import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; @@ -35,6 +34,7 @@ import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -42,7 +42,7 @@ public class InboxSection extends HomeSection { public static final String TAG = "InboxSection"; private static final int NUM_EPISODES = 2; private EpisodeItemListAdapter adapter; - private List<FeedItem> items; + private List<FeedItem> items = new ArrayList<>(); private Disposable disposable; @Nullable @@ -97,15 +97,11 @@ public class InboxSection extends HomeSection { } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with DownloadEvent"); - DownloaderUpdate update = event.update; - if (adapter != null && update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(items, mediaId); - if (pos >= 0) { - adapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(items, downloadUrl); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java index 070d56ed6..33335f2bb 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java @@ -13,11 +13,10 @@ import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.QueueEvent; @@ -33,6 +32,7 @@ import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.ArrayList; import java.util.List; public class QueueSection extends HomeSection { @@ -40,7 +40,7 @@ public class QueueSection extends HomeSection { private static final int NUM_EPISODES = 8; private HorizontalItemListAdapter listAdapter; private Disposable disposable; - private List<FeedItem> queue; + private List<FeedItem> queue = new ArrayList<>(); @Nullable @Override @@ -102,15 +102,11 @@ public class QueueSection extends HomeSection { } @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEventMainThread(DownloadEvent event) { - Log.d(TAG, "onEventMainThread() called with DownloadEvent"); - DownloaderUpdate update = event.update; - if (listAdapter != null && update.mediaIds.length > 0) { - for (long mediaId : update.mediaIds) { - int pos = FeedItemUtil.indexOfItemWithMediaId(queue, mediaId); - if (pos >= 0) { - listAdapter.notifyItemChangedCompat(pos); - } + public void onEventMainThread(EpisodeDownloadEvent event) { + for (String downloadUrl : event.getUrls()) { + int pos = FeedItemUtil.indexOfItemWithDownloadUrl(queue, downloadUrl); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index fd3ec9299..03df844b1 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -21,8 +21,6 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.core.util.download.MediaSizeLoader; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; @@ -31,6 +29,7 @@ import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -117,6 +116,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { bind(item.getMedia()); } else { secondaryActionProgress.setPercentage(0, item); + secondaryActionProgress.setIndeterminate(false); isVideo.setVisibility(View.GONE); progressBar.setVisibility(View.GONE); duration.setVisibility(View.GONE); @@ -145,14 +145,17 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); } - if (DownloadService.isDownloadingFile(media.getDownload_url())) { - final DownloadRequest downloadRequest = DownloadService.findRequest(media.getDownload_url()); - float percent = 0.01f * downloadRequest.getProgressPercent(); + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownload_url())) { + float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownload_url()); secondaryActionProgress.setPercentage(Math.max(percent, 0.01f), item); + secondaryActionProgress.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(media.getDownload_url())); } else if (media.isDownloaded()) { secondaryActionProgress.setPercentage(1, item); // Do not animate 100% -> 0% + secondaryActionProgress.setIndeterminate(false); } else { secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% + secondaryActionProgress.setIndeterminate(false); } duration.setText(Converter.getDurationStringLong(media.getDuration())); @@ -210,6 +213,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { pubDate.setText("████"); duration.setText("████"); secondaryActionProgress.setPercentage(0, null); + secondaryActionProgress.setIndeterminate(false); progressBar.setVisibility(View.GONE); position.setVisibility(View.GONE); dragHandle.setVisibility(View.GONE); diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java index 05240b371..f809de175 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java @@ -14,13 +14,12 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.ui.common.CircularProgressBar; import de.danoeh.antennapod.ui.common.SquareImageView; import de.danoeh.antennapod.ui.common.ThemeUtils; @@ -85,14 +84,17 @@ public class HorizontalItemViewHolder extends RecyclerView.ViewHolder { setProgressBar(false, 0); } - if (DownloadService.isDownloadingFile(media.getDownload_url())) { - final DownloadRequest downloadRequest = DownloadService.findRequest(media.getDownload_url()); - float percent = 0.01f * downloadRequest.getProgressPercent(); + if (DownloadServiceInterface.get().isDownloadingEpisode(media.getDownload_url())) { + float percent = 0.01f * DownloadServiceInterface.get().getProgress(media.getDownload_url()); circularProgressBar.setPercentage(Math.max(percent, 0.01f), item); + circularProgressBar.setIndeterminate( + DownloadServiceInterface.get().isEpisodeQueued(media.getDownload_url())); } else if (media.isDownloaded()) { circularProgressBar.setPercentage(1, item); // Do not animate 100% -> 0% + circularProgressBar.setIndeterminate(false); } else { circularProgressBar.setPercentage(0, item); // Animate X% -> 0% + circularProgressBar.setIndeterminate(false); } } } @@ -107,6 +109,7 @@ public class HorizontalItemViewHolder extends RecyclerView.ViewHolder { date.setText("███"); secondaryActionIcon.setImageDrawable(null); circularProgressBar.setPercentage(0, null); + circularProgressBar.setIndeterminate(false); setProgressBar(true, 50); } diff --git a/app/src/main/res/layout/numberpicker.xml b/app/src/main/res/layout/numberpicker.xml deleted file mode 100644 index d493f2e6c..000000000 --- a/app/src/main/res/layout/numberpicker.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:padding="16dp"> - - <EditText - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:inputType="numberDecimal" - android:ems="10" - android:selectAllOnFocus="true" - android:id="@+id/number" /> - -</LinearLayout> diff --git a/app/src/main/res/xml/preferences_downloads.xml b/app/src/main/res/xml/preferences_downloads.xml index 865748d20..18fc7df11 100644 --- a/app/src/main/res/xml/preferences_downloads.xml +++ b/app/src/main/res/xml/preferences_downloads.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:numberpicker="http://schemas.android.com/apk/de.danoeh.antennapod" xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> <Preference @@ -56,12 +55,6 @@ android:key="prefMobileUpdateTypes" android:summary="@string/pref_mobileUpdate_sum" android:title="@string/pref_mobileUpdate_title"/> - <de.danoeh.antennapod.preferences.NumberPickerPreference - android:defaultValue="4" - numberpicker:minValue="1" - numberpicker:maxValue="50" - android:key="prefParallelDownloads" - android:title="@string/pref_parallel_downloads_title"/> <Preference android:key="prefProxy" android:summary="@string/pref_proxy_sum" diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 6f5508f27..e186a856f 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -16,10 +16,6 @@ android:icon="@mipmap/ic_launcher" android:supportsRtl="true"> - <service - android:name=".service.download.DownloadService" - android:enabled="true" /> - <service android:name=".service.playback.PlaybackService" android:label="@string/app_name" android:enabled="true" diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java deleted file mode 100644 index efd53ab9d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -import de.danoeh.antennapod.core.service.download.Downloader; - -public class DownloadEvent { - - public final DownloaderUpdate update; - - private DownloadEvent(DownloaderUpdate downloader) { - this.update = downloader; - } - - public static DownloadEvent refresh(List<Downloader> list) { - list = new ArrayList<>(list); - DownloaderUpdate update = new DownloaderUpdate(list); - return new DownloadEvent(update); - } - - @NonNull - @Override - public String toString() { - return "DownloadEvent{" + - "update=" + update + - '}'; - } - - public boolean hasChangedFeedUpdateStatus(boolean oldStatus) { - return oldStatus != update.feedIds.length > 0; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java deleted file mode 100644 index 1cab7e0f0..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.NonNull; - -import java.util.Arrays; -import java.util.List; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.util.LongList; - -public class DownloaderUpdate { - - /* Downloaders that are currently running */ - @NonNull - public final List<Downloader> downloaders; - - /** - * IDs of feeds that are currently being downloaded - * Often used to show some progress wheel in the action bar - */ - public final long[] feedIds; - - /** - * IDs of feed media that are currently being downloaded - * Can be used to show and update download progress bars - */ - public final long[] mediaIds; - - DownloaderUpdate(@NonNull List<Downloader> downloaders) { - this.downloaders = downloaders; - LongList feedIds1 = new LongList(); - LongList mediaIds1 = new LongList(); - for(Downloader d1 : downloaders) { - int type = d1.getDownloadRequest().getFeedfileType(); - long id = d1.getDownloadRequest().getFeedfileId(); - if(type == Feed.FEEDFILETYPE_FEED) { - feedIds1.add(id); - } else if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - mediaIds1.add(id); - } - } - - this.feedIds = feedIds1.toArray(); - this.mediaIds = mediaIds1.toArray(); - } - - @NonNull - @Override - public String toString() { - return "DownloaderUpdate{" + - "downloaders=" + downloaders + - ", feedIds=" + Arrays.toString(feedIds) + - ", mediaIds=" + Arrays.toString(mediaIds) + - '}'; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index d4d948b2a..03881ee4f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -28,7 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.documentfile.provider.DocumentFile; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.util.FastDocumentFile; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -246,8 +246,8 @@ public class LocalFeedUpdater { } private static void reportError(Feed feed, String reasonDetailed) { - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.ERROR_IO_ERROR, false, reasonDetailed, true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, reasonDetailed); DBWriter.addDownloadStatus(status); DBWriter.setFeedLastUpdateFailed(feed.getId(), true); } @@ -256,8 +256,7 @@ public class LocalFeedUpdater { * Reports a successful download status. */ private static void reportSuccess(Feed feed) { - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.SUCCESS, true, null, true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), DownloadError.SUCCESS, true, null); DBWriter.addDownloadStatus(status); DBWriter.setFeedLastUpdateFailed(feed.getId(), false); } @@ -266,21 +265,21 @@ public class LocalFeedUpdater { * Answers if reporting success is needed for the given feed. */ private static boolean mustReportDownloadSuccessful(Feed feed) { - List<DownloadStatus> downloadStatuses = DBReader.getFeedDownloadLog(feed.getId()); + List<DownloadResult> downloadResults = DBReader.getFeedDownloadLog(feed.getId()); - if (downloadStatuses.isEmpty()) { + if (downloadResults.isEmpty()) { // report success if never reported before return true; } - Collections.sort(downloadStatuses, (downloadStatus1, downloadStatus2) -> + Collections.sort(downloadResults, (downloadStatus1, downloadStatus2) -> downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())); - DownloadStatus lastDownloadStatus = downloadStatuses.get(downloadStatuses.size() - 1); + DownloadResult lastDownloadResult = downloadResults.get(downloadResults.size() - 1); // report success if the last update was not successful // (avoid logging success again if the last update was ok) - return !lastDownloadStatus.isSuccessful(); + return !lastDownloadResult.isSuccessful(); } @FunctionalInterface diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index 8d9f046e2..5f59f0c41 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -20,12 +20,13 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; import de.danoeh.antennapod.core.service.download.handler.FeedSyncTask; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; @@ -80,6 +81,7 @@ public class FeedUpdateWorker extends Worker { refreshFeeds(toUpdate, true); } notificationManager.cancel(R.id.notification_updating_feeds); + DBTasks.autodownloadUndownloadedItems(getApplicationContext()); return Result.success(); } @@ -115,8 +117,8 @@ public class FeedUpdateWorker extends Worker { } } catch (Exception e) { DBWriter.setFeedLastUpdateFailed(feed.getId(), true); - DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), - DownloadError.ERROR_IO_ERROR, false, e.getMessage(), true); + DownloadResult status = new DownloadResult(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, e.getMessage()); DBWriter.addDownloadStatus(status); } toUpdate.remove(0); @@ -144,7 +146,7 @@ public class FeedUpdateWorker extends Worker { downloader.call(); if (!downloader.getResult().isSuccessful()) { - if (downloader.getResult().isCancelled()) { + if (downloader.cancelled) { return; } DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); @@ -165,7 +167,7 @@ public class FeedUpdateWorker extends Worker { return; // No download logs for new subscriptions } // we create a 'successful' download log if the feed's last refresh failed - List<DownloadStatus> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); + List<DownloadResult> log = DBReader.getFeedDownloadLog(request.getFeedfileId()); if (log.size() > 0 && !log.get(0).isSuccessful()) { DBWriter.addDownloadStatus(feedSyncTask.getDownloadStatus()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java index d6a4b8378..5ca904ff6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequestCreator.java @@ -21,9 +21,6 @@ public class DownloadRequestCreator { public static DownloadRequest.Builder create(Feed feed) { File dest = new File(getFeedfilePath(), getFeedfileName(feed)); - if (!isFilenameAvailable(dest.toString()) && !feed.isLocalFeed()) { - dest = findUnusedFile(dest); - } Log.d(TAG, "Requesting download of url " + feed.getDownload_url()); String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; @@ -45,7 +42,7 @@ public class DownloadRequestCreator { dest = new File(getMediafilePath(media), getMediafilename(media)); } - if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) { + if (dest.exists() && !partiallyDownloadedFileExists) { dest = findUnusedFile(dest); } Log.d(TAG, "Requesting download of url " + media.getDownload_url()); @@ -72,7 +69,7 @@ public class DownloadRequestCreator { + FilenameUtils.getExtension(dest.getName()); Log.d(TAG, "Testing filename " + newName); newDest = new File(dest.getParent(), newName); - if (!newDest.exists() && isFilenameAvailable(newDest.toString())) { + if (!newDest.exists()) { Log.d(TAG, "File doesn't exist yet. Using " + newName); break; } @@ -80,19 +77,6 @@ public class DownloadRequestCreator { return newDest; } - /** - * Returns true if a filename is available and false if it has already been - * taken by another requested download. - */ - private static boolean isFilenameAvailable(String path) { - for (Downloader downloader : DownloadService.downloads) { - if (downloader.request.getDestination().equals(path)) { - return false; - } - } - return true; - } - private static String getFeedfilePath() { return UserPreferences.getDataFolder(FEED_DOWNLOADPATH).toString() + "/"; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java deleted file mode 100644 index 9c238137e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ /dev/null @@ -1,536 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.IBinder; -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.app.ServiceCompat; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.service.download.handler.FailedDownloadHandler; -import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; -import de.danoeh.antennapod.core.service.download.handler.PostDownloaderTask; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithmFactory; -import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.apache.commons.io.FileUtils; -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent. - * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUESTS field of - * the intent. - * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the - * type of the feedfile. - */ -public class DownloadService extends Service { - private static final String TAG = "DownloadService"; - private static final int SCHED_EX_POOL_SIZE = 1; - public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.core.service.cancelDownload"; - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAll"; - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - public static final String EXTRA_REQUESTS = "downloadRequests"; - public static final String EXTRA_INITIATED_BY_USER = "initiatedByUser"; - public static final String EXTRA_CLEANUP_MEDIA = "cleanupMedia"; - - public static boolean isRunning = false; - - // Can be modified from another thread while iterating. Both possible race conditions are not critical: - // Remove while iterating: We think it is still downloading and don't start a new download with the same file. - // Add while iterating: We think it is not downloading and might start a second download with the same file. - static final List<Downloader> downloads = Collections.synchronizedList(new CopyOnWriteArrayList<>()); - private final ExecutorService downloadHandleExecutor; - private final ExecutorService downloadEnqueueExecutor; - - private final List<DownloadStatus> reportQueue = new ArrayList<>(); - private final List<DownloadRequest> failedRequestsForReport = new ArrayList<>(); - private DownloadServiceNotification notificationManager; - private NotificationUpdater notificationUpdater; - private ScheduledFuture<?> notificationUpdaterFuture; - private ScheduledFuture<?> downloadPostFuture; - private final ScheduledThreadPoolExecutor notificationUpdateExecutor; - private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); - private ConnectionStateMonitor connectionMonitor; - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - public DownloadService() { - - downloadEnqueueExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "EnqueueThread"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); - downloadHandleExecutor = Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), - r -> { - Thread t = new Thread(r, "DownloadThread"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - notificationUpdateExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - r -> { - Thread t = new Thread(r, "NotificationUpdateExecutor"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }, (r, executor) -> Log.w(TAG, "SchedEx rejected submission of new task") - ); - } - - @Override - public void onCreate() { - Log.d(TAG, "Service started"); - isRunning = true; - notificationManager = new DownloadServiceNotification(this); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - - connectionMonitor = new ConnectionStateMonitor(); - connectionMonitor.enable(getApplicationContext()); - } - - public static boolean isDownloadingFile(String downloadUrl) { - if (!isRunning) { - return false; - } - for (Downloader downloader : downloads) { - if (downloader.request.getSource().equals(downloadUrl) && !downloader.cancelled) { - return true; - } - } - return false; - } - - public static DownloadRequest findRequest(String downloadUrl) { - for (Downloader downloader : downloads) { - if (downloader.request.getSource().equals(downloadUrl)) { - return downloader.request; - } - } - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && intent.hasExtra(EXTRA_REQUESTS)) { - Notification notification = notificationManager.updateNotifications(downloads); - startForeground(R.id.notification_downloading, notification); - NotificationManagerCompat.from(this).cancel(R.id.notification_download_report); - NotificationManagerCompat.from(this).cancel(R.id.notification_auto_download_report); - setupNotificationUpdaterIfNecessary(); - downloadEnqueueExecutor.execute(() -> onDownloadQueued(intent)); - } else if (downloads.size() == 0) { - shutdown(); - } else { - Log.d(TAG, "onStartCommand: Unknown intent"); - } - return Service.START_NOT_STICKY; - } - - @Override - public void onDestroy() { - Log.d(TAG, "Service shutting down"); - isRunning = false; - - boolean showAutoDownloadReport = UserPreferences.showAutoDownloadReport(); - if (UserPreferences.showDownloadReport() || showAutoDownloadReport) { - notificationManager.updateReport(reportQueue, showAutoDownloadReport, failedRequestsForReport); - reportQueue.clear(); - failedRequestsForReport.clear(); - } - - unregisterReceiver(cancelDownloadReceiver); - connectionMonitor.disable(getApplicationContext()); - - EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); - cancelNotificationUpdater(); - downloadEnqueueExecutor.shutdownNow(); - downloadHandleExecutor.shutdownNow(); - notificationUpdateExecutor.shutdownNow(); - if (downloadPostFuture != null) { - downloadPostFuture.cancel(true); - } - downloads.clear(); - - // start auto download in case anything new has shown up - DBTasks.autodownloadUndownloadedItems(getApplicationContext()); - } - - /** - * This method MUST NOT, in any case, throw an exception. - * Otherwise, it hangs up the refresh thread pool. - */ - private void performDownload(Downloader downloader) { - try { - downloader.call(); - } catch (Exception e) { - e.printStackTrace(); - } - try { - if (downloader.getResult().isSuccessful()) { - handleSuccessfulDownload(downloader); - } else { - handleFailedDownload(downloader); - } - } catch (Exception e) { - e.printStackTrace(); - } - downloadEnqueueExecutor.submit(() -> { - downloads.remove(downloader); - stopServiceIfEverythingDone(); - }); - } - - private void handleSuccessfulDownload(Downloader downloader) { - DownloadRequest request = downloader.getDownloadRequest(); - DownloadStatus status = downloader.getResult(); - final int type = status.getFeedfileType(); - - if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "Handling completed FeedMedia Download"); - MediaDownloadedHandler handler = new MediaDownloadedHandler(DownloadService.this, status, request); - handler.run(); - saveDownloadStatus(handler.getUpdatedStatus(), downloader.getDownloadRequest()); - } - } - - private void handleFailedDownload(Downloader downloader) { - DownloadStatus status = downloader.getResult(); - final int type = status.getFeedfileType(); - - if (!status.isCancelled()) { - if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - notificationManager.postAuthenticationNotification(downloader.getDownloadRequest()); - } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && Integer.parseInt(status.getReasonDetailed()) == 416) { - - Log.d(TAG, "Requested invalid range, restarting download from the beginning"); - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - DownloadServiceInterface.get().download(this, false, downloader.getDownloadRequest()); - } else { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status, downloader.getDownloadRequest()); - new FailedDownloadHandler(downloader.getDownloadRequest()).run(); - - if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedItem item = getFeedItemFromId(status.getFeedfileId()); - if (item == null) { - return; - } - item.increaseFailedAutoDownloadAttempts(System.currentTimeMillis()); - DBWriter.setFeedItem(item); - // to make lists reload the failed item, we fake an item update - EventBus.getDefault().post(FeedItemEvent.updated(item)); - } - } - } else { - // if FeedMedia download has been canceled, fake FeedItem update - // so that lists reload that it - if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedItem item = getFeedItemFromId(status.getFeedfileId()); - if (item == null) { - return; - } - EventBus.getDefault().post(FeedItemEvent.updated(item)); - } - } - } - - private final BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "cancelDownloadReceiver: " + intent.getAction()); - if (!isRunning) { - return; - } - if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { - String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - if (url == null) { - throw new IllegalArgumentException("ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - } - downloadEnqueueExecutor.execute(() -> { - doCancel(url); - postDownloaders(); - stopServiceIfEverythingDone(); - }); - } else if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { - downloadEnqueueExecutor.execute(() -> { - for (Downloader d : downloads) { - d.cancel(); - } - Log.d(TAG, "Cancelled all downloads"); - postDownloaders(); - stopServiceIfEverythingDone(); - }); - } - } - }; - - private void doCancel(String url) { - Log.d(TAG, "Cancelling download with url " + url); - for (Downloader downloader : downloads) { - if (downloader.cancelled || !downloader.getDownloadRequest().getSource().equals(url)) { - continue; - } - downloader.cancel(); - DownloadRequest request = downloader.getDownloadRequest(); - FeedItem item = getFeedItemFromId(request.getFeedfileId()); - if (item != null) { - EventBus.getDefault().post(FeedItemEvent.updated(item)); - // undo enqueue upon cancel - if (request.isMediaEnqueued()) { - Log.v(TAG, "Undoing enqueue upon cancelling download"); - DBWriter.removeQueueItem(getApplicationContext(), false, item); - } - } - } - } - - private void onDownloadQueued(Intent intent) { - List<DownloadRequest> requests = intent.getParcelableArrayListExtra(EXTRA_REQUESTS); - if (requests == null) { - throw new IllegalArgumentException("ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); - } - Log.d(TAG, "Received enqueue request. #requests=" + requests.size()); - - if (intent.getBooleanExtra(EXTRA_CLEANUP_MEDIA, false)) { - EpisodeCleanupAlgorithmFactory.build().makeRoomForEpisodes(getApplicationContext(), requests.size()); - } - - for (DownloadRequest request : requests) { - addNewRequest(request); - } - postDownloaders(); - stopServiceIfEverythingDone(); - - // Add to-download items to the queue before actual download completed - // so that the resulting queue order is the same as when download is clicked - enqueueFeedItems(requests); - } - - private void enqueueFeedItems(@NonNull List<DownloadRequest> requests) { - List<FeedItem> feedItems = new ArrayList<>(); - for (DownloadRequest request : requests) { - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - long mediaId = request.getFeedfileId(); - FeedMedia media = DBReader.getFeedMedia(mediaId); - if (media == null) { - Log.w(TAG, "enqueueFeedItems() : FeedFile Id " + mediaId + " is not found. ignore it."); - continue; - } - feedItems.add(media.getItem()); - } - } - List<FeedItem> actuallyEnqueued = Collections.emptyList(); - try { - actuallyEnqueued = DBTasks.enqueueFeedItemsToDownload(getApplicationContext(), feedItems); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - - for (DownloadRequest request : requests) { - if (request.getFeedfileType() != FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - continue; - } - final long mediaId = request.getFeedfileId(); - for (FeedItem item : actuallyEnqueued) { - if (item.getMedia() != null && item.getMedia().getId() == mediaId) { - request.setMediaEnqueued(true); - } - } - } - } - - private void addNewRequest(@NonNull DownloadRequest request) { - if (isDownloadingFile(request.getSource())) { - Log.d(TAG, "Skipped enqueueing request. Already running."); - return; - } else if (downloadHandleExecutor.isShutdown()) { - Log.d(TAG, "Skipped enqueueing request. Service is already shutting down."); - return; - } - Log.d(TAG, "Add new request: " + request.getSource()); - writeFileUrl(request); - Downloader downloader = downloaderFactory.create(request); - if (downloader != null) { - downloads.add(downloader); - downloadHandleExecutor.submit(() -> performDownload(downloader)); - } - } - - @VisibleForTesting - public static DownloaderFactory getDownloaderFactory() { - return downloaderFactory; - } - - // public scope rather than package private, - // because androidTest put classes in the non-standard de.test.antennapod hierarchy - @VisibleForTesting - public static void setDownloaderFactory(DownloaderFactory downloaderFactory) { - DownloadService.downloaderFactory = downloaderFactory; - } - - /** - * Adds a new DownloadStatus object to the list of completed downloads and - * saves it in the database - * - * @param status the download that is going to be saved - */ - private void saveDownloadStatus(@NonNull DownloadStatus status, @NonNull DownloadRequest request) { - reportQueue.add(status); - if (!status.isSuccessful() && !status.isCancelled()) { - failedRequestsForReport.add(request); - } - DBWriter.addDownloadStatus(status); - } - - /** - * Check if there's something else to download, otherwise stop. - */ - private void stopServiceIfEverythingDone() { - Log.d(TAG, downloads.size() + " downloads left"); - if (downloads.size() <= 0) { - Log.d(TAG, "Attempting shutdown"); - shutdown(); - } - } - - @Nullable - private FeedItem getFeedItemFromId(long id) { - FeedMedia media = DBReader.getFeedMedia(id); - if (media != null) { - return media.getItem(); - } else { - return null; - } - } - - /** - * Creates the destination file and writes FeedMedia File_url directly after starting download - * to make it possible to resume download after the service was killed by the system. - */ - private void writeFileUrl(DownloadRequest request) { - if (request.getFeedfileType() != FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - return; - } - - File dest = new File(request.getDestination()); - if (!dest.exists()) { - try { - dest.createNewFile(); - } catch (IOException e) { - Log.e(TAG, "Unable to create file"); - } - } - - if (dest.exists()) { - Log.d(TAG, "Writing file url"); - FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); - if (media == null) { - Log.d(TAG, "No media"); - return; - } - media.setFile_url(request.getDestination()); - try { - DBWriter.setFeedMedia(media).get(); - } catch (InterruptedException e) { - Log.e(TAG, "writeFileUrl was interrupted"); - } catch (ExecutionException e) { - Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage()); - } - } - } - - /** - * Schedules the notification updater task if it hasn't been scheduled yet. - */ - private void setupNotificationUpdaterIfNecessary() { - if (notificationUpdater == null) { - Log.d(TAG, "Setting up notification updater"); - notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = notificationUpdateExecutor - .scheduleAtFixedRate(notificationUpdater, 1, 1, TimeUnit.SECONDS); - } - } - - private void cancelNotificationUpdater() { - boolean result = false; - if (notificationUpdaterFuture != null) { - result = notificationUpdaterFuture.cancel(true); - } - notificationUpdater = null; - notificationUpdaterFuture = null; - Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); - } - - private class NotificationUpdater implements Runnable { - public void run() { - Notification n = notificationManager.updateNotifications(downloads); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(R.id.notification_downloading, n); - } - } - } - - private void postDownloaders() { - new PostDownloaderTask(downloads).run(); - - if (downloadPostFuture == null) { - downloadPostFuture = notificationUpdateExecutor.scheduleAtFixedRate( - new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS); - } - } - - private void shutdown() { - // If the service was run for a very short time, the system may delay closing - // the notification. Set the notification text now so that a misleading message - // is not left on the notification. - if (notificationUpdater != null) { - notificationUpdater.run(); - } - cancelNotificationUpdater(); - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); - stopSelf(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java index 976d8255f..87cbeda84 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java @@ -1,74 +1,72 @@ package de.danoeh.antennapod.core.service.download; import android.content.Context; -import android.content.Intent; -import androidx.core.content.ContextCompat; -import com.google.android.exoplayer2.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.WorkManager; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.storage.preferences.UserPreferences; -import java.util.ArrayList; - -import static de.danoeh.antennapod.core.service.download.DownloadService.isDownloadingFile; +import java.util.concurrent.TimeUnit; public class DownloadServiceInterfaceImpl extends DownloadServiceInterface { - private static final String TAG = "DownloadServiceInterface"; - - public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { - Intent intent = makeDownloadIntent(context, cleanupMedia, requests); - if (intent != null) { - ContextCompat.startForegroundService(context, intent); + public void downloadNow(Context context, FeedItem item, boolean ignoreConstraints) { + OneTimeWorkRequest.Builder workRequest = getRequest(context, item); + workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST); + if (ignoreConstraints) { + workRequest.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()); + } else { + workRequest.setConstraints(getConstraints()); } + WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownload_url(), + ExistingWorkPolicy.KEEP, workRequest.build()); } - public Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests) { - ArrayList<DownloadRequest> requestsToSend = new ArrayList<>(); - for (DownloadRequest request : requests) { - if (!isDownloadingFile(request.getSource())) { - requestsToSend.add(request); - } - } - if (requestsToSend.isEmpty()) { - return null; - } else if (requestsToSend.size() > 100) { - if (BuildConfig.DEBUG) { - throw new IllegalArgumentException("Android silently drops intent payloads that are too large"); - } else { - Log.d(TAG, "Too many download requests. Dropping some to avoid Android dropping all."); - requestsToSend = new ArrayList<>(requestsToSend.subList(0, 100)); - } - } + public void download(Context context, FeedItem item) { + OneTimeWorkRequest.Builder workRequest = getRequest(context, item); + workRequest.setConstraints(getConstraints()); + WorkManager.getInstance(context).enqueueUniqueWork(item.getMedia().getDownload_url(), + ExistingWorkPolicy.KEEP, workRequest.build()); + } - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putParcelableArrayListExtra(DownloadService.EXTRA_REQUESTS, requestsToSend); - if (cleanupMedia) { - launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, true); + private static OneTimeWorkRequest.Builder getRequest(Context context, FeedItem item) { + OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(EpisodeDownloadWorker.class) + .setInitialDelay(0L, TimeUnit.MILLISECONDS) + .addTag(DownloadServiceInterface.WORK_TAG) + .addTag(DownloadServiceInterface.WORK_TAG_EPISODE_URL + item.getMedia().getDownload_url()); + Data.Builder builder = new Data.Builder(); + builder.putLong(WORK_DATA_MEDIA_ID, item.getMedia().getId()); + if (!item.isTagged(FeedItem.TAG_QUEUE) && UserPreferences.enqueueDownloadedEpisodes()) { + DBWriter.addQueueItem(context, false, item.getId()); + builder.putBoolean(WORK_DATA_WAS_QUEUED, true); } - return launchIntent; + workRequest.setInputData(builder.build()); + return workRequest; } - public void refreshAllFeeds(Context context, boolean initiatedByUser) { - FeedUpdateManager.runOnce(context); + private static Constraints getConstraints() { + Constraints.Builder constraints = new Constraints.Builder(); + if (UserPreferences.isAllowMobileEpisodeDownload()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + return constraints.build(); } + @Override public void cancel(Context context, String url) { - if (!DownloadService.isRunning) { - return; - } - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); - cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, url); - cancelIntent.setPackage(context.getPackageName()); - context.sendBroadcast(cancelIntent); + WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG_EPISODE_URL + url); } + @Override public void cancelAll(Context context) { - if (!DownloadService.isRunning) { - return; - } - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); - cancelIntent.setPackage(context.getPackageName()); - context.sendBroadcast(cancelIntent); + WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java deleted file mode 100644 index b9846c06c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ /dev/null @@ -1,306 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.util.Log; -import androidx.core.app.NotificationCompat; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.util.DownloadErrorLabel; -import de.danoeh.antennapod.model.download.DownloadStatus; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.ui.appstartintent.DownloadAuthenticationActivityStarter; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; - -import java.util.List; - -public class DownloadServiceNotification { - private static final String TAG = "DownloadSvcNotification"; - - private final Context context; - private NotificationCompat.Builder notificationCompatBuilder; - - public DownloadServiceNotification(Context context) { - this.context = context; - setupNotificationBuilders(); - } - - private void setupNotificationBuilders() { - notificationCompatBuilder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) - .setOngoing(false) - .setWhen(0) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setContentIntent(getNotificationContentIntent(context)) - .setSmallIcon(R.drawable.ic_notification_sync) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * after setupNotificationBuilders. - */ - public Notification updateNotifications(List<Downloader> downloads) { - if (notificationCompatBuilder == null) { - return null; - } - - String contentTitle; - if (typeIsOnly(downloads, Feed.FEEDFILETYPE_FEED)) { - contentTitle = context.getString(R.string.download_notification_title_feeds); - } else if (typeIsOnly(downloads, FeedMedia.FEEDFILETYPE_FEEDMEDIA)) { - contentTitle = context.getString(R.string.download_notification_title_episodes); - } else { - contentTitle = context.getString(R.string.download_notification_title); - } - - int numDownloads = getNumberOfRunningDownloads(downloads); - String contentText = context.getString(R.string.completing); - String bigText = context.getString(R.string.completing); - notificationCompatBuilder.clearActions(); - if (numDownloads > 0) { - bigText = compileNotificationString(downloads); - if (numDownloads == 1) { - contentText = bigText; - } else { - contentText = context.getResources().getQuantityString(R.plurals.downloads_left, - numDownloads, numDownloads); - } - - Intent cancelDownloadsIntent = new Intent(DownloadService.ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadsIntent.setPackage(context.getPackageName()); - PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(context, - R.id.pending_intent_download_cancel_all, cancelDownloadsIntent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - notificationCompatBuilder.addAction(new NotificationCompat.Action( - R.drawable.ic_notification_cancel, context.getString(R.string.cancel_label), cancelPendingIntent)); - } - - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(contentText); - notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); - return notificationCompatBuilder.build(); - } - - private int getNumberOfRunningDownloads(List<Downloader> downloads) { - int running = 0; - for (Downloader downloader : downloads) { - if (!downloader.cancelled && !downloader.isFinished()) { - running++; - } - } - return running; - } - - private boolean typeIsOnly(List<Downloader> downloads, int feedFileType) { - for (Downloader downloader : downloads) { - if (downloader.cancelled) { - continue; - } - DownloadRequest request = downloader.getDownloadRequest(); - if (request.getFeedfileType() != feedFileType) { - return false; - } - } - return true; - } - - private static String compileNotificationString(List<Downloader> downloads) { - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < downloads.size(); i++) { - Downloader downloader = downloads.get(i); - if (downloader.cancelled) { - continue; - } - stringBuilder.append("• "); - DownloadRequest request = downloader.getDownloadRequest(); - if (request.getTitle() != null) { - stringBuilder.append(request.getTitle()); - } else { - stringBuilder.append(request.getSource()); - } - if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - stringBuilder.append(" (").append(request.getProgressPercent()).append("%)"); - } else if (request.getSource().startsWith(Feed.PREFIX_LOCAL_FOLDER)) { - stringBuilder.append(" (").append(request.getSoFar()) - .append("/").append(request.getSize()).append(")"); - } - if (i != downloads.size() - 1) { - stringBuilder.append("\n"); - } - } - return stringBuilder.toString(); - } - - private static String createAutoDownloadNotificationContent(List<DownloadStatus> statuses) { - int length = statuses.size(); - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) { - sb.append("• ").append(statuses.get(i).getTitle()); - if (i != length - 1) { - sb.append("\n"); - } - } - - return sb.toString(); - } - - private String createFailedDownloadNotificationContent(List<DownloadStatus> statuses) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < statuses.size(); i++) { - if (statuses.get(i) == null || statuses.get(i).isSuccessful()) { - continue; - } - sb.append("• ").append(statuses.get(i).getTitle()); - if (statuses.get(i).getReason() != null) { - sb.append(": ").append(context.getString(DownloadErrorLabel.from(statuses.get(i).getReason()))); - } - if (i != statuses.size() - 1) { - sb.append("\n"); - } - } - - return sb.toString(); - } - - /** - * Creates a notification at the end of the service lifecycle to notify the - * user about the number of completed downloads. A report will only be - * created if there is at least one failed download excluding images - */ - public void updateReport(List<DownloadStatus> reportQueue, boolean showAutoDownloadReport, - List<DownloadRequest> failedRequests) { - // check if report should be created - boolean createReport = false; - int failedDownloads = 0; - - // a download report is created if at least one download has failed - // (excluding failed image downloads) - for (DownloadStatus status : reportQueue) { - if (status == null || status.isCancelled()) { - continue; - } - if (status.isSuccessful()) { - createReport |= showAutoDownloadReport && !status.isInitiatedByUser() - && status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA; - } else { - failedDownloads++; - createReport = true; - } - } - - if (!createReport) { - Log.d(TAG, "No report is created"); - return; - } - Log.d(TAG, "Creating report"); - if (failedDownloads == 0) { - createAutoDownloadReportNotification(reportQueue); - } else { - createDownloadFailedNotification(reportQueue, failedRequests); - } - Log.d(TAG, "Download report notification was posted"); - } - - private void createAutoDownloadReportNotification(List<DownloadStatus> reportQueue) { - PendingIntent intent = getAutoDownloadReportNotificationContentIntent(context); - String content = createAutoDownloadNotificationContent(reportQueue); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, - NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD); - builder.setTicker(context.getString(R.string.auto_download_report_title)) - .setContentTitle(context.getString(R.string.auto_download_report_title)) - .setContentText(content) - .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) - .setSmallIcon(R.drawable.ic_notification_new) - .setContentIntent(intent) - .setAutoCancel(true) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(R.id.notification_auto_download_report, builder.build()); - } - - private void createDownloadFailedNotification(List<DownloadStatus> reportQueue, - List<DownloadRequest> failedRequests) { - Intent retryIntent = DownloadServiceInterface.get().makeDownloadIntent(context, - false, failedRequests.toArray(new DownloadRequest[0])); - PendingIntent retryPendingIntent = null; - if (retryIntent != null && Build.VERSION.SDK_INT >= 26) { - retryPendingIntent = PendingIntent.getForegroundService(context, R.id.pending_intent_download_service_retry, - retryIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else if (retryIntent != null) { - retryPendingIntent = PendingIntent.getService(context, - R.id.pending_intent_download_service_retry, retryIntent, - PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - PendingIntent intent = getReportNotificationContentIntent(context); - String content = createFailedDownloadNotificationContent(reportQueue); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, - NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); - builder.setTicker(context.getString(R.string.download_report_title)) - .setContentTitle(context.getString(R.string.download_report_title)) - .setContentText(content) - .setStyle(new NotificationCompat.BigTextStyle().bigText(content)) - .setSmallIcon(R.drawable.ic_notification_sync_error) - .setContentIntent(intent) - .setAutoCancel(true); - if (retryPendingIntent != null) { - builder.addAction(new NotificationCompat.Action( - R.drawable.ic_notification_sync, context.getString(R.string.retry_label), retryPendingIntent)); - } - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(R.id.notification_download_report, builder.build()); - } - - public void postAuthenticationNotification(final DownloadRequest downloadRequest) { - final String resourceTitle = (downloadRequest.getTitle() != null) ? - downloadRequest.getTitle() : downloadRequest.getSource(); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_USER_ACTION); - builder.setTicker(context.getText(R.string.authentication_notification_title)) - .setContentTitle(context.getText(R.string.authentication_notification_title)) - .setContentText(context.getText(R.string.authentication_notification_msg)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getText(R.string.authentication_notification_msg) - + ": " + resourceTitle)) - .setSmallIcon(R.drawable.ic_notification_key) - .setAutoCancel(true) - .setContentIntent(new DownloadAuthenticationActivityStarter( - context, downloadRequest.getFeedfileId(), downloadRequest).getPendingIntent()); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), builder.build()); - } - - public PendingIntent getReportNotificationContentIntent(Context context) { - Intent intent = new MainActivityStarter(context) - .withFragmentLoaded("DownloadsFragment") - .withFragmentArgs("show_logs", true) - .getIntent(); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - public PendingIntent getAutoDownloadReportNotificationContentIntent(Context context) { - Intent intent = new MainActivityStarter(context).withFragmentLoaded("QueueFragment").getIntent(); - return PendingIntent.getActivity(context, R.id.pending_intent_download_service_autodownload_report, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - public PendingIntent getNotificationContentIntent(Context context) { - Intent intent = new MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent(); - return PendingIntent.getActivity(context, - R.id.pending_intent_download_service_notification, intent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java index f7f5e8e9c..35247509d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -9,7 +9,7 @@ import java.util.concurrent.Callable; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; /** @@ -25,15 +25,15 @@ public abstract class Downloader implements Callable<Downloader> { @NonNull final DownloadRequest request; @NonNull - final DownloadStatus result; + final DownloadResult result; Downloader(@NonNull DownloadRequest request) { super(); this.request = request; this.request.setStatusMsg(R.string.download_pending); this.cancelled = false; - this.result = new DownloadStatus(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), - false, cancelled, false, null, new Date(), null, request.isInitiatedByUser()); + this.result = new DownloadResult(0, request.getTitle(), request.getFeedfileId(), request.getFeedfileType(), + false, null, new Date(), null); } protected abstract void download(); @@ -63,7 +63,7 @@ public abstract class Downloader implements Callable<Downloader> { } @NonNull - public DownloadStatus getResult() { + public DownloadResult getResult() { return result; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java new file mode 100644 index 000000000..c428bc861 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java @@ -0,0 +1,265 @@ +package de.danoeh.antennapod.core.service.download; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import de.danoeh.antennapod.core.ClientConfigurator; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.model.download.DownloadError; +import de.danoeh.antennapod.model.download.DownloadResult; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import org.apache.commons.io.FileUtils; +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class EpisodeDownloadWorker extends Worker { + private static final String TAG = "EpisodeDownloadWorker"; + private static final Map<String, Integer> notificationProgress = new HashMap<>(); + + private Downloader downloader = null; + + public EpisodeDownloadWorker(@NonNull Context context, @NonNull WorkerParameters params) { + super(context, params); + } + + @Override + @NonNull + public Result doWork() { + ClientConfigurator.initialize(getApplicationContext()); + long mediaId = getInputData().getLong(DownloadServiceInterface.WORK_DATA_MEDIA_ID, 0); + FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media == null) { + return Result.failure(); + } + + DownloadRequest request = DownloadRequestCreator.create(media).build(); + Thread progressUpdaterThread = new Thread() { + @Override + public void run() { + while (!isInterrupted()) { + try { + Thread.sleep(1000); + notificationProgress.put(media.getEpisodeTitle(), request.getProgressPercent()); + setProgressAsync( + new Data.Builder() + .putInt(DownloadServiceInterface.WORK_DATA_PROGRESS, request.getProgressPercent()) + .build()) + .get(); + sendProgressNotification(); + } catch (InterruptedException | ExecutionException e) { + return; + } + } + } + }; + progressUpdaterThread.start(); + final Result result = performDownload(media, request); + progressUpdaterThread.interrupt(); + try { + progressUpdaterThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + notificationProgress.remove(media.getEpisodeTitle()); + if (notificationProgress.isEmpty()) { + NotificationManager nm = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(R.id.notification_downloading); + } + Log.d(TAG, "Worker for " + media.getDownload_url() + " returned."); + return result; + } + + @Override + public void onStopped() { + super.onStopped(); + if (downloader != null) { + downloader.cancel(); + } + } + + private Result performDownload(FeedMedia media, DownloadRequest request) { + File dest = new File(request.getDestination()); + if (!dest.exists()) { + try { + dest.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "Unable to create file"); + } + } + + if (dest.exists()) { + media.setFile_url(request.getDestination()); + try { + DBWriter.setFeedMedia(media).get(); + } catch (Exception e) { + Log.e(TAG, "ExecutionException in writeFileUrl: " + e.getMessage()); + } + } + + downloader = new DefaultDownloaderFactory().create(request); + if (downloader == null) { + Log.d(TAG, "Unable to create downloader"); + return Result.failure(); + } + + try { + downloader.call(); + } catch (Exception e) { + DBWriter.addDownloadStatus(downloader.getResult()); + if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { + sendMessage(request.getTitle(), false); + } else { + sendErrorNotification(); + } + return Result.failure(); + } + + if (downloader.cancelled) { + if (getInputData().getBoolean(DownloadServiceInterface.WORK_DATA_WAS_QUEUED, false)) { + try { + DBWriter.removeQueueItem(getApplicationContext(), false, media.getItem()).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + return Result.success(); + } + + DownloadResult status = downloader.getResult(); + if (status.isSuccessful()) { + MediaDownloadedHandler handler = new MediaDownloadedHandler( + getApplicationContext(), downloader.getResult(), request); + handler.run(); + DBWriter.addDownloadStatus(handler.getUpdatedStatus()); + return Result.success(); + } + + if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR + && Integer.parseInt(status.getReasonDetailed()) == 416) { + Log.d(TAG, "Requested invalid range, restarting download from the beginning"); + FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); + sendMessage(request.getTitle(), true); + return retry3times(); + } + + Log.e(TAG, "Download failed"); + DBWriter.addDownloadStatus(status); + if (status.getReason() == DownloadError.ERROR_FORBIDDEN + || status.getReason() == DownloadError.ERROR_NOT_FOUND + || status.getReason() == DownloadError.ERROR_UNAUTHORIZED + || status.getReason() == DownloadError.ERROR_IO_BLOCKED) { + // Fail fast, these are probably unrecoverable + if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { + sendMessage(request.getTitle(), false); + } else { + sendErrorNotification(); + } + return Result.failure(); + } + sendMessage(request.getTitle(), true); + return retry3times(); + } + + private Result retry3times() { + if (getRunAttemptCount() < 2) { + return Result.retry(); + } else { + sendErrorNotification(); + return Result.failure(); + } + } + + private void sendMessage(String episodeTitle, boolean retrying) { + if (episodeTitle.length() > 20) { + episodeTitle = episodeTitle.substring(0, 19) + "…"; + } + EventBus.getDefault().post(new MessageEvent( + getApplicationContext().getString( + retrying ? R.string.download_error_retrying : R.string.download_error_not_retrying, + episodeTitle), (ctx) -> new MainActivityStarter(ctx).withDownloadLogsOpen().start(), + getApplicationContext().getString(R.string.download_error_details))); + } + + private PendingIntent getDownloadLogsIntent(Context context) { + Intent intent = new MainActivityStarter(context).withDownloadLogsOpen().getIntent(); + return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + + private PendingIntent getDownloadsIntent(Context context) { + Intent intent = new MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent(); + return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); + } + + private void sendErrorNotification() { + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), + NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR); + builder.setTicker(getApplicationContext().getString(R.string.download_report_title)) + .setContentTitle(getApplicationContext().getString(R.string.download_report_title)) + .setContentText(getApplicationContext().getString(R.string.download_error_tap_for_details)) + .setSmallIcon(R.drawable.ic_notification_sync_error) + .setContentIntent(getDownloadLogsIntent(getApplicationContext())) + .setAutoCancel(true); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + NotificationManager nm = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(R.id.notification_download_report, builder.build()); + } + + private void sendProgressNotification() { + StringBuilder bigTextB = new StringBuilder(); + Map<String, Integer> progressCopy = new HashMap<>(notificationProgress); + for (Map.Entry<String, Integer> entry : progressCopy.entrySet()) { + bigTextB.append(String.format(Locale.getDefault(), "%s (%d%%)\n", entry.getKey(), entry.getValue())); + } + String bigText = bigTextB.toString().trim(); + String contentText; + if (notificationProgress.size() == 1) { + contentText = bigText; + } else { + contentText = getApplicationContext().getResources().getQuantityString(R.plurals.downloads_left, + notificationProgress.size(), notificationProgress.size()); + } + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), + NotificationUtils.CHANNEL_ID_DOWNLOADING); + builder.setTicker(getApplicationContext().getString(R.string.download_notification_title_episodes)) + .setContentTitle(getApplicationContext().getString(R.string.download_notification_title_episodes)) + .setContentText(contentText) + .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) + .setContentIntent(getDownloadsIntent(getApplicationContext())) + .setAutoCancel(false) + .setOngoing(true) + .setWhen(0) + .setOnlyAlertOnce(true) + .setShowWhen(false) + .setSmallIcon(R.drawable.ic_notification_sync) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + NotificationManager nm = (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(R.id.notification_downloading, builder.build()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index a0a0615cb..949f9966b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -5,7 +5,7 @@ import android.text.TextUtils; import android.util.Log; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import okhttp3.CacheControl; import okhttp3.internal.http.StatusLine; @@ -149,12 +149,12 @@ public class HttpDownloader extends Downloader { request.setSize(responseBody.contentLength() + request.getSoFar()); Log.d(TAG, "Size is " + request.getSize()); if (request.getSize() < 0) { - request.setSize(DownloadStatus.SIZE_UNKNOWN); + request.setSize(DownloadResult.SIZE_UNKNOWN); } long freeSpace = StorageUtils.getFreeSpaceAvailable(); Log.d(TAG, "Free space is " + freeSpace); - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSize() > freeSpace) { + if (request.getSize() != DownloadResult.SIZE_UNKNOWN && request.getSize() > freeSpace) { onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); return; } @@ -175,7 +175,7 @@ public class HttpDownloader extends Downloader { } else { // check if size specified in the response header is the same as the size of the // written file. This check cannot be made if compression was used - if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN + if (!isGzip && request.getSize() != DownloadResult.SIZE_UNKNOWN && request.getSoFar() != request.getSize()) { onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " + request.getSoFar() + " does not equal expected size " + request.getSize()); @@ -267,7 +267,8 @@ public class HttpDownloader extends Downloader { } else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { error = DownloadError.ERROR_FORBIDDEN; details = String.valueOf(response.code()); - } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) { + } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND + || response.code() == HttpURLConnection.HTTP_GONE) { error = DownloadError.ERROR_NOT_FOUND; details = String.valueOf(response.code()); } else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java deleted file mode 100644 index 750255958..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/LocalFeedStubDownloader.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import androidx.annotation.NonNull; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; - -/** - * This does not actually download, but it keeps track of a local feed's refresh state. - */ -public class LocalFeedStubDownloader extends Downloader { - - public LocalFeedStubDownloader(@NonNull DownloadRequest request) { - super(request); - } - - @Override - protected void download() { - } -}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java deleted file mode 100644 index 937f051ec..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import android.util.Log; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.storage.DBWriter; - -/** - * Handles failed downloads. - * <p/> - * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - * <p/> - * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. - */ -public class FailedDownloadHandler implements Runnable { - private static final String TAG = "FailedDownloadHandler"; - private final DownloadRequest request; - - public FailedDownloadHandler(DownloadRequest request) { - this.request = request; - } - - @Override - public void run() { - if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); - } else if (request.isDeleteOnFailure()) { - Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index 1118b93cd..5da250e15 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -8,7 +8,7 @@ import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.parser.feed.FeedHandler; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; @@ -25,15 +25,15 @@ import java.util.concurrent.Callable; public class FeedParserTask implements Callable<FeedHandlerResult> { private static final String TAG = "FeedParserTask"; private final DownloadRequest request; - private DownloadStatus downloadStatus; + private DownloadResult downloadResult; private boolean successful = true; public FeedParserTask(DownloadRequest request) { this.request = request; - downloadStatus = new DownloadStatus( + downloadResult = new DownloadResult( 0, request.getTitle(), 0, request.getFeedfileType(), false, - false, true, DownloadError.ERROR_REQUEST_ERROR, new Date(), - "Unknown error: Status not set", request.isInitiatedByUser()); + DownloadError.ERROR_REQUEST_ERROR, new Date(), + "Unknown error: Status not set"); } @Override @@ -87,12 +87,12 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { } if (successful) { - downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, - successful, reasonDetailed, request.isInitiatedByUser()); + downloadResult = new DownloadResult(feed, feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, + successful, reasonDetailed); return result; } else { - downloadStatus = new DownloadStatus(feed, feed.getHumanReadableIdentifier(), reason, - successful, reasonDetailed, request.isInitiatedByUser()); + downloadResult = new DownloadResult(feed, feed.getHumanReadableIdentifier(), reason, + successful, reasonDetailed); return null; } } @@ -120,7 +120,7 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { } @NonNull - public DownloadStatus getDownloadStatus() { - return downloadStatus; + public DownloadResult getDownloadStatus() { + return downloadResult; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 9cb1166b4..3b72ed164 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.service.download.handler; import android.content.Context; import androidx.annotation.NonNull; import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; import de.danoeh.antennapod.parser.feed.FeedHandlerResult; @@ -30,7 +30,7 @@ public class FeedSyncTask { } @NonNull - public DownloadStatus getDownloadStatus() { + public DownloadResult getDownloadStatus() { return task.getDownloadStatus(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index c632bf1e0..a46b4c6d0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -13,7 +13,7 @@ import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; @@ -30,9 +30,9 @@ public class MediaDownloadedHandler implements Runnable { private static final String TAG = "MediaDownloadedHandler"; private final DownloadRequest request; private final Context context; - private DownloadStatus updatedStatus; + private DownloadResult updatedStatus; - public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadStatus status, + public MediaDownloadedHandler(@NonNull Context context, @NonNull DownloadResult status, @NonNull DownloadRequest request) { this.request = request; this.context = context; @@ -94,8 +94,8 @@ public class MediaDownloadedHandler implements Runnable { Log.e(TAG, "MediaHandlerThread was interrupted"); } catch (ExecutionException e) { Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.getMessage()); - updatedStatus = new DownloadStatus(media, media.getEpisodeTitle(), - DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage(), request.isInitiatedByUser()); + updatedStatus = new DownloadResult(media, media.getEpisodeTitle(), + DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); } if (item != null) { @@ -107,7 +107,7 @@ public class MediaDownloadedHandler implements Runnable { } @NonNull - public DownloadStatus getUpdatedStatus() { + public DownloadResult getUpdatedStatus() { return updatedStatus; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java deleted file mode 100644 index 5d2c48679..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/PostDownloaderTask.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.core.service.download.handler; - -import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.service.download.Downloader; -import org.greenrobot.eventbus.EventBus; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class PostDownloaderTask implements Runnable { - private List<Downloader> downloads; - - public PostDownloaderTask(List<Downloader> downloads) { - this.downloads = downloads; - } - - @Override - public void run() { - List<Downloader> runningDownloads = new ArrayList<>(); - for (Downloader downloader : downloads) { - if (!downloader.cancelled) { - runningDownloads.add(downloader); - } - } - List<Downloader> list = Collections.unmodifiableList(runningDownloads); - EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 42631296b..6fc9035ca 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -824,7 +824,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { && SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) { setSleepTimer(SleepTimerPreferences.timerMillis()); EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label), - PlaybackService.this::disableSleepTimer)); + (ctx) -> disableSleepTimer(), getString(R.string.undo))); } loadQueueForMediaSession(); break; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 0f3121551..dbbfba379 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -9,12 +9,10 @@ import java.util.List; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.PowerUtils; @@ -97,13 +95,9 @@ public class AutomaticDownloadAlgorithm { if (itemsToDownload.size() > 0) { Log.d(TAG, "Enqueueing " + itemsToDownload.size() + " items for download"); - List<DownloadRequest> requests = new ArrayList<>(); for (FeedItem episode : itemsToDownload) { - DownloadRequest.Builder request = DownloadRequestCreator.create(episode.getMedia()); - request.withInitiatedByUser(false); - requests.add(request.build()); + DownloadServiceInterface.get().download(context, episode); } - DownloadServiceInterface.get().download(context, false, requests.toArray(new DownloadRequest[0])); } } }; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 94a7334f3..d83557b0c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -16,10 +16,9 @@ import java.util.List; import java.util.Map; import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.core.util.comparator.DownloadResultComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; -import de.danoeh.antennapod.model.download.DownloadStatus; import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; @@ -28,14 +27,15 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.feed.SubscriptionsFilter; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.storage.database.PodDBAdapter; import de.danoeh.antennapod.storage.database.mapper.ChapterCursorMapper; -import de.danoeh.antennapod.storage.database.mapper.DownloadStatusCursorMapper; +import de.danoeh.antennapod.storage.database.mapper.DownloadResultCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedItemCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedMediaCursorMapper; import de.danoeh.antennapod.storage.database.mapper.FeedPreferencesCursorMapper; -import de.danoeh.antennapod.storage.preferences.UserPreferences; /** * Provides methods for reading data from the AntennaPod database. @@ -394,17 +394,17 @@ public final class DBReader { * @return A list with DownloadStatus objects that represent the download log. * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. */ - public static List<DownloadStatus> getDownloadLog() { + public static List<DownloadResult> getDownloadLog() { Log.d(TAG, "getDownloadLog() called"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) { - List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); + List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { - downloadLog.add(DownloadStatusCursorMapper.convert(cursor)); + downloadLog.add(DownloadResultCursorMapper.convert(cursor)); } - Collections.sort(downloadLog, new DownloadStatusComparator()); + Collections.sort(downloadLog, new DownloadResultComparator()); return downloadLog; } finally { adapter.close(); @@ -418,17 +418,17 @@ public final class DBReader { * @return A list with DownloadStatus objects that represent the feed's download log, * newest events first. */ - public static List<DownloadStatus> getFeedDownloadLog(long feedId) { + public static List<DownloadResult> getFeedDownloadLog(long feedId) { Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feedId + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) { - List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); + List<DownloadResult> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { - downloadLog.add(DownloadStatusCursorMapper.convert(cursor)); + downloadLog.add(DownloadResultCursorMapper.convert(cursor)); } - Collections.sort(downloadLog, new DownloadStatusComparator()); + Collections.sort(downloadLog, new DownloadResultComparator()); return downloadLog; } finally { adapter.close(); @@ -717,10 +717,10 @@ public final class DBReader { } } - public static List<FeedItem> getFeedItemsWithMedia(Long[] mediaIds) { + public static List<FeedItem> getFeedItemsWithUrl(List<String> urls) { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - try (Cursor itemCursor = adapter.getFeedItemCursorByMediaIds(mediaIds)) { + try (Cursor itemCursor = adapter.getFeedItemCursorByUrl(urls)) { List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); loadAdditionalFeedItemListData(items); Collections.sort(items, new PlaybackCompletionDateComparator()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 8b79d594c..e3ac7a7e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -7,14 +7,13 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; @@ -114,21 +113,6 @@ public final class DBTasks { EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); } - public static List<FeedItem> enqueueFeedItemsToDownload(final Context context, - List<FeedItem> items) throws InterruptedException, ExecutionException { - List<FeedItem> itemsToEnqueue = new ArrayList<>(); - if (UserPreferences.enqueueDownloadedEpisodes()) { - LongList queueIDList = DBReader.getQueueIDList(); - for (FeedItem item : items) { - if (!queueIDList.contains(item.getId())) { - itemsToEnqueue.add(item); - } - } - DBWriter.addQueueItem(context, false, itemsToEnqueue.toArray(new FeedItem[0])).get(); - } - return itemsToEnqueue; - } - /** * Looks for non-downloaded episodes in the queue or list of unread items and request a download if * 1. Network is available @@ -267,13 +251,13 @@ public final class DBTasks { FeedItem possibleDuplicate = searchFeedItemGuessDuplicate(newFeed.getItems(), item); if (!newFeed.isLocalFeed() && possibleDuplicate != null && item != possibleDuplicate) { // Canonical episode is the first one returned (usually oldest) - DBWriter.addDownloadStatus(new DownloadStatus(savedFeed, + DBWriter.addDownloadStatus(new DownloadResult(savedFeed, item.getTitle(), DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, "The podcast host appears to have added the same episode twice. " + "AntennaPod still refreshed the feed and attempted to repair it." + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(item) + "\n\nSecond episode that is also in the feed:\n" - + duplicateEpisodeDetails(possibleDuplicate), false)); + + duplicateEpisodeDetails(possibleDuplicate))); continue; } @@ -282,13 +266,13 @@ public final class DBTasks { oldItem = searchFeedItemGuessDuplicate(savedFeed.getItems(), item); if (oldItem != null) { Log.d(TAG, "Repaired duplicate: " + oldItem + ", " + item); - DBWriter.addDownloadStatus(new DownloadStatus(savedFeed, + DBWriter.addDownloadStatus(new DownloadResult(savedFeed, item.getTitle(), DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, "The podcast host changed the ID of an existing episode instead of just " + "updating the episode itself. AntennaPod still refreshed the feed and " + "attempted to repair it." + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(oldItem) - + "\n\nNow the feed contains:\n" + duplicateEpisodeDetails(item), false)); + + "\n\nNow the feed contains:\n" + duplicateEpisodeDetails(item))); oldItem.setItemIdentifier(item.getItemIdentifier()); if (oldItem.isPlayed() && oldItem.getMedia() != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index dcee8a45a..4815737f4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -36,7 +36,7 @@ import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedEvent; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.IntentUtils; @@ -299,7 +299,7 @@ public class DBWriter { * * @param status The DownloadStatus object. */ - public static Future<?> addDownloadStatus(final DownloadStatus status) { + public static Future<?> addDownloadStatus(final DownloadResult status) { return dbExec.submit(() -> { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java index b81f281e8..4ed17c43f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java @@ -8,9 +8,9 @@ import androidx.annotation.Nullable; import java.util.List; import java.util.Random; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.model.playback.Playable; @@ -74,7 +74,7 @@ class ItemEnqueuePositionCalculator { } return curItem != null && curItem.getMedia() != null - && DownloadService.isDownloadingFile(curItem.getMedia().getDownload_url()); + && DownloadServiceInterface.get().isDownloadingEpisode(curItem.getMedia().getDownload_url()); } private static int getCurrentlyPlayingPosition(@NonNull List<FeedItem> curQueue, diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java index 50b4d411f..d848b5a30 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -24,10 +24,10 @@ public class FeedItemUtil { return -1; } - public static int indexOfItemWithMediaId(List<FeedItem> items, long mediaId) { - for(int i=0; i < items.size(); i++) { + public static int indexOfItemWithDownloadUrl(List<FeedItem> items, String downloadUrl) { + for (int i = 0; i < items.size(); i++) { FeedItem item = items.get(i); - if(item != null && item.getMedia() != null && item.getMedia().getId() == mediaId) { + if (item != null && item.getMedia() != null && item.getMedia().getDownload_url().equals(downloadUrl)) { return i; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java new file mode 100644 index 000000000..d1d50fc8a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadResultComparator.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.util.comparator; + +import java.util.Comparator; + +import de.danoeh.antennapod.model.download.DownloadResult; + +/** Compares the completion date of two DownloadResult objects. */ +public class DownloadResultComparator implements Comparator<DownloadResult> { + + @Override + public int compare(DownloadResult lhs, DownloadResult rhs) { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java deleted file mode 100644 index 68b38ec7f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.danoeh.antennapod.core.util.comparator; - -import java.util.Comparator; - -import de.danoeh.antennapod.model.download.DownloadStatus; - -/** Compares the completion date of two Downloadstatus objects. */ -public class DownloadStatusComparator implements Comparator<DownloadStatus> { - - @Override - public int compare(DownloadStatus lhs, DownloadStatus rhs) { - return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); - } - -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java index d66bd2360..6a7e51bac 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java @@ -13,7 +13,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -26,7 +25,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import static de.danoeh.antennapod.core.util.FeedItemUtil.getIdList; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -255,63 +253,6 @@ public class DbTasksTest { } } - @Test - public void testAddQueueItemsInDownload_EnqueueEnabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(true); - UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - // the first item fis1.get(0) is already in the queue - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Arrays.asList(fis1.get(1), fis2.get(2), fis2.get(1)); - List<FeedItem> expectedQueue = new ArrayList<>(); - expectedQueue.addAll(DBReader.getQueue()); - expectedQueue.addAll(expectedEnqueued); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("Only items not in the queue are enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue()); - } - - @Test - public void testAddQueueItemsInDownload_EnqueueDisabled() throws Exception { - // Setup test data / environment - UserPreferences.setEnqueueDownloadedEpisodes(false); - - List<FeedItem> fis1 = createSavedFeed("Feed 1", 2).getItems(); - List<FeedItem> fis2 = createSavedFeed("Feed 2", 3).getItems(); - - DBWriter.addQueueItem(context, fis1.get(0), fis2.get(0)).get(); - FeedItem[] itemsToDownload = new FeedItem[]{ fis1.get(0), fis1.get(1), fis2.get(2), fis2.get(1) }; - - // Expectations: - List<FeedItem> expectedEnqueued = Collections.emptyList(); - List<FeedItem> expectedQueue = DBReader.getQueue(); - - // Run actual test and assert results - List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); - - assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued); - assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue()); - } - - private void assertEqualsByIds(String msg, List<? extends FeedItem> expected, List<? extends FeedItem> actual) { - // assert only the IDs, so that any differences are easily to spot. - List<Long> expectedIds = getIdList(expected); - List<Long> actualIds = getIdList(actual); - assertEquals(msg, expectedIds, actualIds); - } - private Feed createSavedFeed(String title, int numFeedItems) { final Feed feed = new Feed("url", null, title); diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java index 376e0e65c..2594fabf6 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -1,7 +1,8 @@ package de.danoeh.antennapod.core.storage; -import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -21,8 +22,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMother; import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; import de.danoeh.antennapod.model.playback.Playable; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import static de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation.AFTER_CURRENTLY_PLAYING; import static de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation.BACK; @@ -74,6 +73,7 @@ public class ItemEnqueuePositionCalculatorTest { */ @Test public void test() { + DownloadServiceInterface.setImpl(new DownloadServiceInterfaceStub()); ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); // shallow copy to which the test will add items @@ -128,95 +128,6 @@ public class ItemEnqueuePositionCalculatorTest { } - @RunWith(Parameterized.class) - public static class PreserveDownloadOrderTest { - /** - * The test covers the use case that when user initiates multiple downloads in succession, - * resulting in multiple addQueueItem() calls in succession. - * the items in the queue will be in the same order as the order user taps to download - */ - @Parameters(name = "{index}: case<{0}>") - public static Iterable<Object[]> data() { - // Attempts to make test more readable by showing the expected list of ids - // (rather than the expected positions) - return Arrays.asList(new Object[][] { - {"download order test, enqueue default", - concat(QUEUE_DEFAULT_IDS, 101L), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L)), - concat(QUEUE_DEFAULT_IDS, list(101L, 102L, 103L)), - BACK, QUEUE_DEFAULT, ID_CURRENTLY_PLAYING_NULL}, - {"download order test, enqueue at front (currently playing has no effect)", - concat(101L, QUEUE_DEFAULT_IDS), - concat(list(101L, 102L), QUEUE_DEFAULT_IDS), - concat(list(101L, 103L, 102L), QUEUE_DEFAULT_IDS), - // ^ 103 is put ahead of 102, after 102 failed. - // It is a limitation as the logic can't tell 102 download has failed - // (as opposed to simply being enqueued) - FRONT, QUEUE_DEFAULT, 11L}, // 11 is at the front, currently playing - {"download order test, enqueue after currently playing", - list(11L, 101L, 12L, 13L, 14L), - list(11L, 101L, 102L, 12L, 13L, 14L), - list(11L, 101L, 103L, 102L, 12L, 13L, 14L), - AFTER_CURRENTLY_PLAYING, QUEUE_DEFAULT, 11L} // 11 is at the front, currently playing - }); - } - - @Parameter - public String message; - - @Parameter(1) - public List<Long> idsExpectedAfter101; - - @Parameter(2) - public List<Long> idsExpectedAfter102; - - @Parameter(3) - public List<Long> idsExpectedAfter103; - - @Parameter(4) - public EnqueueLocation options; - - @Parameter(5) - public List<FeedItem> queueInitial; - - @Parameter(6) - public long idCurrentlyPlaying; - - @Test - public void testQueueOrderWhenDownloading2Items() { - ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options); - try (MockedStatic<DownloadService> downloadServiceMock = Mockito.mockStatic(DownloadService.class)) { - List<FeedItem> queue = new ArrayList<>(queueInitial); - - // Test body - Playable currentlyPlaying = getCurrentlyPlaying(idCurrentlyPlaying); - // User clicks download on feed item 101 - FeedItem feedItem101 = createFeedItem(101); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem101.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message + " (1st download)", - calculator, feedItem101, queue, currentlyPlaying, idsExpectedAfter101); - // Then user clicks download on feed item 102 - FeedItem feedItem102 = createFeedItem(102); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem102.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message + " (2nd download, it should preserve order of download)", - calculator, feedItem102, queue, currentlyPlaying, idsExpectedAfter102); - // simulate download failure case for 102 - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem102.getMedia().getDownload_url())).thenReturn(false); - // Then user clicks download on feed item 103 - FeedItem feedItem103 = createFeedItem(103); - downloadServiceMock.when(() -> - DownloadService.isDownloadingFile(feedItem103.getMedia().getDownload_url())).thenReturn(true); - doAddToQueueAndAssertResult(message - + " (3rd download, with 2nd download failed; " - + "it should be behind 1st download (unless enqueueLocation is BACK)", - calculator, feedItem103, queue, currentlyPlaying, idsExpectedAfter103); - } - } - } - static void doAddToQueueAndAssertResult(String message, ItemEnqueuePositionCalculator calculator, FeedItem itemToAdd, diff --git a/event/build.gradle b/event/build.gradle index 033fc5a3c..b1efac85d 100644 --- a/event/build.gradle +++ b/event/build.gradle @@ -5,6 +5,7 @@ apply from: "../common.gradle" dependencies { implementation project(':model') + implementation "androidx.core:core:$coreVersion" annotationProcessor "androidx.annotation:annotation:$annotationVersion" } diff --git a/event/src/main/java/de/danoeh/antennapod/event/EpisodeDownloadEvent.java b/event/src/main/java/de/danoeh/antennapod/event/EpisodeDownloadEvent.java new file mode 100644 index 000000000..c9790d18a --- /dev/null +++ b/event/src/main/java/de/danoeh/antennapod/event/EpisodeDownloadEvent.java @@ -0,0 +1,18 @@ +package de.danoeh.antennapod.event; + +import de.danoeh.antennapod.model.download.DownloadStatus; + +import java.util.Map; +import java.util.Set; + +public class EpisodeDownloadEvent { + private final Map<String, DownloadStatus> map; + + public EpisodeDownloadEvent(Map<String, DownloadStatus> map) { + this.map = map; + } + + public Set<String> getUrls() { + return map.keySet(); + } +} diff --git a/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java b/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java index 3f6b2db32..7f1cb20e2 100644 --- a/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java +++ b/event/src/main/java/de/danoeh/antennapod/event/MessageEvent.java @@ -1,21 +1,27 @@ package de.danoeh.antennapod.event; +import android.content.Context; import androidx.annotation.Nullable; +import androidx.core.util.Consumer; + public class MessageEvent { public final String message; @Nullable - public final Runnable action; + public final Consumer<Context> action; + + @Nullable + public final String actionText; public MessageEvent(String message) { - this(message, null); + this(message, null, null); } - public MessageEvent(String message, Runnable action) { + public MessageEvent(String message, Consumer<Context> action, String actionText) { this.message = message; this.action = action; + this.actionText = actionText; } - } diff --git a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java new file mode 100644 index 000000000..66901cf65 --- /dev/null +++ b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadResult.java @@ -0,0 +1,128 @@ +package de.danoeh.antennapod.model.download; + +import androidx.annotation.NonNull; + +import java.util.Date; + +import de.danoeh.antennapod.model.feed.FeedFile; + +/** + * Contains status attributes for one download + */ +public class DownloadResult { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + private final String title; + private final long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + private final int feedfileType; + /** + * Unique id for storing the object in database. + */ + private long id; + private DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + private String reasonDetailed; + private boolean successful; + private final Date completionDate; + + /** + * Constructor for creating new completed downloads. + */ + public DownloadResult(@NonNull FeedFile feedfile, String title, DownloadError reason, boolean successful, + String reasonDetailed) { + this(0, title, feedfile.getId(), feedfile.getTypeAsInt(), successful, reason, new Date(), + reasonDetailed); + } + + public DownloadResult(long id, String title, long feedfileId, int feedfileType, boolean successful, + DownloadError reason, Date completionDate, + String reasonDetailed) { + this.id = id; + this.title = title; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = (Date) completionDate.clone(); + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + @Override + @NonNull + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + "]"; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return (Date) completionDate.clone(); + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + } +}
\ No newline at end of file diff --git a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java index 0a18973df..823c183d2 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java +++ b/model/src/main/java/de/danoeh/antennapod/model/download/DownloadStatus.java @@ -1,153 +1,23 @@ package de.danoeh.antennapod.model.download; -import androidx.annotation.NonNull; - -import java.util.Date; - -import de.danoeh.antennapod.model.feed.FeedFile; - -/** - * Contains status attributes for one download - */ public class DownloadStatus { - /** - * Downloaders should use this constant for the size attribute if necessary - * so that the listadapters etc. can react properly. - */ - public static final int SIZE_UNKNOWN = -1; + public static final int STATE_QUEUED = 0; + public static final int STATE_COMPLETED = 1; // Both successful and not successful + public static final int STATE_RUNNING = 2; - // ----------------------------------- ATTRIBUTES STORED IN DB - /** - * A human-readable string which is shown to the user so that he can - * identify the download. Should be the title of the item/feed/media or the - * URL if the download has no other title. - */ - private final String title; - private final long feedfileId; - /** - * Is used to determine the type of the feedfile even if the feedfile does - * not exist anymore. The value should be FEEDFILETYPE_FEED, - * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA - */ - private final int feedfileType; - private final boolean initiatedByUser; - /** - * Unique id for storing the object in database. - */ - private long id; - private DownloadError reason; - /** - * A message which can be presented to the user to give more information. - * Should be null if Download was successful. - */ - private String reasonDetailed; - private boolean successful; - private final Date completionDate; - // ------------------------------------ NOT STORED IN DB - private boolean done; - private boolean cancelled; - - /** - * Constructor for creating new completed downloads. - */ - public DownloadStatus(@NonNull FeedFile feedfile, String title, DownloadError reason, boolean successful, - String reasonDetailed, boolean initiatedByUser) { - this(0, title, feedfile.getId(), feedfile.getTypeAsInt(), successful, false, true, reason, new Date(), - reasonDetailed, initiatedByUser); - } - - public DownloadStatus(long id, String title, long feedfileId, int feedfileType, boolean successful, - boolean cancelled, boolean done, DownloadError reason, Date completionDate, - String reasonDetailed, boolean initiatedByUser) { - this.id = id; - this.title = title; - this.feedfileId = feedfileId; - this.reason = reason; - this.successful = successful; - this.cancelled = cancelled; - this.done = done; - this.completionDate = (Date) completionDate.clone(); - this.reasonDetailed = reasonDetailed; - this.feedfileType = feedfileType; - this.initiatedByUser = initiatedByUser; - } - - @Override - @NonNull - public String toString() { - return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" - + reason + ", reasonDetailed=" + reasonDetailed - + ", successful=" + successful + ", completionDate=" - + completionDate + ", feedfileId=" + feedfileId - + ", feedfileType=" + feedfileType + ", done=" + done - + ", cancelled=" + cancelled + "]"; - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public DownloadError getReason() { - return reason; - } - - public String getReasonDetailed() { - return reasonDetailed; - } - - public boolean isSuccessful() { - return successful; - } - - public Date getCompletionDate() { - return (Date) completionDate.clone(); - } - - public long getFeedfileId() { - return feedfileId; - } - - public int getFeedfileType() { - return feedfileType; - } - - public boolean isInitiatedByUser() { - return initiatedByUser; - } - - public boolean isDone() { - return done; - } - - public boolean isCancelled() { - return cancelled; - } + private final int state; + private final int progress; - public void setSuccessful() { - this.successful = true; - this.reason = DownloadError.SUCCESS; - this.done = true; + public DownloadStatus(int state, int progress) { + this.state = state; + this.progress = progress; } - public void setFailed(DownloadError reason, String reasonDetailed) { - this.successful = false; - this.reason = reason; - this.reasonDetailed = reasonDetailed; - this.done = true; + public int getState() { + return state; } - public void setCancelled() { - this.successful = false; - this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; - this.done = true; - this.cancelled = true; + public int getProgress() { + return progress; } -}
\ No newline at end of file +} diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java index b5d0cd991..5fac79ce8 100644 --- a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterface.java @@ -1,10 +1,20 @@ package de.danoeh.antennapod.net.download.serviceinterface; import android.content.Context; -import android.content.Intent; +import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.feed.FeedItem; + +import java.util.HashMap; +import java.util.Map; public abstract class DownloadServiceInterface { + public static final String WORK_TAG = "episodeDownload"; + public static final String WORK_TAG_EPISODE_URL = "episodeUrl:"; + public static final String WORK_DATA_PROGRESS = "progress"; + public static final String WORK_DATA_MEDIA_ID = "media_id"; + public static final String WORK_DATA_WAS_QUEUED = "was_queued"; private static DownloadServiceInterface impl; + private Map<String, DownloadStatus> currentDownloads = new HashMap<>(); public static DownloadServiceInterface get() { return impl; @@ -14,13 +24,35 @@ public abstract class DownloadServiceInterface { DownloadServiceInterface.impl = impl; } - public abstract void download(Context context, boolean cleanupMedia, DownloadRequest... requests); + public void setCurrentDownloads(Map<String, DownloadStatus> currentDownloads) { + this.currentDownloads = currentDownloads; + } - public abstract Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests); + /** + * Download immediately after user action. + */ + public abstract void downloadNow(Context context, FeedItem item, boolean ignoreConstraints); - public abstract void refreshAllFeeds(Context context, boolean initiatedByUser); + /** + * Download when device seems fit. + */ + public abstract void download(Context context, FeedItem item); public abstract void cancel(Context context, String url); public abstract void cancelAll(Context context); + + public boolean isDownloadingEpisode(String url) { + return currentDownloads.containsKey(url) + && currentDownloads.get(url).getState() != DownloadStatus.STATE_COMPLETED; + } + + public boolean isEpisodeQueued(String url) { + return currentDownloads.containsKey(url) + && currentDownloads.get(url).getState() == DownloadStatus.STATE_QUEUED; + } + + public int getProgress(String url) { + return isDownloadingEpisode(url) ? currentDownloads.get(url).getProgress() : -1; + } } diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java index 947746485..716d68d14 100644 --- a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/DownloadServiceInterfaceStub.java @@ -1,23 +1,23 @@ package de.danoeh.antennapod.net.download.serviceinterface; import android.content.Context; -import android.content.Intent; +import de.danoeh.antennapod.model.feed.FeedItem; public class DownloadServiceInterfaceStub extends DownloadServiceInterface { - public void download(Context context, boolean cleanupMedia, DownloadRequest... requests) { + @Override + public void downloadNow(Context context, FeedItem item, boolean ignoreConstraints) { } - public Intent makeDownloadIntent(Context context, boolean cleanupMedia, DownloadRequest... requests) { - return null; - } - - public void refreshAllFeeds(Context context, boolean initiatedByUser) { + @Override + public void download(Context context, FeedItem item) { } + @Override public void cancel(Context context, String url) { } + @Override public void cancelAll(Context context) { } } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java index 40933e8d1..21f12e223 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/PodDBAdapter.java @@ -35,7 +35,7 @@ import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.storage.database.mapper.FeedItemFilterQuery; import de.danoeh.antennapod.storage.database.mapper.FeedItemSortQuery; @@ -757,7 +757,7 @@ public class PodDBAdapter { /** * Inserts or updates a download status. */ - public long setDownloadStatus(DownloadStatus status) { + public long setDownloadStatus(DownloadResult status) { ContentValues values = new ContentValues(); values.put(KEY_FEEDFILE, status.getFeedfileId()); values.put(KEY_FEEDFILETYPE, status.getFeedfileType()); @@ -1142,12 +1142,19 @@ public class PodDBAdapter { return db.rawQuery(query, null); } - public final Cursor getFeedItemCursorByMediaIds(final Long[] ids) { - if (ids.length > IN_OPERATOR_MAXIMUM) { + public final Cursor getFeedItemCursorByUrl(List<String> urls) { + if (urls.size() > IN_OPERATOR_MAXIMUM) { throw new IllegalArgumentException("number of IDs must not be larger than " + IN_OPERATOR_MAXIMUM); } + StringBuilder urlsString = new StringBuilder(); + for (int i = 0; i < urls.size(); i++) { + if (i != 0) { + urlsString.append(","); + } + urlsString.append(DatabaseUtils.sqlEscapeString(urls.get(i))); + } final String query = SELECT_FEED_ITEMS_AND_MEDIA - + " WHERE " + SELECT_KEY_MEDIA_ID + " IN (" + TextUtils.join(",", ids) + ")"; + + " WHERE " + KEY_DOWNLOAD_URL + " IN (" + urlsString + ")"; return db.rawQuery(query, null); } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadResultCursorMapper.java index 1b8f3c726..d8f40d6a3 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadStatusCursorMapper.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/mapper/DownloadResultCursorMapper.java @@ -2,21 +2,21 @@ package de.danoeh.antennapod.storage.database.mapper; import android.database.Cursor; import androidx.annotation.NonNull; -import de.danoeh.antennapod.model.download.DownloadStatus; +import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.storage.database.PodDBAdapter; import java.util.Date; /** - * Converts a {@link Cursor} to a {@link DownloadStatus} object. + * Converts a {@link Cursor} to a {@link DownloadResult} object. */ -public abstract class DownloadStatusCursorMapper { +public abstract class DownloadResultCursorMapper { /** - * Create a {@link DownloadStatus} instance from a database row (cursor). + * Create a {@link DownloadResult} instance from a database row (cursor). */ @NonNull - public static DownloadStatus convert(@NonNull Cursor cursor) { + public static DownloadResult convert(@NonNull Cursor cursor) { int indexId = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ID); int indexTitle = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE); int indexFeedFile = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_FEEDFILE); @@ -26,10 +26,10 @@ public abstract class DownloadStatusCursorMapper { int indexCompletionDate = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_COMPLETION_DATE); int indexReasonDetailed = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_REASON_DETAILED); - return new DownloadStatus(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile), - cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, false, true, + return new DownloadResult(cursor.getLong(indexId), cursor.getString(indexTitle), cursor.getLong(indexFeedFile), + cursor.getInt(indexFileFileType), cursor.getInt(indexSuccessful) > 0, DownloadError.fromCode(cursor.getInt(indexReason)), new Date(cursor.getLong(indexCompletionDate)), - cursor.getString(indexReasonDetailed), false); + cursor.getString(indexReasonDetailed)); } } diff --git a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java index 8a19300bb..f2b122fcc 100644 --- a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java +++ b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java @@ -90,7 +90,6 @@ public class UserPreferences { public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; private static final String PREF_MOBILE_UPDATE = "prefMobileUpdateTypes"; public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup"; - public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; public static final String PREF_ENABLE_AUTODL_ON_BATTERY = "prefEnableAutoDownloadOnBattery"; @@ -525,10 +524,6 @@ public class UserPreferences { setAllowMobileFor("sync", allow); } - public static int getParallelDownloads() { - return Integer.parseInt(prefs.getString(PREF_PARALLEL_DOWNLOADS, "4")); - } - /** * Returns the capacity of the episode cache. This method will return the * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java deleted file mode 100644 index 03c5e915e..000000000 --- a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/DownloadAuthenticationActivityStarter.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.danoeh.antennapod.ui.appstartintent; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Parcelable; - -/** - * Launches the download authentication activity of the app with specific arguments. - * Does not require a dependency on the actual implementation of the activity. - */ -public class DownloadAuthenticationActivityStarter { - public static final String INTENT = "de.danoeh.antennapod.intents.DOWNLOAD_AUTH_ACTIVITY"; - public static final String EXTRA_DOWNLOAD_REQUEST = "download_request"; - - private final Intent intent; - private final Context context; - private final long feedFileId; - - public DownloadAuthenticationActivityStarter(Context context, long feedFileId, Parcelable downloadRequest) { - this.context = context; - this.feedFileId = feedFileId; - intent = new Intent(INTENT); - intent.setAction("request" + feedFileId); - intent.putExtra(EXTRA_DOWNLOAD_REQUEST, downloadRequest); - intent.setPackage(context.getPackageName()); - } - - public Intent getIntent() { - return intent; - } - - public PendingIntent getPendingIntent() { - return PendingIntent.getActivity(context.getApplicationContext(), - ("downloadAuth" + feedFileId).hashCode(), getIntent(), - PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } -} diff --git a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java index 1463978ee..c635ff0cc 100644 --- a/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java +++ b/ui/app-start-intent/src/main/java/de/danoeh/antennapod/ui/appstartintent/MainActivityStarter.java @@ -17,6 +17,7 @@ public class MainActivityStarter { public static final String EXTRA_ADD_TO_BACK_STACK = "add_to_back_stack"; public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_OPEN_DRAWER = "open_drawer"; + public static final String EXTRA_OPEN_DOWNLOAD_LOGS = "open_download_logs"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; private final Intent intent; @@ -30,6 +31,9 @@ public class MainActivityStarter { } public Intent getIntent() { + if (fragmentArgs != null) { + intent.putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs); + } return intent; } @@ -67,6 +71,11 @@ public class MainActivityStarter { return this; } + public MainActivityStarter withDownloadLogsOpen() { + intent.putExtra(EXTRA_OPEN_DOWNLOAD_LOGS, true); + return this; + } + public MainActivityStarter withFragmentArgs(String name, boolean value) { if (fragmentArgs == null) { fragmentArgs = new Bundle(); diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java index a693c28b0..42f63bd9d 100644 --- a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java +++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java @@ -4,7 +4,9 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.DashPathEffect; import android.graphics.Paint; +import android.graphics.PathEffect; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; @@ -13,11 +15,13 @@ import androidx.annotation.Nullable; public class CircularProgressBar extends View { public static final float MINIMUM_PERCENTAGE = 0.005f; public static final float MAXIMUM_PERCENTAGE = 1 - MINIMUM_PERCENTAGE; + private static final PathEffect DASHED = new DashPathEffect(new float[] {5f, 5f}, 0f); private final Paint paintBackground = new Paint(); private final Paint paintProgress = new Paint(); private float percentage = 0; private float targetPercentage = 0; + private boolean isIndeterminate = false; private Object tag = null; private final RectF bounds = new RectF(); @@ -73,9 +77,10 @@ public class CircularProgressBar extends View { float padding = getHeight() * 0.07f; paintBackground.setStrokeWidth(getHeight() * 0.02f); + paintBackground.setPathEffect(isIndeterminate ? DASHED : null); paintProgress.setStrokeWidth(padding); bounds.set(padding, padding, getWidth() - padding, getHeight() - padding); - canvas.drawArc(bounds, 0, 360, false, paintBackground); + canvas.drawArc(bounds, -90, 360, false, paintBackground); if (MINIMUM_PERCENTAGE <= percentage && percentage <= MAXIMUM_PERCENTAGE) { canvas.drawArc(bounds, -90, percentage * 360, false, paintProgress); @@ -92,4 +97,8 @@ public class CircularProgressBar extends View { invalidate(); } } + + public void setIndeterminate(boolean indeterminate) { + isIndeterminate = indeterminate; + } } diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index 59fc8b4e5..1959a2252 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -130,7 +130,6 @@ <string name="feed_volume_reduction_off">Off</string> <string name="feed_volume_reduction_light">Light</string> <string name="feed_volume_reduction_heavy">Heavy</string> - <string name="parallel_downloads">%1$d parallel downloads</string> <string name="feed_auto_download_always">Always</string> <string name="feed_auto_download_never">Never</string> <string name="feed_new_episodes_action_add_to_inbox">Add to Inbox</string> @@ -239,7 +238,6 @@ <item quantity="other">%d episodes marked as unplayed.</item> </plurals> <string name="add_to_queue_label">Add to Queue</string> - <string name="added_to_queue_label">Added to Queue</string> <plurals name="added_to_queue_batch_label"> <item quantity="one">%d episode added to queue.</item> <item quantity="other">%d episodes added to queue.</item> @@ -266,6 +264,8 @@ <string name="download_running">Download running</string> <string name="download_error_details">Details</string> <string name="download_log_details_message">%1$s \n\nTechnical reason: \n%2$s \n\nFile URL:\n%3$s</string> + <string name="download_error_retrying">Download of \"%1$s\" failed. Will be retried later.</string> + <string name="download_error_not_retrying">Download of \"%1$s\" failed.</string> <string name="download_error_tap_for_details">Tap to view details.</string> <string name="download_error_device_not_found">Storage Device not found</string> <string name="download_error_insufficient_space">There is not enough space left on your device.</string> @@ -277,7 +277,7 @@ <string name="download_error_not_found">The podcast host\'s server does not know where to find the file. It may have been deleted.</string> <string name="download_error_connection_error">Connection Error</string> <string name="download_error_unknown_host">Cannot find the server. Check if the address is typed correctly and if you have a working network connection.</string> - <string name="download_error_unauthorized">Authentication Error</string> + <string name="download_error_unauthorized">Authentication Error. Make sure that username and password are correct.</string> <string name="download_error_file_type_type">File Type Error</string> <string name="download_error_forbidden">The podcast host\'s server refuses to respond.</string> <string name="download_canceled_msg">Download canceled</string> @@ -285,7 +285,6 @@ <string name="download_error_blocked">The download was blocked by another app on your device (like a VPN or ad blocker).</string> <string name="download_error_certificate">Unable to establish a secure connection. This can mean that another app on your device (like a VPN or an ad blocker) blocked the download, or that something is wrong with the server certificates.</string> <string name="download_report_title">Downloads completed with error(s)</string> - <string name="auto_download_report_title">Auto-downloads completed</string> <string name="download_error_io_error">IO Error</string> <string name="download_error_request_error">Request Error</string> <string name="download_error_db_access">Database Access Error</string> @@ -293,8 +292,6 @@ <item quantity="one">%d download left</item> <item quantity="other">%d downloads left</item> </plurals> - <string name="completing">Completing…</string> - <string name="download_notification_title">Downloading podcast data</string> <string name="download_notification_title_feeds">Refreshing podcasts</string> <string name="download_notification_title_episodes">Downloading episodes</string> <string name="download_log_title_unknown">Unknown Title</string> @@ -303,16 +300,14 @@ <string name="null_value_podcast_error">No podcast was provided that could be shown.</string> <string name="no_feed_url_podcast_found_by_search">The suggested podcast did not have an RSS link, AntennaPod found a podcast that could match</string> <string name="authentication_notification_title">Authentication required</string> - <string name="authentication_notification_msg">The resource you requested requires a username and a password</string> <string name="confirm_mobile_download_dialog_title">Confirm Mobile Download</string> - <string name="confirm_mobile_download_dialog_message_not_in_queue">Downloading over mobile data connection is disabled in the settings.\n\nYou can choose to either only add the episode to the queue or you can allow downloading temporarily.\n\n<small>Your choice will be remembered for 10 minutes.</small></string> - <string name="confirm_mobile_download_dialog_message">Downloading over mobile data connection is disabled in the settings.\n\nDo you want to allow downloading temporarily?\n\n<small>Your choice will be remembered for 10 minutes.</small></string> + <string name="confirm_mobile_download_dialog_message">Downloading over mobile data connection is disabled in the settings. AntennaPod can download the episode later automatically when Wi-Fi is available.</string> + <string name="confirm_mobile_download_dialog_download_later">Download later</string> + <string name="confirm_mobile_download_dialog_allow_this_time">Download anyway</string> <string name="confirm_mobile_streaming_notification_title">Confirm Mobile streaming</string> <string name="confirm_mobile_streaming_notification_message">Streaming over mobile data connection is disabled in the settings. Tap to stream anyway.</string> <string name="confirm_mobile_streaming_button_always">Always</string> <string name="confirm_mobile_streaming_button_once">Once</string> - <string name="confirm_mobile_download_dialog_only_add_to_queue">Enqueue</string> - <string name="confirm_mobile_download_dialog_enable_temporarily">Allow temporarily</string> <!-- Mediaplayer messages --> <string name="playback_error_generic"><![CDATA[The media file could not be played.\n\n- Try deleting and re-downloading the episode.\n- Check your network connection, and make sure no VPN or login page is blocking access.\n- Try long-pressing and sharing the \"Media address\" to your web browser to see if it can be played there. If not, contact the podcast creators.]]></string> @@ -453,7 +448,6 @@ <string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string> <string name="pref_automatic_download_on_battery_title">Download when not charging</string> <string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string> - <string name="pref_parallel_downloads_title">Parallel Downloads</string> <string name="pref_episode_cache_title">Episode Cache</string> <string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.</string> <string name="pref_episode_cover_title">Use Episode Cover</string> diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml b/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml deleted file mode 100644 index a5480c71f..000000000 --- a/ui/png-icons/src/main/res/drawable/ic_notification_cancel.xml +++ /dev/null @@ -1,5 +0,0 @@ -<vector android:height="24dp" - android:viewportHeight="24.0" android:viewportWidth="24.0" - android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FFFFFFFF" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/> -</vector> diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_key.xml b/ui/png-icons/src/main/res/drawable/ic_notification_key.xml deleted file mode 100644 index c8a817eeb..000000000 --- a/ui/png-icons/src/main/res/drawable/ic_notification_key.xml +++ /dev/null @@ -1,6 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" android:viewportHeight="24.0" - android:viewportWidth="24.0" android:width="24dp"> - <path android:fillColor="#FFFFFFFF" - android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/> -</vector> |