diff options
17 files changed, 514 insertions, 205 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java index 3b5b35946..d0e14d70a 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/DownloadServiceTest.java @@ -5,7 +5,6 @@ import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import de.danoeh.antennapod.core.service.download.DownloaderFactory; import org.awaitility.Awaitility; import org.awaitility.core.ConditionTimeoutException; import org.junit.After; @@ -22,10 +21,12 @@ import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.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; @@ -58,7 +59,9 @@ public class DownloadServiceTest { } private Feed setUpTestFeeds() throws Exception { - Feed feed = new Feed("url", null, "Test Feed title 1"); + // 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); @@ -77,29 +80,108 @@ public class DownloadServiceTest { } @Test - public void testEventsGeneratedCaseMediaDownloadSuccess() throws Exception { + 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 -> { - downloadStatus.setSuccessful(); + 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()); - DownloadRequester.getInstance().downloadMedia(InstrumentationRegistry.getTargetContext(), - testMedia11); - Awaitility.await() + DownloadRequester.getInstance().downloadMedia(false, InstrumentationRegistry.getTargetContext(), + testMedia11.getItem());Awaitility.await() .atMost(1000, TimeUnit.MILLISECONDS) - .until(() -> feedItemEventListener.getEvents().size() > 0); + .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 { + // let download takes longer to ensure the test can cancel the download in time + DownloadService.setDownloaderFactory(new StubDownloaderFactory(150, downloadStatus -> { + downloadStatus.setSuccessful(); + })); + UserPreferences.setEnqueueDownloadedEpisodes(true); + UserPreferences.setEnableAutodownload(false); + + final long item1Id = testMedia11.getItem().getId(); + if (itemAlreadyInQueue) { + // simulate item already in queue condition + DBWriter.addQueueItem(InstrumentationRegistry.getTargetContext(), false, item1Id).get(); + assertTrue(DBReader.getQueueIDList().contains(item1Id)); + } else { + assertFalse(DBReader.getQueueIDList().contains(item1Id)); + } + + withFeedItemEventListener(feedItemEventListener -> { + try { + DownloadRequester.getInstance().downloadMedia(false, InstrumentationRegistry.getTargetContext(), + testMedia11.getItem()); + if (itemAlreadyInQueue) { + Awaitility.await("download service receives the request - " + + "no event is expected before cancel is issued") + .atLeast(100, TimeUnit.MILLISECONDS) + .until(() -> true); + } else { + Awaitility.await("item enqueue event") + .atMost(1000, TimeUnit.MILLISECONDS) + .until(() -> feedItemEventListener.getEvents().size() >= 1); + } + DownloadRequester.getInstance().cancelDownload(InstrumentationRegistry.getTargetContext(), + testMedia11); + final int totalNumEventsExpected = itemAlreadyInQueue ? 1 : 3; + Awaitility.await("item dequeue event + download termination event") + .atMost(1000, 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)); + } } catch (ConditionTimeoutException cte) { fail("The expected FeedItemEvent (for media download complete) has not been posted. " + cte.getMessage()); diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java index cce4e5111..090cd2213 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/DBTasksTest.java @@ -206,7 +206,7 @@ public class DBTasksTest { // Run actual test and assert results List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, itemsToDownload); + 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()); @@ -229,7 +229,7 @@ public class DBTasksTest { // Run actual test and assert results List<? extends FeedItem> actualEnqueued = - DBTasks.enqueueFeedItemsToDownload(context, itemsToDownload); + DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload)); assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued); assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue()); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java index fd669f3e6..ed1698ecf 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java @@ -54,7 +54,6 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR private final boolean showOnlyNewEpisodes; private FeedItem selectedItem; - private Holder currentlyPlayingItem = null; private final int playingBackGroundColor; private final int normalBackGroundColor; @@ -172,7 +171,6 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR if (media.isCurrentlyPlaying()) { holder.container.setBackgroundColor(playingBackGroundColor); - currentlyPlayingItem = holder; } else { holder.container.setBackgroundColor(normalBackGroundColor); } @@ -202,22 +200,6 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR .load(); } - @Override - public void onBindViewHolder(@NonNull Holder holder, int pos, List<Object> payload) { - onBindViewHolder(holder, pos); - - if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) { - PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0); - holder.progress.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); - } - } - - public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event) { - if (currentlyPlayingItem != null && currentlyPlayingItem.getAdapterPosition() != RecyclerView.NO_POSITION) { - notifyItemChanged(currentlyPlayingItem.getAdapterPosition(), event); - } - } - @Nullable public FeedItem getSelectedItem() { return selectedItem; @@ -302,6 +284,14 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item); } + public boolean isCurrentlyPlayingItem() { + return item.getMedia() != null && item.getMedia().isCurrentlyPlaying(); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + progress.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + } + } public interface ItemAccess { 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 b8764c2ae..4d66fd486 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.content.Context; import android.os.Build; -import androidx.core.content.ContextCompat; import android.text.Layout; import android.text.format.DateUtils; import android.util.Log; @@ -13,6 +12,8 @@ import android.widget.BaseAdapter; import android.widget.TextView; import android.widget.Toast; +import androidx.core.content.ContextCompat; + import com.joanzapata.iconify.widget.IconButton; import com.joanzapata.iconify.widget.IconTextView; @@ -24,6 +25,7 @@ import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; /** Displays a list of DownloadStatus entries. */ public class DownloadLogAdapter extends BaseAdapter { @@ -132,7 +134,7 @@ public class DownloadLogAdapter extends BaseAdapter { FeedMedia media = DBReader.getFeedMedia(holder.id); if (media != null) { try { - DBTasks.downloadFeedItems(context, media.getItem()); + DownloadRequester.getInstance().downloadMedia(context, media.getItem()); Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); } catch (DownloadRequestException e) { e.printStackTrace(); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index 42f009c85..ddfcfe0d3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -61,7 +61,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap private boolean locked; private FeedItem selectedItem; - private ViewHolder currentlyPlayingItem = null; private final int playingBackGroundColor; private final int normalBackGroundColor; @@ -84,11 +83,13 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap notifyDataSetChanged(); } + @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.queue_listitem, parent, false); return new ViewHolder(view); } + @Override public void onBindViewHolder(ViewHolder holder, int pos) { FeedItem item = itemAccess.getItem(pos); holder.bind(item); @@ -98,18 +99,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap }); } - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int pos, List<Object> payload) { - onBindViewHolder(holder, pos); - - if (holder == currentlyPlayingItem && payload.size() == 1 && payload.get(0) instanceof PlaybackPositionEvent) { - PlaybackPositionEvent event = (PlaybackPositionEvent) payload.get(0); - holder.progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); - holder.progressLeft.setText(Converter.getDurationStringLong(event.getPosition())); - holder.progressRight.setText(Converter.getDurationStringLong(event.getDuration())); - } - } - @Nullable public FeedItem getSelectedItem() { return selectedItem; @@ -125,12 +114,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap return itemAccess.getCount(); } - public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event) { - if (currentlyPlayingItem != null && currentlyPlayingItem.getAdapterPosition() != RecyclerView.NO_POSITION) { - notifyItemChanged(currentlyPlayingItem.getAdapterPosition(), event); - } - } - public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnCreateContextMenuListener, @@ -310,7 +293,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap if(media.isCurrentlyPlaying()) { container.setBackgroundColor(playingBackGroundColor); - currentlyPlayingItem = this; } else { container.setBackgroundColor(normalBackGroundColor); } @@ -330,6 +312,15 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap .load(); } + public boolean isCurrentlyPlayingItem() { + return item.getMedia() != null && item.getMedia().isCurrentlyPlaying(); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + progressLeft.setText(Converter.getDurationStringLong(event.getPosition())); + progressRight.setText(Converter.getDurationStringLong(event.getDuration())); + } } public interface ItemAccess { 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 55ca5471b..026386cf9 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 @@ -1,16 +1,16 @@ package de.danoeh.antennapod.adapter.actionbutton; import android.content.Context; +import android.widget.Toast; + import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; -import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -64,7 +64,7 @@ class DownloadActionButton extends ItemActionButton { private void downloadEpisode(Context context) { try { - DBTasks.downloadFeedItems(context, item); + DownloadRequester.getInstance().downloadMedia(context, item); Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); } catch (DownloadRequestException e) { e.printStackTrace(); 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 index 4fdfe231d..77efd9023 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java @@ -8,9 +8,9 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; class MobileDownloadHelper { private static long addToQueueTimestamp; @@ -48,7 +48,7 @@ class MobileDownloadHelper { private static void downloadFeedItems(Context context, FeedItem item) { allowMobileDownloadTimestamp = System.currentTimeMillis(); try { - DBTasks.downloadFeedItems(context, item); + DownloadRequester.getInstance().downloadMedia(context, item); Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); } catch (DownloadRequestException e) { e.printStackTrace(); diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index 447e98dac..ff131aeba 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -36,9 +36,9 @@ import java.util.Map; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.SortOrder; @@ -485,7 +485,7 @@ public class EpisodesApplyActionFragment extends Fragment { } } try { - DBTasks.downloadFeedItems(getActivity(), toDownload.toArray(new FeedItem[toDownload.size()])); + DownloadRequester.getInstance().downloadMedia(getActivity(), toDownload.toArray(new FeedItem[toDownload.size()])); } catch (DownloadRequestException e) { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage()); 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 3802e1f65..fe48de815 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -383,7 +383,14 @@ public abstract class EpisodesListFragment extends Fragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(PlaybackPositionEvent event) { if (listAdapter != null) { - listAdapter.notifyCurrentlyPlayingItemChanged(event); + for (int i = 0; i < listAdapter.getItemCount(); i++) { + AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder) + recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } } } 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 ce1276175..82b388b1b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -212,7 +212,14 @@ public class QueueFragment extends Fragment { @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(PlaybackPositionEvent event) { if (recyclerAdapter != null) { - recyclerAdapter.notifyCurrentlyPlayingItemChanged(event); + for (int i = 0; i < recyclerAdapter.getItemCount(); i++) { + QueueRecyclerAdapter.ViewHolder holder = (QueueRecyclerAdapter.ViewHolder) + recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } } } diff --git a/core/src/androidTest/java/de/danoeh/antennapod/core/service/download/DownloadRequestTest.java b/core/src/androidTest/java/de/danoeh/antennapod/core/service/download/DownloadRequestTest.java new file mode 100644 index 000000000..71212e6ec --- /dev/null +++ b/core/src/androidTest/java/de/danoeh/antennapod/core/service/download/DownloadRequestTest.java @@ -0,0 +1,122 @@ +package de.danoeh.antennapod.core.service.download; + +import android.os.Bundle; +import android.os.Parcel; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +import java.util.ArrayList; + +import de.danoeh.antennapod.core.feed.FeedFile; + +import static org.junit.Assert.assertEquals; + +@SmallTest +public class DownloadRequestTest { + + @Test + public void parcelInArrayListTest_WithAuth() { + doTestParcelInArrayList("case has authentication", + "usr1", "pass1", "usr2", "pass2"); + } + + @Test + public void parcelInArrayListTest_NoAuth() { + doTestParcelInArrayList("case no authentication", + null, null, null, null); + } + + @Test + public void parcelInArrayListTest_MixAuth() { + doTestParcelInArrayList("case mixed authentication", + null, null, "usr2", "pass2"); + } + + // Test to ensure parcel using put/getParcelableArrayList() API work + // based on: https://stackoverflow.com/a/13507191 + private void doTestParcelInArrayList(String message, + String username1, String password1, + String username2, String password2) { + ArrayList<DownloadRequest> toParcel; + { // test DownloadRequests to parcel + String destStr = "file://location/media.mp3"; + FeedFile item1 = createFeedItem(1); + Bundle arg1 = new Bundle(); + arg1.putString("arg1", "value1"); + DownloadRequest request1 = new DownloadRequest.Builder(destStr, item1) + .withAuthentication(username1, password1) + .withArguments(arg1) + .build(); + + FeedFile item2 = createFeedItem(2); + DownloadRequest request2 = new DownloadRequest.Builder(destStr, item2) + .withAuthentication(username2, password2) + .build(); + + toParcel = new ArrayList<>(); + toParcel.add(request1); + toParcel.add(request2); + } + + // parcel the download requests + Bundle bundleIn = new Bundle(); + bundleIn.putParcelableArrayList("r", toParcel); + + Parcel parcel = Parcel.obtain(); + bundleIn.writeToParcel(parcel, 0); + + Bundle bundleOut = new Bundle(); + bundleOut.setClassLoader(DownloadRequest.class.getClassLoader()); + parcel.setDataPosition(0); // to read the parcel from the beginning. + bundleOut.readFromParcel(parcel); + + ArrayList<DownloadRequest> fromParcel = bundleOut.getParcelableArrayList("r"); + + // spot-check contents to ensure they are the same + // DownloadRequest.equals() implementation doesn't quite work + // for DownloadRequest.argument (a Bundle) + assertEquals(message + " - size", toParcel.size(), fromParcel.size()); + assertEquals(message + " - source", toParcel.get(1).getSource(), fromParcel.get(1).getSource()); + assertEquals(message + " - password", toParcel.get(0).getPassword(), fromParcel.get(0).getPassword()); + assertEquals(message + " - argument", toString(toParcel.get(0).getArguments()), + toString(fromParcel.get(0).getArguments())); + } + + private static String toString(Bundle b) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (String key: b.keySet()) { + Object val = b.get(key); + sb.append("(").append(key).append(":").append(val).append(") "); + } + sb.append("}"); + return sb.toString(); + } + + private FeedFile createFeedItem(final int id) { + // Use mockito would be less verbose, but it'll take extra 1 second for this tiny test + return new FeedFile() { + @Override + public long getId() { + return id; + } + + @Override + public String getDownload_url() { + return "http://example.com/episode" + id; + } + + @Override + public int getTypeAsInt() { + return 0; + } + + @Override + public String getHumanReadableIdentifier() { + return "human-id-" + id; + } + }; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index a3d3b56e0..750f79711 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -544,6 +544,11 @@ public class UserPreferences { return prefs.getBoolean(PREF_ENABLE_AUTODL, false); } + @VisibleForTesting + public static void setEnableAutodownload(boolean enabled) { + prefs.edit().putBoolean(PREF_ENABLE_AUTODL, enabled).apply(); + } + public static boolean isEnableAutodownloadOnBattery() { return prefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java index 60591899d..2dd46cf96 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -3,6 +3,8 @@ package de.danoeh.antennapod.core.service.download; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -26,6 +28,7 @@ public class DownloadRequest implements Parcelable { private long soFar; private long size; private int statusMsg; + private boolean mediaEnqueued; public DownloadRequest(@NonNull String destination, @NonNull String source, @@ -45,6 +48,7 @@ public class DownloadRequest implements Parcelable { this.username = username; this.password = password; this.deleteOnFailure = deleteOnFailure; + this.mediaEnqueued = false; this.arguments = (arguments != null) ? arguments : new Bundle(); } @@ -74,17 +78,10 @@ public class DownloadRequest implements Parcelable { feedfileType = in.readInt(); lastModified = in.readString(); deleteOnFailure = (in.readByte() > 0); + username = nullIfEmpty(in.readString()); + password = nullIfEmpty(in.readString()); + mediaEnqueued = (in.readByte() > 0); arguments = in.readBundle(); - if (in.dataAvail() > 0) { - username = in.readString(); - } else { - username = null; - } - if (in.dataAvail() > 0) { - password = in.readString(); - } else { - password = null; - } } @Override @@ -101,13 +98,23 @@ public class DownloadRequest implements Parcelable { dest.writeInt(feedfileType); dest.writeString(lastModified); dest.writeByte((deleteOnFailure) ? (byte) 1 : 0); + // in case of null username/password, still write an empty string + // (rather than skipping it). Otherwise, unmarshalling a collection + // of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail. + // + // see: https://stackoverflow.com/a/22926342 + dest.writeString(nonNullString(username)); + dest.writeString(nonNullString(password)); + dest.writeByte((mediaEnqueued) ? (byte) 1 : 0); dest.writeBundle(arguments); - if (username != null) { - dest.writeString(username); - } - if (password != null) { - dest.writeString(password); - } + } + + private static String nonNullString(String str) { + return str != null ? str : ""; + } + + private static String nullIfEmpty(String str) { + return TextUtils.isEmpty(str) ? null : str; } public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { @@ -145,6 +152,7 @@ public class DownloadRequest implements Parcelable { if (title != null ? !title.equals(that.title) : that.title != null) return false; if (username != null ? !username.equals(that.username) : that.username != null) return false; + if (mediaEnqueued != that.mediaEnqueued) return false; return true; } @@ -164,6 +172,7 @@ public class DownloadRequest implements Parcelable { result = 31 * result + (int) (soFar ^ (soFar >>> 32)); result = 31 * result + (int) (size ^ (size >>> 32)); result = 31 * result + statusMsg; + result = 31 * result + (mediaEnqueued ? 1 : 0); return result; } @@ -245,6 +254,18 @@ public class DownloadRequest implements Parcelable { return deleteOnFailure; } + public boolean isMediaEnqueued() { + return mediaEnqueued; + } + + /** + * Set to true if the media is enqueued because of this download. + * The state is helpful if the download is cancelled, and undoing the enqueue is needed. + */ + public void setMediaEnqueued(boolean mediaEnqueued) { + this.mediaEnqueued = mediaEnqueued; + } + public Bundle getArguments() { return arguments; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 95f78366f..cf0502c92 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -12,8 +12,30 @@ import android.os.Handler; 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 org.apache.commons.io.FileUtils; +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; @@ -32,27 +54,10 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.DownloadError; -import java.io.File; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletionService; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.commons.io.FileUtils; -import org.greenrobot.eventbus.EventBus; /** * Manages the download of feedfiles in the app. Downloads can be enqueued via the startService intent. - * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * 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. @@ -79,7 +84,9 @@ public class DownloadService extends Service { /** * Extra for ACTION_ENQUEUE_DOWNLOAD intent. */ - public static final String EXTRA_REQUEST = "request"; + public static final String EXTRA_REQUESTS = "downloadRequests"; + + public static final String EXTRA_CLEANUP_MEDIA = "cleanupMedia"; public static final int NOTIFICATION_ID = 2; @@ -156,7 +163,8 @@ public class DownloadService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { + if (intent != null && + intent.getParcelableArrayListExtra(EXTRA_REQUESTS) != null) { onDownloadQueued(intent); } else if (numberOfDownloads.get() == 0) { stopSelf(); @@ -363,10 +371,20 @@ public class DownloadService extends Service { Downloader d = getDownloader(url); if (d != null) { d.cancel(); - DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + DownloadRequest request = d.getDownloadRequest(); + DownloadRequester.getInstance().removeDownload(request); - FeedItem item = getFeedItemFromId(d.getDownloadRequest().getFeedfileId()); + FeedItem item = getFeedItemFromId(request.getFeedfileId()); if (item != null) { + // undo enqueue upon cancel + if (request.isMediaEnqueued()) { + Log.v(TAG, "Undoing enqueue upon cancelling download"); + try { + DBWriter.removeQueueItem(getApplicationContext(), false, item).get(); + } catch (Throwable t) { + Log.e(TAG, "Unexpected exception during undoing enqueue upon cancel", t); + } + } EventBus.getDefault().post(FeedItemEvent.updated(item)); } } else { @@ -387,13 +405,55 @@ public class DownloadService extends Service { }; private void onDownloadQueued(Intent intent) { - Log.d(TAG, "Received enqueue request"); - DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); - if (request == null) { + List<DownloadRequest> requests = intent.getParcelableArrayListExtra(EXTRA_REQUESTS); + if (requests == null) { throw new IllegalArgumentException( "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); } + boolean cleanupMedia = intent.getBooleanExtra(EXTRA_CLEANUP_MEDIA, false); + Log.d(TAG, "Received enqueue request. #requests=" + requests.size() + + ", cleanupMedia=" + cleanupMedia); + + if (cleanupMedia) { + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() + .makeRoomForEpisodes(getApplicationContext(), requests.size()); + } + + // #2448: First, add to-download items to the queue before actual download + // so that the resulting queue order is the same as when download is clicked + List<? extends FeedItem> itemsEnqueued; + try { + itemsEnqueued = enqueueFeedItems(requests); + } catch (Exception e) { + Log.e(TAG, "Unexpected exception during enqueue before downloads. Abort download", e); + return; + } + for (DownloadRequest request : requests) { + onDownloadQueued(request, itemsEnqueued); + } + } + + private List<? extends FeedItem> enqueueFeedItems(@NonNull List<? extends DownloadRequest> requests) + throws Exception { + 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()); + } + } + + return DBTasks.enqueueFeedItemsToDownload(getApplicationContext(), feedItems); + } + + private void onDownloadQueued(@NonNull DownloadRequest request, + @NonNull List<? extends FeedItem> itemsEnqueued) { writeFileUrl(request); Downloader downloader = downloaderFactory.create(request); @@ -403,6 +463,9 @@ public class DownloadService extends Service { if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { downloads.add(0, downloader); } else { + if (isEnqueued(request, itemsEnqueued)) { + request.setMediaEnqueued(true); + } downloads.add(downloader); } downloadExecutor.submit(downloader); @@ -413,6 +476,19 @@ public class DownloadService extends Service { queryDownloads(); } + private static boolean isEnqueued(@NonNull DownloadRequest request, + @NonNull List<? extends FeedItem> itemsEnqueued) { + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + final long mediaId = request.getFeedfileId(); + for (FeedItem item : itemsEnqueued) { + if (item.getMedia() != null && item.getMedia().getId() == mediaId) { + return true; + } + } + } + return false; + } + @VisibleForTesting public static DownloaderFactory getDownloaderFactory() { return downloaderFactory; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java index aa97b321a..218320c68 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java @@ -92,7 +92,7 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download"); try { - DBTasks.downloadFeedItems(false, context, itemsToDownload); + DownloadRequester.getInstance().downloadMedia(false, context, itemsToDownload); } catch (DownloadRequestException e) { e.printStackTrace(); } 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 7dc53f8b3..826fe48b5 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 @@ -9,6 +9,8 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; +import org.greenrobot.eventbus.EventBus; + import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -38,7 +40,6 @@ import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; -import org.greenrobot.eventbus.EventBus; import static android.content.Context.MODE_PRIVATE; @@ -305,70 +306,9 @@ public final class DBTasks { EventBus.getDefault().post(new FeedListUpdateEvent(media.getItem().getFeed())); } - /** - * Requests the download of a list of FeedItem objects. - * - * @param context Used for requesting the download and accessing the DB. - * @param items The FeedItem objects. - */ - public static void downloadFeedItems(final Context context, - FeedItem... items) throws DownloadRequestException { - downloadFeedItems(true, context, items); - } - - static void downloadFeedItems(boolean performAutoCleanup, - final Context context, final FeedItem... items) - throws DownloadRequestException { - final DownloadRequester requester = DownloadRequester.getInstance(); - - if (performAutoCleanup) { - new Thread() { - - @Override - public void run() { - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() - .makeRoomForEpisodes(context, items.length); - } - - }.start(); - } - // #2448: First, add to-download items to the queue before actual download - // so that the resulting queue order is the same as when download is clicked - try { - enqueueFeedItemsToDownload(context, items); - } catch (Throwable t) { - throw new DownloadRequestException("Unexpected exception during enqueue before downloads", t); - } - - // Then, download them - for (FeedItem item : items) { - if (item.getMedia() != null - && !requester.isDownloadingFile(item.getMedia()) - && !item.getMedia().isDownloaded()) { - if (items.length > 1) { - try { - requester.downloadMedia(context, item.getMedia()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - new DownloadStatus(item.getMedia(), item - .getMedia() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage() - ) - ); - } - } else { - requester.downloadMedia(context, item.getMedia()); - } - } - } - } - - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context, - FeedItem... items) + List<? extends FeedItem> items) throws InterruptedException, ExecutionException { List<FeedItem> itemsToEnqueue = new ArrayList<>(); if (UserPreferences.enqueueDownloadedEpisodes()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index c61abc168..ea3724adc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -8,21 +8,28 @@ import android.util.Log; import android.webkit.URLUtil; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.content.ContextCompat; import org.apache.commons.io.FilenameUtils; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.URLChecker; @@ -64,40 +71,56 @@ public class DownloadRequester implements DownloadStateProvider { } /** - * Starts a new download with the given DownloadRequest. This method should only + * Starts a new download with the given a list of DownloadRequest. This method should only * be used from outside classes if the DownloadRequest was created by the DownloadService to * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. * * @param context Context object for starting the DownloadService - * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method - * call will return false. - * @return True if the download request was accepted, false otherwise. + * @param requests The list of DownloadRequest objects. If another DownloadRequest + * with the same source URL is already stored, this one will be skipped. + * @return True if any of the download request was accepted, false otherwise. */ public synchronized boolean download(@NonNull Context context, - @NonNull DownloadRequest request) { - if (downloads.containsKey(request.getSource())) { - if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); - return false; - } - downloads.put(request.getSource(), request); + DownloadRequest... requests) { + return download(context, false, requests); + } + private boolean download(@NonNull Context context, boolean cleanupMedia, + DownloadRequest... requests) { + boolean result = false; + + ArrayList<DownloadRequest> requestsToSend = new ArrayList<>(requests.length); + for (DownloadRequest request : requests) { + if (downloads.containsKey(request.getSource())) { + if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); + continue; + } + downloads.put(request.getSource(), request); + + requestsToSend.add(request); + result = true; + } Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + launchIntent.putParcelableArrayListExtra(DownloadService.EXTRA_REQUESTS, requestsToSend); + if (cleanupMedia) { + launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, cleanupMedia); + } ContextCompat.startForegroundService(context, launchIntent); - return true; + return result; } - private void download(Context context, FeedFile item, FeedFile container, File dest, - boolean overwriteIfExists, String username, String password, - String lastModified, boolean deleteOnFailure, Bundle arguments) { + @Nullable + private DownloadRequest createRequest(FeedFile item, FeedFile container, File dest, + boolean overwriteIfExists, String username, String password, + String lastModified, boolean deleteOnFailure, Bundle arguments) { final boolean partiallyDownloadedFileExists = item.getFile_url() != null && new File(item.getFile_url()).exists(); Log.d(TAG, "partiallyDownloadedFileExists: " + partiallyDownloadedFileExists); if (isDownloadingFile(item)) { - Log.e(TAG, "URL " + item.getDownload_url() - + " is already being downloaded"); - return; + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + return null; } if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) { Log.d(TAG, "Filename already used."); @@ -136,8 +159,7 @@ public class DownloadRequester implements DownloadStateProvider { .lastModified(lastModified) .deleteOnFailure(deleteOnFailure) .withArguments(arguments); - DownloadRequest request = builder.build(); - download(context, request); + return builder.build(); } /** @@ -178,8 +200,9 @@ public class DownloadRequester implements DownloadStateProvider { args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages); - download(context, feed, null, new File(getFeedfilePath(), getFeedfileName(feed)), + DownloadRequest request = createRequest(feed, null, new File(getFeedfilePath(), getFeedfileName(feed)), true, username, password, lastModified, true, args); + download(context, request); } } @@ -187,29 +210,72 @@ public class DownloadRequester implements DownloadStateProvider { downloadFeed(context, feed, false, false); } - public synchronized void downloadMedia(Context context, FeedMedia feedmedia) + public synchronized void downloadMedia(@NonNull Context context, FeedItem... feedItems) throws DownloadRequestException { - if (feedFileValid(feedmedia)) { - Feed feed = feedmedia.getItem().getFeed(); - String username; - String password; - if (feed != null && feed.getPreferences() != null) { - username = feed.getPreferences().getUsername(); - password = feed.getPreferences().getPassword(); - } else { - username = null; - password = null; - } + downloadMedia(true, context, feedItems); - File dest; - if (feedmedia.getFile_url() != null) { - dest = new File(feedmedia.getFile_url()); - } else { - dest = new File(getMediafilePath(feedmedia), getMediafilename(feedmedia)); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public synchronized void downloadMedia(boolean performAutoCleanup, @NonNull Context context, + FeedItem... items) + throws DownloadRequestException { + Log.d(TAG, "downloadMedia() called with: performAutoCleanup = [" + performAutoCleanup + + "], #items = [" + items.length + "]"); + + List<DownloadRequest> requests = new ArrayList<>(items.length); + for (FeedItem item : items) { + try { + DownloadRequest request = createRequest(item.getMedia()); + if (request != null) { + requests.add(request); + } + } catch (DownloadRequestException e) { + if (items.length < 2) { + // single download, typically initiated from users + throw e; + } else { + // batch download, typically initiated by auto-download in the background + e.printStackTrace(); + DBWriter.addDownloadStatus( + new DownloadStatus(item.getMedia(), item + .getMedia() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage() + ) + ); + } } - download(context, feedmedia, feed, - dest, false, username, password, null, false, null); } + download(context, performAutoCleanup, requests.toArray(new DownloadRequest[0])); + } + + @Nullable + private DownloadRequest createRequest(@Nullable FeedMedia feedmedia) + throws DownloadRequestException { + if (!feedFileValid(feedmedia)) { + return null; + } + Feed feed = feedmedia.getItem().getFeed(); + String username; + String password; + if (feed != null && feed.getPreferences() != null) { + username = feed.getPreferences().getUsername(); + password = feed.getPreferences().getPassword(); + } else { + username = null; + password = null; + } + + File dest; + if (feedmedia.getFile_url() != null) { + dest = new File(feedmedia.getFile_url()); + } else { + dest = new File(getMediafilePath(feedmedia), getMediafilename(feedmedia)); + } + return createRequest(feedmedia, feed, + dest, false, username, password, null, false, null); } /** |