refactor downloadMedia() - make DownloadService accepts a batch of DownloadRequests.

- the DB logic originally in DBTasks.downloadFeedItems() are moved to DownloadService.
This commit is contained in:
orionlee 2019-10-26 17:40:55 -07:00
parent 7bc5ca74f1
commit a6e5cd144d
5 changed files with 114 additions and 59 deletions

View File

@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia; 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.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.DownloadStatus;
@ -77,16 +78,29 @@ public class DownloadServiceTest {
} }
@Test @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 // create a stub download that returns successful
// //
// OPEN: Ideally, I'd like the download time long enough so that multiple in-progress DownloadEvents // 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) // are generated (to simulate typical download), but it'll make download time quite long (1-2 seconds)
// to do so // to do so
DownloadService.setDownloaderFactory(new StubDownloaderFactory(50, downloadStatus -> { DownloadService.setDownloaderFactory(new StubDownloaderFactory(50, downloadStatus -> {
downloadStatus.setSuccessful(); downloadStatus.setSuccessful();
})); }));
UserPreferences.setEnqueueDownloadedEpisodes(enqueueDownloaded);
withFeedItemEventListener(feedItemEventListener -> { withFeedItemEventListener(feedItemEventListener -> {
try { try {
assertEquals(0, feedItemEventListener.getEvents().size()); assertEquals(0, feedItemEventListener.getEvents().size());
@ -96,9 +110,12 @@ public class DownloadServiceTest {
DownloadRequester.getInstance().downloadMedia(false, InstrumentationRegistry.getTargetContext(), DownloadRequester.getInstance().downloadMedia(false, InstrumentationRegistry.getTargetContext(),
testMedia11.getItem());Awaitility.await() testMedia11.getItem());Awaitility.await()
.atMost(1000, TimeUnit.MILLISECONDS) .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.", assertTrue("After media download has completed, FeedMedia object in db should indicate so.",
DBReader.getFeedMedia(testMedia11.getId()).isDownloaded()); DBReader.getFeedMedia(testMedia11.getId()).isDownloaded());
assertEquals("The FeedItem should have been " + (enqueueDownloaded ? "" : "not ") + "enqueued",
enqueueDownloaded,
DBReader.getQueueIDList().contains(testMedia11.getItem().getId()));
} catch (ConditionTimeoutException cte) { } catch (ConditionTimeoutException cte) {
fail("The expected FeedItemEvent (for media download complete) has not been posted. " fail("The expected FeedItemEvent (for media download complete) has not been posted. "
+ cte.getMessage()); + cte.getMessage());

View File

@ -206,7 +206,7 @@ public class DBTasksTest {
// Run actual test and assert results // Run actual test and assert results
List<? extends FeedItem> actualEnqueued = 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("Only items not in the queue are enqueued", expectedEnqueued, actualEnqueued);
assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue()); assertEqualsByIds("Queue has new items appended", expectedQueue, DBReader.getQueue());
@ -229,7 +229,7 @@ public class DBTasksTest {
// Run actual test and assert results // Run actual test and assert results
List<? extends FeedItem> actualEnqueued = List<? extends FeedItem> actualEnqueued =
DBTasks.enqueueFeedItemsToDownload(context, itemsToDownload); DBTasks.enqueueFeedItemsToDownload(context, Arrays.asList(itemsToDownload));
assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued); assertEqualsByIds("No item is enqueued", expectedEnqueued, actualEnqueued);
assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue()); assertEqualsByIds("Queue is unchanged", expectedQueue, DBReader.getQueue());

View File

@ -12,8 +12,30 @@ import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; 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.ClientConfig;
import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.FeedItemEvent; 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.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.DownloadError; 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. * 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. * the intent.
* After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the
* type of the feedfile. * type of the feedfile.
@ -79,7 +84,9 @@ public class DownloadService extends Service {
/** /**
* Extra for ACTION_ENQUEUE_DOWNLOAD intent. * 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; public static final int NOTIFICATION_ID = 2;
@ -156,7 +163,8 @@ public class DownloadService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { 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); onDownloadQueued(intent);
} else if (numberOfDownloads.get() == 0) { } else if (numberOfDownloads.get() == 0) {
stopSelf(); stopSelf();
@ -387,13 +395,55 @@ public class DownloadService extends Service {
}; };
private void onDownloadQueued(Intent intent) { private void onDownloadQueued(Intent intent) {
Log.d(TAG, "Received enqueue request"); List<DownloadRequest> requests = intent.getParcelableArrayListExtra(EXTRA_REQUESTS);
DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); if (requests == null) {
if (request == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); "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;
}
// TODO-2448: use itemsEnqueued to handle cancel download
for (DownloadRequest request : requests) {
onDownloadQueued(request);
}
}
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) {
writeFileUrl(request); writeFileUrl(request);
Downloader downloader = downloaderFactory.create(request); Downloader downloader = downloaderFactory.create(request);

View File

@ -308,7 +308,7 @@ public final class DBTasks {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context, public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context,
FeedItem... items) List<? extends FeedItem> items)
throws InterruptedException, ExecutionException { throws InterruptedException, ExecutionException {
List<FeedItem> itemsToEnqueue = new ArrayList<>(); List<FeedItem> itemsToEnqueue = new ArrayList<>();
if (UserPreferences.enqueueDownloadedEpisodes()) { if (UserPreferences.enqueueDownloadedEpisodes()) {

View File

@ -21,7 +21,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFile; import de.danoeh.antennapod.core.feed.FeedFile;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
@ -77,14 +76,20 @@ public class DownloadRequester implements DownloadStateProvider {
* ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead.
* *
* @param context Context object for starting the DownloadService * @param context Context object for starting the DownloadService
* @param requests The list of DownloadRequest objects. If another DownloadRequest with the same source URL is already stored, * @param requests The list of DownloadRequest objects. If another DownloadRequest
* this one will be skipped. * with the same source URL is already stored, this one will be skipped.
* @return True if the any of the download request was accepted, false otherwise. * @return True if any of the download request was accepted, false otherwise.
*/ */
public synchronized boolean download(@NonNull Context context, public synchronized boolean download(@NonNull Context context,
DownloadRequest... requests) { DownloadRequest... requests) {
return download(context, false, requests);
}
private boolean download(@NonNull Context context, boolean cleanupMedia,
DownloadRequest... requests) {
boolean result = false; boolean result = false;
// TODO-2448: send the requests as a batch to the service in one intent
ArrayList<DownloadRequest> requestsToSend = new ArrayList<>(requests.length);
for (DownloadRequest request : requests) { for (DownloadRequest request : requests) {
if (downloads.containsKey(request.getSource())) { if (downloads.containsKey(request.getSource())) {
if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored.");
@ -92,11 +97,15 @@ public class DownloadRequester implements DownloadStateProvider {
} }
downloads.put(request.getSource(), request); downloads.put(request.getSource(), request);
Intent launchIntent = new Intent(context, DownloadService.class); requestsToSend.add(request);
launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request);
ContextCompat.startForegroundService(context, launchIntent);
result = true; result = true;
} }
Intent launchIntent = new Intent(context, DownloadService.class);
launchIntent.putParcelableArrayListExtra(DownloadService.EXTRA_REQUESTS, requestsToSend);
if (cleanupMedia) {
launchIntent.putExtra(DownloadService.EXTRA_CLEANUP_MEDIA, cleanupMedia);
}
ContextCompat.startForegroundService(context, launchIntent);
return result; return result;
} }
@ -213,27 +222,6 @@ public class DownloadRequester implements DownloadStateProvider {
throws DownloadRequestException { throws DownloadRequestException {
Log.d(TAG, "downloadMedia() called with: performAutoCleanup = [" + performAutoCleanup Log.d(TAG, "downloadMedia() called with: performAutoCleanup = [" + performAutoCleanup
+ "], #items = [" + items.length + "]"); + "], #items = [" + items.length + "]");
// TODO-2448: OPEN: move to DownloadService as well?!
if (performAutoCleanup) {
new Thread() {
@Override
public void run() {
ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm()
.makeRoomForEpisodes(context, items.length);
}
}.start();
}
// TODO-2448: move to DownloadService
// #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 {
// DBTasks.enqueueFeedItemsToDownload(context, items);
// } catch (Throwable t) {
// throw new DownloadRequestException("Unexpected exception during enqueue before downloads", t);
// }
List<DownloadRequest> requests = new ArrayList<>(items.length); List<DownloadRequest> requests = new ArrayList<>(items.length);
for (FeedItem item : items) { for (FeedItem item : items) {
@ -260,7 +248,7 @@ public class DownloadRequester implements DownloadStateProvider {
} }
} }
} }
download(context, requests.toArray(new DownloadRequest[0])); download(context, performAutoCleanup, requests.toArray(new DownloadRequest[0]));
} }
@Nullable @Nullable