/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.mobiledatadownload; import static com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode.ANDROID_DOWNLOADER_UNKNOWN; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.net.Uri; import android.util.Log; import androidx.test.core.app.ApplicationProvider; import com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints; import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; import com.google.android.libraries.mobiledatadownload.foreground.ForegroundDownloadKey; import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor; import com.google.android.libraries.mobiledatadownload.monitor.NetworkUsageMonitor; import com.google.android.libraries.mobiledatadownload.testing.BlockingFileDownloader; import com.google.android.libraries.mobiledatadownload.testing.FakeTimeSource; import com.google.android.libraries.mobiledatadownload.testing.TestFlags; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.labs.concurrent.LabsFutures; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowLog; @RunWith(RobolectricTestRunner.class) public final class DownloadFileTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); // 1MB file. private static final String FILE_URL = "https://www.gstatic.com/icing/idd/sample_group/sample_file_3_1519240701"; private static final Uri DESTINATION_FILE_URI = Uri.parse( "android://com.google.android.libraries.mobiledatadownload/files/datadownload/shared/public/file_1"); private static final Context context = ApplicationProvider.getApplicationContext(); private final TestFlags flags = new TestFlags(); private final FakeTimeSource clock = new FakeTimeSource(); private DownloadProgressMonitor downloadProgressMonitor; private SynchronousFileStorage fileStorage; @Mock private SingleFileDownloadListener mockDownloadListener; @Mock private NetworkUsageMonitor mockNetworkUsageMonitor; @Mock private FileDownloader mockFileDownloader; @Mock private DownloadProgressMonitor mockDownloadMonitor; private BlockingFileDownloader blockingFileDownloader; private SingleFileDownloadRequest singleFileDownloadRequest; private MobileDataDownload mobileDataDownload; @Captor ArgumentCaptor downloadListenerCaptor; @Captor ArgumentCaptor liteDownloadListenerCaptor; @Captor ArgumentCaptor singleFileDownloadRequestCaptor; ListeningExecutorService controlExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); @Before public void setUp() { ShadowLog.setLoggable(LogUtil.TAG, Log.DEBUG); downloadProgressMonitor = new DownloadProgressMonitor(clock, controlExecutor); fileStorage = new SynchronousFileStorage( /* backends= */ ImmutableList.of(AndroidFileBackend.builder(context).build()), /* transforms= */ ImmutableList.of(), /* monitors= */ ImmutableList.of(downloadProgressMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setNotificationContentTitle("File url: " + FILE_URL) .build(); blockingFileDownloader = new BlockingFileDownloader(controlExecutor); when(mockDownloadListener.onComplete()).thenReturn(Futures.immediateVoidFuture()); when(mockFileDownloader.startDownloading(singleFileDownloadRequestCaptor.capture())) .thenReturn(Futures.immediateVoidFuture()); } @After public void tearDown() { // Reset state of blockingFileDownloader to prevent deadlocks blockingFileDownloader.resetState(); } @Test public void downloadFile_whenRequestAlreadyMade_dedups() throws Exception { // Use BlockingFileDownloader to ensure first download is in progress. mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ListenableFuture downloadFuture1 = mobileDataDownload.downloadFile(singleFileDownloadRequest); ListenableFuture downloadFuture2 = mobileDataDownload.downloadFile(singleFileDownloadRequest); // Allow blocking download to finish blockingFileDownloader.finishDownloading(); // Finish future 2 and assert that future 1 has completed as well downloadFuture2.get(); awaitAllExecutorsIdle(); assertThat(downloadFuture1.isDone()).isTrue(); } @Test public void downloadFile_whenRequestAlreadyMadeUsingForegroundService_dedups() throws Exception { // Use BlockingFileDownloader to ensure first download is in progress. mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ListenableFuture downloadFuture1 = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); ListenableFuture downloadFuture2 = mobileDataDownload.downloadFile(singleFileDownloadRequest); // Allow blocking download to finish blockingFileDownloader.finishDownloading(); // Finish future 2 and assert that future 1 has completed as well downloadFuture2.get(); awaitAllExecutorsIdle(); assertThat(downloadFuture1.isDone()).isTrue(); } @Test public void downloadFile_beginsDownload() throws Exception { mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, /* foregroundDownloadServiceClassOptional= */ Optional.absent(), Optional.of(mockDownloadMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(singleFileDownloadRequest); downloadFuture.get(); awaitAllExecutorsIdle(); // Verify that correct DownloadRequest is sent to underlying FileDownloader DownloadRequest actualDownloadRequest = singleFileDownloadRequestCaptor.getValue(); assertThat(actualDownloadRequest.fileUri()).isEqualTo(DESTINATION_FILE_URI); assertThat(actualDownloadRequest.urlToDownload()).isEqualTo(FILE_URL); assertThat(actualDownloadRequest.downloadConstraints()) .isEqualTo(DownloadConstraints.NETWORK_CONNECTED); // Verify that downloadMonitor adds the listener verify(mockDownloadMonitor).addDownloadListener(any(), liteDownloadListenerCaptor.capture()); verify(mockFileDownloader).startDownloading(any()); verify(mockDownloadMonitor).removeDownloadListener(DESTINATION_FILE_URI); verify(mockDownloadListener).onComplete(); // Ensure that given download listener is the same one passed to download monitor com.google.android.libraries.mobiledatadownload.lite.DownloadListener capturedDownloadListener = liteDownloadListenerCaptor.getValue(); DownloadException testException = DownloadException.builder().setDownloadResultCode(ANDROID_DOWNLOADER_UNKNOWN).build(); capturedDownloadListener.onProgress(10); capturedDownloadListener.onFailure(testException); capturedDownloadListener.onPausedForConnectivity(); verify(mockDownloadListener).onProgress(10); verify(mockDownloadListener).onFailure(testException); verify(mockDownloadListener).onPausedForConnectivity(); } @Test public void download_whenListenerProvided_handlesOnCompleteFailed() throws Exception { Exception failureException = new Exception("test failure"); when(mockDownloadListener.onComplete()) .thenReturn(Futures.immediateFailedFuture(failureException)); mobileDataDownload = getMobileDataDownload( createSuccessfulFileDownloaderSupplier(), /* foregroundDownloadServiceClassOptional= */ Optional.absent(), Optional.of(downloadProgressMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); mobileDataDownload.downloadFile(singleFileDownloadRequest).get(); awaitAllExecutorsIdle(); // Verify the DownloadListeners onComplete was invoked verify(mockDownloadListener).onComplete(); } @Test public void downloadFile_whenDownloadFails_reportsFailure() throws Exception { DownloadException downloadException = DownloadException.builder().setDownloadResultCode(ANDROID_DOWNLOADER_UNKNOWN).build(); mobileDataDownload = getMobileDataDownload( createFailingFileDownloaderSupplier(downloadException), /* foregroundDownloadServiceClassOptional= */ Optional.absent(), Optional.of(downloadProgressMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(singleFileDownloadRequest); assertThrows(ExecutionException.class, downloadFuture::get); DownloadException e = LabsFutures.getFailureCauseAs(downloadFuture, DownloadException.class); assertThat(e.getDownloadResultCode()).isEqualTo(ANDROID_DOWNLOADER_UNKNOWN); // Verify that DownloadListener.onFailure was invoked with failure verify(mockDownloadListener).onFailure(downloadException); verify(mockDownloadListener, times(0)).onComplete(); } @Test public void downloadFile_whenReturnedFutureIsCanceled_cancelsDownload() throws Exception { // Wrap mock around BlockingFileDownloader to simulate long download mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(singleFileDownloadRequest); // Wait for download to start and confirm download future is still running. blockingFileDownloader.waitForDownloadStarted(); assertThat(downloadFuture.isDone()).isFalse(); // Cancel download future. downloadFuture.cancel(true); // Check that future is now cancelled. assertThat(downloadFuture.isCancelled()).isTrue(); } @Test public void downloadFile_whenMonitorNotProvided_whenDownloadFails_reportsFailure() throws Exception { DownloadException downloadException = DownloadException.builder().setDownloadResultCode(ANDROID_DOWNLOADER_UNKNOWN).build(); mobileDataDownload = getMobileDataDownload(createFailingFileDownloaderSupplier(downloadException)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(singleFileDownloadRequest); assertThrows(ExecutionException.class, downloadFuture::get); DownloadException e = LabsFutures.getFailureCauseAs(downloadFuture, DownloadException.class); assertThat(e.getDownloadResultCode()).isEqualTo(ANDROID_DOWNLOADER_UNKNOWN); // Verify that DownloadListener.onFailure was invoked with failure verify(mockDownloadListener).onFailure(downloadException); verify(mockDownloadListener, times(0)).onComplete(); } @Test public void downloadFileWithWithForegroundService_requiresForegroundDownloadService() throws Exception { // Create downloader without providing foreground service mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, /* foregroundDownloadServiceClassOptional= */ Optional.absent(), Optional.of(downloadProgressMonitor)); // Without foreground service, download call should fail with IllegalStateException ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); ExecutionException e = assertThrows(ExecutionException.class, downloadFuture::get); assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); // Verify that underlying download is not started verify(mockFileDownloader, times(0)).startDownloading(any()); } @Test public void downloadFileWithForegroundService_requiresDownloadMonitor() throws Exception { // Create downloader without providing DownloadMonitor mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, Optional.of(this.getClass()), /* downloadProgressMonitorOptional= */ Optional.absent()); // Without monitor, download call should fail with IllegalStateException ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); ExecutionException e = assertThrows(ExecutionException.class, downloadFuture::get); assertThat(e).hasCauseThat().isInstanceOf(IllegalStateException.class); // Verify that underlying download is not started verify(mockFileDownloader, times(0)).startDownloading(any()); } @Test public void downloadFileWithForegroundService_whenRequestAlreadyMade_dedups() throws Exception { // Use BlockingFileDownloader to control when the download will finish. mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ListenableFuture downloadFuture1 = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); ListenableFuture downloadFuture2 = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); // Now we let the 2 futures downloadFuture1 downloadFuture2 to run by opening the latch. blockingFileDownloader.finishDownloading(); // Now finish future 2, future 1 should finish too and the cache clears the future. downloadFuture2.get(); awaitAllExecutorsIdle(); assertThat(downloadFuture1.isDone()).isTrue(); } @Test public void downloadFileWithForegroundService_whenRequestAlreadyMadeWithoutForegroundService_dedups() throws Exception { // Use BlockingFileDownloader to control when the download will finish. mobileDataDownload = getMobileDataDownload( () -> blockingFileDownloader, Optional.of(this.getClass()), Optional.of(downloadProgressMonitor)); ListenableFuture downloadFuture1 = mobileDataDownload.downloadFile(singleFileDownloadRequest); ListenableFuture downloadFuture2 = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); // Now we let the 2 futures downloadFuture1 downloadFuture2 to run by opening the latch. blockingFileDownloader.finishDownloading(); // Now finish future 2, future 1 should finish too and the cache clears the future. downloadFuture2.get(); awaitAllExecutorsIdle(); assertThat(downloadFuture1.isDone()).isTrue(); } @Test public void downloadFileWithForegroundService() throws Exception { when(mockFileDownloader.startDownloading(singleFileDownloadRequestCaptor.capture())) .thenReturn(Futures.immediateVoidFuture()); mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, Optional.of(this.getClass()), Optional.of(mockDownloadMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setNotificationContentTitle("File url: " + FILE_URL) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); downloadFuture.get(); awaitAllExecutorsIdle(); // Verify that the correct DownloadRequest is sent to underderlying FileDownloader. DownloadRequest actualDownloadRequest = singleFileDownloadRequestCaptor.getValue(); assertThat(actualDownloadRequest.fileUri()).isEqualTo(DESTINATION_FILE_URI); assertThat(actualDownloadRequest.urlToDownload()).isEqualTo(FILE_URL); assertThat(actualDownloadRequest.downloadConstraints()) .isEqualTo(DownloadConstraints.NETWORK_CONNECTED); // Verify that downloadMonitor will add a DownloadListener. verify(mockDownloadMonitor).addDownloadListener(any(), liteDownloadListenerCaptor.capture()); verify(mockFileDownloader).startDownloading(any()); verify(mockDownloadMonitor).removeDownloadListener(DESTINATION_FILE_URI); verify(mockDownloadListener).onComplete(); com.google.android.libraries.mobiledatadownload.lite.DownloadListener capturedListener = liteDownloadListenerCaptor.getValue(); // Now simulate other DownloadListener's callbacks: capturedListener.onProgress(10); capturedListener.onPausedForConnectivity(); DownloadException downloadException = DownloadException.builder().setDownloadResultCode(ANDROID_DOWNLOADER_UNKNOWN).build(); capturedListener.onFailure(downloadException); awaitAllExecutorsIdle(); verify(mockDownloadListener).onProgress(10); verify(mockDownloadListener).onPausedForConnectivity(); verify(mockDownloadListener).onFailure(downloadException); } @Test public void downloadFileWithForegroundService_clientOnCompleteFailed() throws Exception { Exception failureException = new Exception("test failure"); when(mockFileDownloader.startDownloading(singleFileDownloadRequestCaptor.capture())) .thenReturn(Futures.immediateVoidFuture()); mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, Optional.of(this.getClass()), Optional.of(downloadProgressMonitor)); // Client's provided DownloadListener.onComplete failed. when(mockDownloadListener.onComplete()) .thenReturn(Futures.immediateFailedFuture(failureException)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setNotificationContentTitle("File url: " + FILE_URL) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); downloadFuture.get(); awaitAllExecutorsIdle(); // Verify that the correct DownloadRequest is sent to underderlying FileDownloader. DownloadRequest actualDownloadRequest = singleFileDownloadRequestCaptor.getValue(); assertThat(actualDownloadRequest.fileUri()).isEqualTo(DESTINATION_FILE_URI); assertThat(actualDownloadRequest.urlToDownload()).isEqualTo(FILE_URL); assertThat(actualDownloadRequest.downloadConstraints()) .isEqualTo(DownloadConstraints.NETWORK_CONNECTED); verify(mockFileDownloader).startDownloading(any()); verify(mockDownloadListener).onComplete(); } @Test public void downloadFileWithForegroundService_failure() throws Exception { DownloadException downloadException = DownloadException.builder().setDownloadResultCode(ANDROID_DOWNLOADER_UNKNOWN).build(); when(mockFileDownloader.startDownloading(singleFileDownloadRequestCaptor.capture())) .thenReturn(Futures.immediateFailedFuture(downloadException)); mobileDataDownload = getMobileDataDownload( () -> mockFileDownloader, Optional.of(this.getClass()), Optional.of(downloadProgressMonitor)); singleFileDownloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(DESTINATION_FILE_URI) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setNotificationContentTitle("File url: " + FILE_URL) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); assertThrows(ExecutionException.class, downloadFuture::get); DownloadException e = LabsFutures.getFailureCauseAs(downloadFuture, DownloadException.class); assertThat(e.getDownloadResultCode()).isEqualTo(ANDROID_DOWNLOADER_UNKNOWN); awaitAllExecutorsIdle(); // Verify that the correct DownloadRequest is sent to underderlying FileDownloader. DownloadRequest actualDownloadRequest = singleFileDownloadRequestCaptor.getValue(); assertThat(actualDownloadRequest.fileUri()).isEqualTo(DESTINATION_FILE_URI); assertThat(actualDownloadRequest.urlToDownload()).isEqualTo(FILE_URL); assertThat(actualDownloadRequest.downloadConstraints()) .isEqualTo(DownloadConstraints.NETWORK_CONNECTED); // Since the download failed, onComplete will not be called but onFailure. verify(mockDownloadListener, times(0)).onComplete(); verify(mockDownloadListener).onFailure(downloadException); } @Test public void cancelDownloadFileWithForegroundService() throws Exception { // Use BlockingFileDownloader to control when the download will finish. mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ForegroundDownloadKey foregroundDownloadKey = ForegroundDownloadKey.ofSingleFile(DESTINATION_FILE_URI); ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); blockingFileDownloader.waitForDownloadStarted(); mobileDataDownload.cancelForegroundDownload(foregroundDownloadKey.toString()); awaitAllExecutorsIdle(); assertTrue(downloadFuture.isCancelled()); } @Test public void cancelListenableFuture() throws Exception { // Use BlockingFileDownloader to control when the download will finish. mobileDataDownload = getMobileDataDownload(() -> blockingFileDownloader); ListenableFuture downloadFuture = mobileDataDownload.downloadFileWithForegroundService(singleFileDownloadRequest); // Wait for download to start and confirm download future is still running. blockingFileDownloader.waitForDownloadStarted(); assertThat(downloadFuture.isDone()).isFalse(); // Cancel download future. downloadFuture.cancel(true); // Check that future is now cancelled. assertThat(downloadFuture.isCancelled()).isTrue(); } private Supplier createFailingFileDownloaderSupplier(Throwable throwable) { return Suppliers.ofInstance( new FileDownloader() { @Override public ListenableFuture startDownloading(DownloadRequest request) { return Futures.immediateFailedFuture(throwable); } }); } private Supplier createSuccessfulFileDownloaderSupplier() { return Suppliers.ofInstance( new FileDownloader() { @Override public ListenableFuture startDownloading(DownloadRequest request) { return Futures.immediateVoidFuture(); } }); } private MobileDataDownload getMobileDataDownload( Supplier fileDownloaderSupplier) { return getMobileDataDownload( fileDownloaderSupplier, Optional.of(this.getClass()), Optional.of(downloadProgressMonitor)); } private MobileDataDownload getMobileDataDownload( Supplier fileDownloaderSupplier, Optional> foregroundDownloadServiceClassOptional, Optional downloadProgressMonitorOptional) { return MobileDataDownloadBuilder.newBuilder() .setContext(context) .setControlExecutor(controlExecutor) .setFileDownloaderSupplier(fileDownloaderSupplier) .setFileStorage(fileStorage) .setDownloadMonitorOptional(downloadProgressMonitorOptional) .setNetworkUsageMonitor(mockNetworkUsageMonitor) .setForegroundDownloadServiceOptional(foregroundDownloadServiceClassOptional) .setFlagsOptional(Optional.of(flags)) .build(); } /** Waits long enough for async operations to finish running. */ // TODO(b/217551873): investigate ways to make this more robust private static void awaitAllExecutorsIdle() throws Exception { Thread.sleep(/* millis= */ 100); } }