diff options
author | Hao Liu <haoliuu@google.com> | 2023-04-07 21:23:15 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-04-07 21:23:15 +0000 |
commit | 824f349a0225e1a67e4cbbacd81e34755c1d8f97 (patch) | |
tree | 58860f9e332410ab89db0730585820af6ce5c448 /java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java | |
parent | 3a067b5d70b0429acc213c9c16a2b19037810829 (diff) | |
parent | 9058c22d3f5f5bd9162c7ecc24402187375adae9 (diff) | |
download | mobile-data-download-824f349a0225e1a67e4cbbacd81e34755c1d8f97.tar.gz |
Code dump with latest updates. am: f8fef1787d am: 9058c22d3f
Original change: https://googleplex-android-review.googlesource.com/c/platform/external/mobile-data-download/+/22462863
Change-Id: I9daa3d37544ad17f0ce401f7d181c5606b666ca0
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java')
-rw-r--r-- | java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java | 435 |
1 files changed, 268 insertions, 167 deletions
diff --git a/java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java b/java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java index 1c0cb49..8472667 100644 --- a/java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java +++ b/java/com/google/android/libraries/mobiledatadownload/lite/DownloaderImpl.java @@ -15,6 +15,9 @@ */ package com.google.android.libraries.mobiledatadownload.lite; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +import static com.google.common.util.concurrent.Futures.immediateVoidFuture; + import android.content.Context; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; @@ -22,17 +25,18 @@ import androidx.core.app.NotificationManagerCompat; import com.google.android.libraries.mobiledatadownload.DownloadException; import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; +import com.google.android.libraries.mobiledatadownload.foreground.ForegroundDownloadKey; import com.google.android.libraries.mobiledatadownload.foreground.NotificationUtil; import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; +import com.google.android.libraries.mobiledatadownload.internal.util.DownloadFutureMap; +import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; +import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; import com.google.common.base.Optional; -import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.MoreExecutors; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.Executor; import org.checkerframework.checker.nullness.compatqual.NullableDecl; @@ -46,9 +50,8 @@ final class DownloaderImpl implements Downloader { private final Optional<SingleFileDownloadProgressMonitor> downloadMonitorOptional; private final Supplier<FileDownloader> fileDownloaderSupplier; - // Synchronization will be done through sequentialControlExecutor - @VisibleForTesting - final Map<String, ListenableFuture<Void>> keyToListenableFuture = new HashMap<>(); + @VisibleForTesting final DownloadFutureMap<Void> downloadFutureMap; + @VisibleForTesting final DownloadFutureMap<Void> foregroundDownloadFutureMap; DownloaderImpl( Context context, @@ -61,19 +64,25 @@ final class DownloaderImpl implements Downloader { this.foregroundDownloadServiceClassOptional = foregroundDownloadServiceClassOptional; this.downloadMonitorOptional = downloadMonitorOptional; this.fileDownloaderSupplier = fileDownloaderSupplier; + this.downloadFutureMap = DownloadFutureMap.create(sequentialControlExecutor); + this.foregroundDownloadFutureMap = + DownloadFutureMap.create( + sequentialControlExecutor, + createCallbacksForForegroundService(context, foregroundDownloadServiceClassOptional)); } @Override public ListenableFuture<Void> download(DownloadRequest downloadRequest) { LogUtil.d("%s: download for Uri = %s", TAG, downloadRequest.destinationFileUri().toString()); - return Futures.submitAsync( - () -> { + ForegroundDownloadKey foregroundDownloadKey = + ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri()); + + return PropagatedFutures.transformAsync( + getInProgressDownloadFuture(foregroundDownloadKey.toString()), + (Optional<ListenableFuture<Void>> existingDownloadFuture) -> { // if there is the same on-going request, return that one. - if (keyToListenableFuture.containsKey(downloadRequest.destinationFileUri().toString())) { - // uriToListenableFuture.get must return Non-null since we check the containsKey above. - // checkNotNull is to suppress false alarm about @Nullable result. - return Preconditions.checkNotNull( - keyToListenableFuture.get(downloadRequest.destinationFileUri().toString())); + if (existingDownloadFuture.isPresent()) { + return existingDownloadFuture.get(); } // Register listener with monitor if present @@ -87,13 +96,19 @@ final class DownloaderImpl implements Downloader { } else { LogUtil.w( "%s: download request included DownloadListener, but DownloadMonitor is not" - + " present! DownloadListener will only be invoked for complete/failure."); + + " present! DownloadListener will only be invoked for complete/failure.", + TAG); } } - ListenableFuture<Void> downloadFuture = startDownload(downloadRequest); + // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the + // future to our map. + ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null); + ListenableFuture<Void> downloadFuture = + PropagatedFutures.transformAsync( + startTask, unused -> startDownload(downloadRequest), sequentialControlExecutor); - Futures.addCallback( + PropagatedFutures.addCallback( downloadFuture, new FutureCallback<Void>() { @Override @@ -104,36 +119,36 @@ final class DownloaderImpl implements Downloader { // Remove download listener and remove download future from map after listener // completes if (downloadRequest.listenerOptional().isPresent()) { - Futures.addCallback( + PropagatedFutures.addCallback( downloadRequest.listenerOptional().get().onComplete(), new FutureCallback<Void>() { @Override public void onSuccess(@NullableDecl Void result) { - keyToListenableFuture.remove( - downloadRequest.destinationFileUri().toString()); if (downloadMonitorOptional.isPresent()) { downloadMonitorOptional .get() .removeDownloadListener(downloadRequest.destinationFileUri()); } + ListenableFuture<Void> unused = + downloadFutureMap.remove(foregroundDownloadKey.toString()); } @Override public void onFailure(Throwable t) { LogUtil.e(t, "%s: Failed to run client onComplete", TAG); - keyToListenableFuture.remove( - downloadRequest.destinationFileUri().toString()); if (downloadMonitorOptional.isPresent()) { downloadMonitorOptional .get() .removeDownloadListener(downloadRequest.destinationFileUri()); } + ListenableFuture<Void> unused = + downloadFutureMap.remove(foregroundDownloadKey.toString()); } }, sequentialControlExecutor); } else { - // remove from future map immediately - keyToListenableFuture.remove(downloadRequest.destinationFileUri().toString()); + ListenableFuture<Void> unused = + downloadFutureMap.remove(foregroundDownloadKey.toString()); } } @@ -151,14 +166,20 @@ final class DownloaderImpl implements Downloader { .removeDownloadListener(downloadRequest.destinationFileUri()); } } - keyToListenableFuture.remove(downloadRequest.destinationFileUri().toString()); + ListenableFuture<Void> unused = + downloadFutureMap.remove(foregroundDownloadKey.toString()); } }, MoreExecutors.directExecutor()); - keyToListenableFuture.put( - downloadRequest.destinationFileUri().toString(), downloadFuture); - return downloadFuture; + return PropagatedFutures.transformAsync( + downloadFutureMap.add(foregroundDownloadKey.toString(), downloadFuture), + unused -> { + // Now that the download future is added, start the task and return the future + startTask.run(); + return downloadFuture; + }, + sequentialControlExecutor); }, sequentialControlExecutor); } @@ -178,7 +199,7 @@ final class DownloaderImpl implements Downloader { return fileDownloaderSupplier.get().startDownloading(fileDownloaderRequest); } catch (RuntimeException e) { // Catch any unchecked exceptions that prevented the download from starting. - return Futures.immediateFailedFuture( + return immediateFailedFuture( DownloadException.builder() .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR) .setCause(e) @@ -192,23 +213,25 @@ final class DownloaderImpl implements Downloader { "%s: downloadWithForegroundService for Uri = %s", TAG, downloadRequest.destinationFileUri().toString()); if (!downloadMonitorOptional.isPresent()) { - return Futures.immediateFailedFuture( + return immediateFailedFuture( new IllegalStateException( "downloadWithForegroundService: DownloadMonitor is not provided!")); } if (!foregroundDownloadServiceClassOptional.isPresent()) { - return Futures.immediateFailedFuture( + return immediateFailedFuture( new IllegalStateException( "downloadWithForegroundService: ForegroundDownloadService is not provided!")); } - return Futures.submitAsync( - () -> { + + ForegroundDownloadKey foregroundDownloadKey = + ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri()); + + return PropagatedFutures.transformAsync( + getInProgressDownloadFuture(foregroundDownloadKey.toString()), + (Optional<ListenableFuture<Void>> existingDownloadFuture) -> { // if there is the same on-going request, return that one. - if (keyToListenableFuture.containsKey(downloadRequest.destinationFileUri().toString())) { - // uriToListenableFuture.get must return Non-null since we check the containsKey above. - // checkNotNull is to suppress false alarm about @Nullable result. - return Preconditions.checkNotNull( - keyToListenableFuture.get(downloadRequest.destinationFileUri().toString())); + if (existingDownloadFuture.isPresent()) { + return existingDownloadFuture.get(); } // It's OK to recreate the NotificationChannel since it can also be used to restore a @@ -216,14 +239,6 @@ final class DownloaderImpl implements Downloader { // importance. NotificationUtil.createNotificationChannel(context); - // Only start the foreground download service when there is the first download request. - if (keyToListenableFuture.isEmpty()) { - NotificationUtil.startForegroundDownloadService( - context, - foregroundDownloadServiceClassOptional.get(), - downloadRequest.destinationFileUri().toString()); - } - DownloadListener downloadListenerWithNotification = createDownloadListenerWithNotification(downloadRequest); @@ -233,9 +248,14 @@ final class DownloaderImpl implements Downloader { .addDownloadListener( downloadRequest.destinationFileUri(), downloadListenerWithNotification); - ListenableFuture<Void> downloadFuture = startDownload(downloadRequest); + // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the + // future to our map. + ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null); + ListenableFuture<Void> downloadFuture = + PropagatedFutures.transformAsync( + startTask, unused -> startDownload(downloadRequest), sequentialControlExecutor); - Futures.addCallback( + PropagatedFutures.addCallback( downloadFuture, new FutureCallback<Void>() { @Override @@ -243,7 +263,7 @@ final class DownloaderImpl implements Downloader { // Currently the MobStore monitor does not support onSuccess so we have to add // callback to the download future here. - Futures.addCallback( + PropagatedFutures.addCallback( downloadListenerWithNotification.onComplete(), new FutureCallback<Void>() { @Override @@ -267,15 +287,25 @@ final class DownloaderImpl implements Downloader { }, MoreExecutors.directExecutor()); - keyToListenableFuture.put( - downloadRequest.destinationFileUri().toString(), downloadFuture); - return downloadFuture; + return PropagatedFutures.transformAsync( + foregroundDownloadFutureMap.add(foregroundDownloadKey.toString(), downloadFuture), + unused -> { + // Now that the download future is added, start the task and return the future + startTask.run(); + return downloadFuture; + }, + sequentialControlExecutor); }, sequentialControlExecutor); } // Assertion: foregroundDownloadService and downloadMonitor are present private DownloadListener createDownloadListenerWithNotification(DownloadRequest downloadRequest) { + String networkPausedMessage = + downloadRequest.downloadConstraints().requireUnmeteredNetwork() + ? NotificationUtil.getDownloadPausedWifiMessage(context) + : NotificationUtil.getDownloadPausedMessage(context); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); NotificationCompat.Builder notification = NotificationUtil.createNotificationBuilder( @@ -284,14 +314,16 @@ final class DownloaderImpl implements Downloader { downloadRequest.notificationContentTitle(), downloadRequest.notificationContentTextOptional().or(downloadRequest.urlToDownload())); - int notificationKey = - NotificationUtil.notificationKeyForKey(downloadRequest.destinationFileUri().toString()); + ForegroundDownloadKey foregroundDownloadKey = + ForegroundDownloadKey.ofSingleFile(downloadRequest.destinationFileUri()); + + int notificationKey = NotificationUtil.notificationKeyForKey(foregroundDownloadKey.toString()); // Attach the Cancel action to the notification. NotificationUtil.createCancelAction( context, foregroundDownloadServiceClassOptional.get(), - downloadRequest.destinationFileUri().toString(), + foregroundDownloadKey.toString(), notification, notificationKey); notificationManager.notify(notificationKey, notification.build()); @@ -299,49 +331,56 @@ final class DownloaderImpl implements Downloader { return new DownloadListener() { @Override public void onProgress(long currentSize) { - sequentialControlExecutor.execute( - () -> { - // There can be a race condition, where onPausedForConnectivity can be called - // after onComplete or onFailure which removes the future and the notification. - if (keyToListenableFuture.containsKey( - downloadRequest.destinationFileUri().toString())) { - notification - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setProgress( - downloadRequest.fileSizeBytes(), - (int) currentSize, - /* indeterminate = */ downloadRequest.fileSizeBytes() <= 0); - notificationManager.notify(notificationKey, notification.build()); - } - if (downloadRequest.listenerOptional().isPresent()) { - downloadRequest.listenerOptional().get().onProgress(currentSize); - } - }); + // TODO(b/229123693): return this future once DownloadListener has an async api. + ListenableFuture<?> unused = + PropagatedFutures.transformAsync( + foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()), + futureInProgress -> { + if (futureInProgress) { + notification + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setContentText( + downloadRequest + .notificationContentTextOptional() + .or(downloadRequest.urlToDownload())) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setProgress( + downloadRequest.fileSizeBytes(), + (int) currentSize, + /* indeterminate= */ downloadRequest.fileSizeBytes() <= 0); + notificationManager.notify(notificationKey, notification.build()); + } + if (downloadRequest.listenerOptional().isPresent()) { + downloadRequest.listenerOptional().get().onProgress(currentSize); + } + return immediateVoidFuture(); + }, + sequentialControlExecutor); } @Override public void onPausedForConnectivity() { - sequentialControlExecutor.execute( - () -> { - // There can be a race condition, where onPausedForConnectivity can be called - // after onComplete or onFailure which removes the future and the notification. - if (keyToListenableFuture.containsKey( - downloadRequest.destinationFileUri().toString())) { - notification - .setCategory(NotificationCompat.CATEGORY_STATUS) - .setContentText(NotificationUtil.getDownloadPausedMessage(context)) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setOngoing(true) - // hide progress bar. - .setProgress(0, 0, false); - notificationManager.notify(notificationKey, notification.build()); - } - - if (downloadRequest.listenerOptional().isPresent()) { - downloadRequest.listenerOptional().get().onPausedForConnectivity(); - } - }); + // TODO(b/229123693): return this future once DownloadListener has an async api. + ListenableFuture<?> unused = + PropagatedFutures.transformAsync( + foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()), + futureInProgress -> { + if (futureInProgress) { + notification + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setContentText(networkPausedMessage) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setOngoing(true) + // hide progress bar. + .setProgress(0, 0, false); + notificationManager.notify(notificationKey, notification.build()); + } + if (downloadRequest.listenerOptional().isPresent()) { + downloadRequest.listenerOptional().get().onPausedForConnectivity(); + } + return immediateVoidFuture(); + }, + sequentialControlExecutor); } @Override @@ -350,92 +389,154 @@ final class DownloaderImpl implements Downloader { ListenableFuture<Void> clientOnCompleteFuture = downloadRequest.listenerOptional().isPresent() ? downloadRequest.listenerOptional().get().onComplete() - : Futures.immediateVoidFuture(); + : immediateVoidFuture(); // Logic to shutdown Foreground Download Service after the client's provided onComplete // finished - clientOnCompleteFuture.addListener( - () -> { - // Clear the notification action. - notification.mActions.clear(); - - if (downloadRequest.showDownloadedNotification()) { - notification - .setCategory(NotificationCompat.CATEGORY_STATUS) - .setContentText(NotificationUtil.getDownloadSuccessMessage(context)) - .setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - // hide progress bar. - .setProgress(0, 0, false); - - notificationManager.notify(notificationKey, notification.build()); - } else { - NotificationUtil.cancelNotificationForKey( - context, downloadRequest.destinationFileUri().toString()); - } - - keyToListenableFuture.remove(downloadRequest.destinationFileUri().toString()); - // If there is no other on-going foreground download, shutdown the - // ForegroundDownloadService - if (keyToListenableFuture.isEmpty()) { - NotificationUtil.stopForegroundDownloadService( - context, foregroundDownloadServiceClassOptional.get()); - } + return PropagatedFluentFuture.from(clientOnCompleteFuture) + .transformAsync( + unused -> { + // onComplete succeeded, show a success message + notification.mActions.clear(); + + if (downloadRequest.showDownloadedNotification()) { + notification + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setContentText(NotificationUtil.getDownloadSuccessMessage(context)) + .setOngoing(false) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + // hide progress bar. + .setProgress(0, 0, false); + + notificationManager.notify(notificationKey, notification.build()); + } else { + NotificationUtil.cancelNotificationForKey( + context, foregroundDownloadKey.toString()); + } + return immediateVoidFuture(); + }, + sequentialControlExecutor) + .catchingAsync( + Exception.class, + e -> { + LogUtil.w( + e, + "%s: Delegate onComplete failed for uri: %s, showing failure notification.", + TAG, + downloadRequest.destinationFileUri()); + notification.mActions.clear(); + + if (downloadRequest.showDownloadedNotification()) { + notification + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setContentText(NotificationUtil.getDownloadFailedMessage(context)) + .setOngoing(false) + .setSmallIcon(android.R.drawable.stat_sys_warning) + // hide progress bar. + .setProgress(0, 0, false); + + notificationManager.notify(notificationKey, notification.build()); + } else { + NotificationUtil.cancelNotificationForKey( + context, downloadRequest.destinationFileUri().toString()); + } - downloadMonitorOptional - .get() - .removeDownloadListener(downloadRequest.destinationFileUri()); - }, - sequentialControlExecutor); - return clientOnCompleteFuture; + return immediateVoidFuture(); + }, + sequentialControlExecutor) + .transformAsync( + unused -> { + // After success or failure notification is shown, clean up + downloadMonitorOptional + .get() + .removeDownloadListener(downloadRequest.destinationFileUri()); + + return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString()); + }, + sequentialControlExecutor); } @Override public void onFailure(Throwable t) { - sequentialControlExecutor.execute( - () -> { - // Clear the notification action. - notification.mActions.clear(); - - // Show download failed in notification. - notification - .setCategory(NotificationCompat.CATEGORY_STATUS) - .setContentText(NotificationUtil.getDownloadFailedMessage(context)) - .setOngoing(false) - .setSmallIcon(android.R.drawable.stat_sys_warning) - // hide progress bar. - .setProgress(0, 0, false); - - notificationManager.notify(notificationKey, notification.build()); - - keyToListenableFuture.remove(downloadRequest.destinationFileUri().toString()); - - // If there is no other on-going foreground download, shutdown the - // ForegroundDownloadService - if (keyToListenableFuture.isEmpty()) { - NotificationUtil.stopForegroundDownloadService( - context, foregroundDownloadServiceClassOptional.get()); - } + // TODO(b/229123693): return this future once DownloadListener has an async api. + ListenableFuture<?> unused = + PropagatedFutures.submitAsync( + () -> { + // Clear the notification action. + notification.mActions.clear(); + + // Show download failed in notification. + notification + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setContentText(NotificationUtil.getDownloadFailedMessage(context)) + .setOngoing(false) + .setSmallIcon(android.R.drawable.stat_sys_warning) + // hide progress bar. + .setProgress(0, 0, false); + + notificationManager.notify(notificationKey, notification.build()); - if (downloadRequest.listenerOptional().isPresent()) { - downloadRequest.listenerOptional().get().onFailure(t); - } - downloadMonitorOptional - .get() - .removeDownloadListener(downloadRequest.destinationFileUri()); - }); + if (downloadRequest.listenerOptional().isPresent()) { + downloadRequest.listenerOptional().get().onFailure(t); + } + downloadMonitorOptional + .get() + .removeDownloadListener(downloadRequest.destinationFileUri()); + + return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString()); + }, + sequentialControlExecutor); } }; } @Override - public void cancelForegroundDownload(String destinationFileUri) { - LogUtil.d("%s: CancelForegroundDownload for Uri = %s", TAG, destinationFileUri); - sequentialControlExecutor.execute( - () -> { - if (keyToListenableFuture.containsKey(destinationFileUri)) { - keyToListenableFuture.get(destinationFileUri).cancel(true); - } - }); + public void cancelForegroundDownload(String downloadKey) { + LogUtil.d("%s: CancelForegroundDownload for Uri = %s", TAG, downloadKey); + ListenableFuture<?> unused = + PropagatedFutures.transformAsync( + getInProgressDownloadFuture(downloadKey), + downloadFuture -> { + if (downloadFuture.isPresent()) { + LogUtil.v( + "%s: CancelForegroundDownload future found for key = %s, cancelling...", + TAG, downloadKey); + downloadFuture.get().cancel(false); + } + return immediateVoidFuture(); + }, + sequentialControlExecutor); + } + + private ListenableFuture<Optional<ListenableFuture<Void>>> getInProgressDownloadFuture( + String key) { + return PropagatedFutures.transformAsync( + foregroundDownloadFutureMap.containsKey(key), + isInForeground -> + isInForeground ? foregroundDownloadFutureMap.get(key) : downloadFutureMap.get(key), + sequentialControlExecutor); + } + + private static DownloadFutureMap.StateChangeCallbacks createCallbacksForForegroundService( + Context context, Optional<Class<?>> foregroundDownloadServiceClassOptional) { + return new DownloadFutureMap.StateChangeCallbacks() { + @Override + public void onAdd(String key, int newSize) { + // Only start foreground service if this is the first future we are adding. + if (newSize == 1 && foregroundDownloadServiceClassOptional.isPresent()) { + NotificationUtil.startForegroundDownloadService( + context, foregroundDownloadServiceClassOptional.get(), key); + } + } + + @Override + public void onRemove(String key, int newSize) { + // Only stop foreground service if there are no more futures remaining. + if (newSize == 0 && foregroundDownloadServiceClassOptional.isPresent()) { + NotificationUtil.stopForegroundDownloadService( + context, foregroundDownloadServiceClassOptional.get(), key); + } + } + }; } } |