/* * 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_HTTP_ERROR; import static com.google.android.libraries.mobiledatadownload.testing.MddTestDependencies.ExecutorType; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.Futures.immediateVoidFuture; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints; import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; import com.google.android.libraries.mobiledatadownload.downloader.offroad.dagger.downloader2.BaseFileDownloaderModule; import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri; import com.google.android.libraries.mobiledatadownload.file.integration.downloader.SharedPreferencesDownloadMetadata; 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.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import java.util.concurrent.CancellationException; 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.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @RunWith(TestParameterInjector.class) public class DownloadFileIntegrationTest { @Rule(order = 1) public final MockitoRule mocks = MockitoJUnit.rule(); private static final String TAG = "DownloadFileIntegrationTest"; private static final long TIMEOUT_MS = 3000; private static final int FILE_SIZE = 554; private static final String FILE_URL = "https://www.gstatic.com/suggest-dev/odws1_empty.jar"; private static final String DOES_NOT_EXIST_FILE_URL = "https://www.gstatic.com/non-existing/suggest-dev/not-exist.txt"; private static final ListeningScheduledExecutorService DOWNLOAD_EXECUTOR = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(2)); private static final Context context = ApplicationProvider.getApplicationContext(); private final Uri destinationFileUri = AndroidUri.builder(context).setModule("mdd").setRelativePath("file_1").build(); private final FakeTimeSource clock = new FakeTimeSource(); private final TestFlags flags = new TestFlags(); private MobileDataDownload mobileDataDownload; private DownloadProgressMonitor downloadProgressMonitor; private SynchronousFileStorage fileStorage; private Supplier fileDownloaderSupplier; private ListeningExecutorService controlExecutor; @Mock private SingleFileDownloadListener mockDownloadListener; @Mock private NetworkUsageMonitor mockNetworkUsageMonitor; @TestParameter ExecutorType controlExecutorType; @Before public void setUp() throws Exception { // Set a default behavior for the download listener. when(mockDownloadListener.onComplete()).thenReturn(immediateVoidFuture()); controlExecutor = controlExecutorType.executor(); downloadProgressMonitor = new DownloadProgressMonitor(clock, controlExecutor); fileStorage = new SynchronousFileStorage( /* backends= */ ImmutableList.of(AndroidFileBackend.builder(context).build()), /* transforms= */ ImmutableList.of(), /* monitors= */ ImmutableList.of(downloadProgressMonitor)); fileDownloaderSupplier = () -> BaseFileDownloaderModule.createOffroad2FileDownloader( context, DOWNLOAD_EXECUTOR, controlExecutor, fileStorage, new SharedPreferencesDownloadMetadata( context.getSharedPreferences("downloadmetadata", 0), controlExecutor), Optional.of(downloadProgressMonitor), /* urlEngineOptional= */ Optional.absent(), /* exceptionHandlerOptional= */ Optional.absent(), /* authTokenProviderOptional= */ Optional.absent(), // /* cookieJarSupplierOptional= */ Optional.absent(), /* trafficTag= */ Optional.absent(), flags); } @After public void tearDown() throws Exception { if (fileStorage.exists(destinationFileUri)) { fileStorage.deleteFile(destinationFileUri); } } @Test public void downloadFile_success() throws Exception { mobileDataDownload = builderForTest().build(); assertThat(fileStorage.exists(destinationFileUri)).isFalse(); SingleFileDownloadRequest downloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(destinationFileUri) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(downloadRequest); downloadFuture.get(); // Verify the file is downloaded. assertThat(fileStorage.exists(destinationFileUri)).isTrue(); assertThat(fileStorage.fileSize(destinationFileUri)).isEqualTo(FILE_SIZE); fileStorage.deleteFile(destinationFileUri); // Verify the downloadListener is called. // Sleep for 1 sec to wait for the Future's callback to finish. Thread.sleep(/* millis= */ 1000); verify(mockDownloadListener).onComplete(); } @Test public void downloadFile_failure() throws Exception { mobileDataDownload = builderForTest().build(); assertThat(fileStorage.exists(destinationFileUri)).isFalse(); // Trying to download doesn't exist URL. SingleFileDownloadRequest downloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(destinationFileUri) .setUrlToDownload(DOES_NOT_EXIST_FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .setListenerOptional(Optional.of(mockDownloadListener)) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(downloadRequest); ExecutionException ex = assertThrows(ExecutionException.class, downloadFuture::get); assertThat(ex).hasCauseThat().isInstanceOf(DownloadException.class); assertThat(((DownloadException) ex.getCause()).getDownloadResultCode()) .isEqualTo(ANDROID_DOWNLOADER_HTTP_ERROR); // Verify the file is downloaded. assertThat(fileStorage.exists(destinationFileUri)).isFalse(); // Verify the downloadListener is called. // Sleep for 1 sec to wait for the Future's callback to finish. Thread.sleep(/* millis= */ 1000); verify(mockDownloadListener).onFailure(any(DownloadException.class)); } @Test public void downloadFile_cancel() throws Exception { // Use a BlockingFileDownloader to ensure download remains in progress until it is cancelled. BlockingFileDownloader blockingFileDownloader = new BlockingFileDownloader(DOWNLOAD_EXECUTOR); mobileDataDownload = builderForTest().setFileDownloaderSupplier(() -> blockingFileDownloader).build(); SingleFileDownloadRequest downloadRequest = SingleFileDownloadRequest.newBuilder() .setDestinationFileUri(destinationFileUri) .setUrlToDownload(FILE_URL) .setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED) .build(); ListenableFuture downloadFuture = mobileDataDownload.downloadFile(downloadRequest); // Note: we could have a race condition here between when the FileDownloader.startDownloading() // is called and when we cancel our download with Future.cancel(). To prevent this, we first // wait until we have started downloading to ensure that it is in progress before we cancel. blockingFileDownloader.waitForDownloadStarted(); // Cancel the download downloadFuture.cancel(/* mayInterruptIfRunning= */ true); assertThrows(CancellationException.class, downloadFuture::get); // Cleanup blockingFileDownloader.resetState(); } /** * Returns MDD Builder with common dependencies set -- additional dependencies are added in each * test as needed. */ private MobileDataDownloadBuilder builderForTest() { return MobileDataDownloadBuilder.newBuilder() .setContext(context) .setControlExecutor(controlExecutor) .setFileDownloaderSupplier(fileDownloaderSupplier) .setFileStorage(fileStorage) .setDownloadMonitorOptional(Optional.of(downloadProgressMonitor)) .setNetworkUsageMonitor(mockNetworkUsageMonitor) .setFlagsOptional(Optional.of(flags)); } }