1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.downloader.offroad; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static com.google.common.util.concurrent.Futures.immediateFailedFuture; 20 import static com.google.common.util.concurrent.Futures.immediateFuture; 21 import static com.google.common.util.concurrent.Futures.immediateVoidFuture; 22 import static java.util.concurrent.TimeUnit.SECONDS; 23 import static org.junit.Assert.assertThrows; 24 25 import android.content.Context; 26 import android.net.Uri; 27 import android.util.Pair; 28 import androidx.test.core.app.ApplicationProvider; 29 import com.google.android.downloader.ConnectivityHandler; 30 import com.google.android.downloader.CookieJar; 31 import com.google.android.downloader.DownloadConstraints; 32 import com.google.android.downloader.DownloadConstraints.NetworkType; 33 import com.google.android.downloader.DownloadMetadata; 34 import com.google.android.downloader.Downloader; 35 import com.google.android.downloader.FloggerDownloaderLogger; 36 import com.google.android.downloader.OAuthTokenProvider; 37 import com.google.android.downloader.PlatformUrlEngine; 38 import com.google.android.downloader.contrib.InMemoryCookieJar; 39 import com.google.android.downloader.testing.TestUrlEngine; 40 import com.google.android.downloader.testing.TestUrlEngine.TestUrlRequest; 41 import com.google.android.libraries.mobiledatadownload.DownloadException; 42 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 43 import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; 44 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 45 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidFileBackend; 46 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend; 47 import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri; 48 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore; 49 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; 50 import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener; 51 import com.google.android.libraries.mobiledatadownload.testing.TestHttpServer; 52 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 53 import com.google.common.base.Optional; 54 import com.google.common.collect.ImmutableList; 55 import com.google.common.collect.Iterables; 56 import com.google.common.io.ByteStreams; 57 import com.google.common.net.HttpHeaders; 58 import com.google.common.util.concurrent.AsyncFunction; 59 import com.google.common.util.concurrent.ListenableFuture; 60 import com.google.common.util.concurrent.ListeningExecutorService; 61 import com.google.common.util.concurrent.MoreExecutors; 62 import com.google.devtools.build.runtime.RunfilesPaths; 63 import java.io.InputStream; 64 import java.io.OutputStream; 65 import java.net.URI; 66 import java.util.ArrayList; 67 import java.util.HashMap; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.concurrent.CountDownLatch; 71 import java.util.concurrent.ExecutionException; 72 import java.util.concurrent.Executors; 73 import java.util.concurrent.ScheduledExecutorService; 74 import org.junit.After; 75 import org.junit.Before; 76 import org.junit.Rule; 77 import org.junit.Test; 78 import org.junit.runner.RunWith; 79 import org.robolectric.RobolectricTestRunner; 80 81 /** 82 * Unit tests for {@link 83 * com.google.android.libraries.mobiledatadownload.downloader.offroad.Offroad2FileDownloader}. 84 */ 85 @RunWith(RobolectricTestRunner.class) 86 public class Offroad2FileDownloaderTest { 87 88 private static final int TRAFFIC_TAG = 1000; 89 private static final ScheduledExecutorService DOWNLOAD_EXECUTOR = 90 Executors.newScheduledThreadPool(2); 91 private static final ListeningExecutorService CONTROL_EXECUTOR = 92 MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); 93 94 private static final long MAX_CONNECTION_WAIT_SECS = 10L; 95 private static final int MAX_PLATFORM_ENGINE_TIMEOUT_MILLIS = 1000; 96 97 /** Endpoint that can be registered to TestHttpServer to serve a file that can be downloaded. */ 98 private static final String TEST_DATA_ENDPOINT = "/testfile"; 99 100 /** Path to the underlying test data that is the source of what TestHttpServer will serve. */ 101 private static final String TEST_DATA_PATH = 102 RunfilesPaths.resolve( 103 "third_party/java_src/android_libs/mobiledatadownload/javatests/com/google/android/libraries/mobiledatadownload/testdata/full_file.txt") 104 .toString(); 105 106 private static final String PARTIAL_TEST_DATA_PATH = 107 RunfilesPaths.resolve( 108 "third_party/java_src/android_libs/mobiledatadownload/javatests/com/google/android/libraries/mobiledatadownload/testdata/partial_file.txt") 109 .toString(); 110 111 private Context context; 112 113 private Uri.Builder testUrlPrefix; 114 private TestHttpServer testHttpServer; 115 116 private SynchronousFileStorage fileStorage; 117 private FakeConnectivityHandler fakeConnectivityHandler; 118 private FakeDownloadMetadataStore fakeDownloadMetadataStore; 119 private FakeOAuthTokenProvider fakeOAuthTokenProvider; 120 private FakeTrafficStatsTagger fakeTrafficStatsTagger; 121 private TestUrlEngine testUrlEngine; 122 private CookieJar cookieJar; 123 private Downloader downloader; 124 125 private Offroad2FileDownloader fileDownloader; 126 127 @Rule(order = 1) 128 public TemporaryUri tmpUri = new TemporaryUri(); 129 130 @Before setUp()131 public void setUp() throws Exception { 132 context = ApplicationProvider.getApplicationContext(); 133 fileStorage = 134 new SynchronousFileStorage( 135 /* backends= */ ImmutableList.of( 136 AndroidFileBackend.builder(context).build(), new JavaFileBackend()), 137 /* transforms= */ ImmutableList.of(), 138 /* monitors= */ ImmutableList.of()); 139 140 fakeDownloadMetadataStore = new FakeDownloadMetadataStore(); 141 142 fakeTrafficStatsTagger = new FakeTrafficStatsTagger(); 143 144 PlatformUrlEngine urlEngine = 145 new PlatformUrlEngine( 146 CONTROL_EXECUTOR, 147 MAX_PLATFORM_ENGINE_TIMEOUT_MILLIS, 148 MAX_PLATFORM_ENGINE_TIMEOUT_MILLIS, 149 /* followHttpRedirects= */ false, 150 fakeTrafficStatsTagger); 151 152 testUrlEngine = new TestUrlEngine(urlEngine); 153 154 fakeConnectivityHandler = new FakeConnectivityHandler(); 155 156 downloader = 157 new Downloader.Builder() 158 .withIOExecutor(CONTROL_EXECUTOR) 159 .withConnectivityHandler(fakeConnectivityHandler) 160 .withMaxConcurrentDownloads(2) 161 .withLogger(new FloggerDownloaderLogger()) 162 .addUrlEngine(ImmutableList.of("http", "https"), testUrlEngine) 163 .build(); 164 165 fakeOAuthTokenProvider = new FakeOAuthTokenProvider(); 166 167 cookieJar = new InMemoryCookieJar(); 168 169 fileDownloader = 170 new Offroad2FileDownloader( 171 downloader, 172 fileStorage, 173 DOWNLOAD_EXECUTOR, 174 fakeOAuthTokenProvider, 175 fakeDownloadMetadataStore, 176 ExceptionHandler.withDefaultHandling(), 177 Optional.of(() -> cookieJar), 178 Optional.absent()); 179 180 testHttpServer = new TestHttpServer(); 181 testUrlPrefix = testHttpServer.startServer(); 182 } 183 184 @After tearDown()185 public void tearDown() throws Exception { 186 testHttpServer.stopServer(); 187 fakeConnectivityHandler.reset(); 188 fakeDownloadMetadataStore.reset(); 189 fakeOAuthTokenProvider.reset(); 190 fakeTrafficStatsTagger.reset(); 191 } 192 193 @Test testStartDownloading_downloadConditionsNull_usesWifiOnly()194 public void testStartDownloading_downloadConditionsNull_usesWifiOnly() throws Exception { 195 Uri fileUri = tmpUri.newUri(); 196 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 197 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 198 199 // Setup custom handler to ensure expected constraints. 200 fakeConnectivityHandler.customHandler = 201 constraints -> { 202 assertThat(constraints.requireUnmeteredNetwork()).isTrue(); 203 assertThat(constraints.requiredNetworkTypes()) 204 .containsExactly(NetworkType.WIFI, NetworkType.ETHERNET, NetworkType.BLUETOOTH); 205 206 return immediateVoidFuture(); 207 }; 208 209 ListenableFuture<Void> downloadFuture = 210 fileDownloader.startDownloading( 211 DownloadRequest.newBuilder() 212 .setFileUri(fileUri) 213 .setUrlToDownload(urlToDownload) 214 .setDownloadConstraints( 215 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 216 .NONE) 217 .build()); 218 219 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 220 221 assertThat(fakeConnectivityHandler.invokedCustomHandler).isTrue(); 222 223 // Check DownloadMetadataStore calls 224 assertThat(fakeDownloadMetadataStore.upsertCalls).containsKey(fileUri); 225 assertThat(fakeDownloadMetadataStore.deleteCalls).contains(fileUri); 226 assertThat(fakeDownloadMetadataStore.read(fileUri).get(MAX_CONNECTION_WAIT_SECS, SECONDS)) 227 .isAbsent(); 228 } 229 230 @Test testStartDownloading_wifi()231 public void testStartDownloading_wifi() throws Exception { 232 Uri fileUri = tmpUri.newUri(); 233 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 234 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 235 236 // Setup custom handler to ensure expected constraints. 237 fakeConnectivityHandler.customHandler = 238 constraints -> { 239 assertThat(constraints.requireUnmeteredNetwork()).isTrue(); 240 assertThat(constraints.requiredNetworkTypes()) 241 .containsExactly(NetworkType.WIFI, NetworkType.ETHERNET, NetworkType.BLUETOOTH); 242 243 return immediateVoidFuture(); 244 }; 245 246 // Setup custom handler to add authorization token 247 fakeOAuthTokenProvider.customHandler = unused -> immediateFuture("TEST_TOKEN"); 248 249 ListenableFuture<Void> downloadFuture = 250 fileDownloader.startDownloading( 251 DownloadRequest.newBuilder() 252 .setFileUri(fileUri) 253 .setUrlToDownload(urlToDownload) 254 .setDownloadConstraints( 255 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 256 .NETWORK_UNMETERED) 257 .setTrafficTag(TRAFFIC_TAG) 258 .build()); 259 260 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 261 262 assertThat(testUrlEngine.storedRequests()).hasSize(1); 263 TestUrlRequest request = testUrlEngine.storedRequests().get(0); 264 assertThat(request.trafficTag()).isEqualTo(TRAFFIC_TAG); 265 assertThat(request.headers()).containsKey(HttpHeaders.AUTHORIZATION); 266 assertThat(request.headers()) 267 .valuesForKey(HttpHeaders.AUTHORIZATION) 268 .contains("Bearer TEST_TOKEN"); 269 270 assertThat(fakeConnectivityHandler.invokedCustomHandler).isTrue(); 271 assertThat(fakeOAuthTokenProvider.invokedCustomHandler).isTrue(); 272 assertThat(fakeTrafficStatsTagger.storedTrafficTags).contains(TRAFFIC_TAG); 273 } 274 275 @Test testStartDownloading_wifi_notSettingTrafficTag()276 public void testStartDownloading_wifi_notSettingTrafficTag() throws Exception { 277 Uri fileUri = tmpUri.newUri(); 278 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 279 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 280 281 ListenableFuture<Void> downloadFuture = 282 fileDownloader.startDownloading( 283 DownloadRequest.newBuilder() 284 .setFileUri(fileUri) 285 .setUrlToDownload(urlToDownload) 286 .setDownloadConstraints( 287 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 288 .NETWORK_UNMETERED) 289 .build()); 290 291 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 292 293 assertThat(testUrlEngine.storedRequests()).hasSize(1); 294 TestUrlRequest request = testUrlEngine.storedRequests().get(0); 295 assertThat(request.trafficTag()).isEqualTo(0); 296 297 assertThat(fakeTrafficStatsTagger.storedTrafficTags).doesNotContain(TRAFFIC_TAG); 298 } 299 300 @Test testStartDownloading_extraHttpHeaders()301 public void testStartDownloading_extraHttpHeaders() throws Exception { 302 Uri fileUri = tmpUri.newUri(); 303 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 304 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 305 306 ListenableFuture<Void> downloadFuture = 307 fileDownloader.startDownloading( 308 DownloadRequest.newBuilder() 309 .setFileUri(fileUri) 310 .setUrlToDownload(urlToDownload) 311 .setDownloadConstraints( 312 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 313 .NETWORK_UNMETERED) 314 .setTrafficTag(TRAFFIC_TAG) 315 .setExtraHttpHeaders( 316 ImmutableList.of( 317 Pair.create("user-agent", "mdd-downloader"), 318 Pair.create("other-header", "header-value"))) 319 .build()); 320 321 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 322 323 assertThat(testUrlEngine.storedRequests()).hasSize(1); 324 TestUrlRequest request = testUrlEngine.storedRequests().get(0); 325 assertThat(request.headers().keySet()).containsExactly("user-agent", "other-header"); 326 assertThat(request.headers()).valuesForKey("user-agent").contains("mdd-downloader"); 327 assertThat(request.headers()).valuesForKey("other-header").contains("header-value"); 328 } 329 330 @Test testStartDownloading_cellular()331 public void testStartDownloading_cellular() throws Exception { 332 Uri fileUri = tmpUri.newUri(); 333 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 334 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 335 336 // Setup custom handler to ensure expected constraints. 337 fakeConnectivityHandler.customHandler = 338 constraints -> { 339 assertThat(constraints).isEqualTo(DownloadConstraints.NETWORK_CONNECTED); 340 341 return immediateVoidFuture(); 342 }; 343 344 ListenableFuture<Void> downloadFuture = 345 fileDownloader.startDownloading( 346 DownloadRequest.newBuilder() 347 .setFileUri(fileUri) 348 .setUrlToDownload(urlToDownload) 349 .setDownloadConstraints( 350 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 351 .NETWORK_CONNECTED) 352 .build()); 353 354 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 355 356 assertThat(fakeConnectivityHandler.invokedCustomHandler).isTrue(); 357 } 358 359 @Test testStartDownloading_failed()360 public void testStartDownloading_failed() throws Exception { 361 Uri fileUri = tmpUri.newUri(); 362 363 // Simulate failure due to bad url; 364 ListenableFuture<Void> downloadFuture = 365 fileDownloader.startDownloading( 366 DownloadRequest.newBuilder() 367 .setFileUri(fileUri) 368 .setUrlToDownload("https://BADURL") 369 .setDownloadConstraints( 370 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 371 .NETWORK_UNMETERED) 372 .build()); 373 374 ExecutionException exception = assertThrows(ExecutionException.class, downloadFuture::get); 375 assertThat(exception).hasCauseThat().isInstanceOf(DownloadException.class); 376 377 DownloadException dex = (DownloadException) exception.getCause(); 378 assertThat(dex.getDownloadResultCode()).isEqualTo(DownloadResultCode.UNKNOWN_ERROR); 379 } 380 381 @Test testStartDownloading_whenPartialFile_whenMetadataNotPresent_getsFullFile()382 public void testStartDownloading_whenPartialFile_whenMetadataNotPresent_getsFullFile() 383 throws Exception { 384 Uri fileUri = tmpUri.newUri(); 385 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 386 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 387 388 // Write partial content to file but do _not_ write partial metadata. 389 try (InputStream inStream = 390 fileStorage.open( 391 Uri.parse("file://" + PARTIAL_TEST_DATA_PATH), ReadStreamOpener.create()); 392 OutputStream outStream = fileStorage.open(fileUri, WriteStreamOpener.create())) { 393 ByteStreams.copy(inStream, outStream); 394 } 395 396 ListenableFuture<Void> downloadFuture = 397 fileDownloader.startDownloading( 398 DownloadRequest.newBuilder() 399 .setFileUri(fileUri) 400 .setUrlToDownload(urlToDownload) 401 .setDownloadConstraints( 402 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 403 .NONE) 404 .build()); 405 406 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 407 408 // Check that full file is requested (no HTTP range headers) 409 assertThat(testUrlEngine.storedRequests().get(0).headers()) 410 .doesNotContainKey(HttpHeaders.RANGE); 411 assertThat(testUrlEngine.storedRequests().get(0).headers()) 412 .doesNotContainKey(HttpHeaders.IF_RANGE); 413 414 // Check DownloadMetadataStore calls 415 assertThat(fakeDownloadMetadataStore.readCalls).contains(fileUri); 416 assertThat(fakeDownloadMetadataStore.upsertCalls).containsKey(fileUri); 417 assertThat(fakeDownloadMetadataStore.deleteCalls).contains(fileUri); 418 assertThat(fakeDownloadMetadataStore.read(fileUri).get(MAX_CONNECTION_WAIT_SECS, SECONDS)) 419 .isAbsent(); 420 } 421 422 @Test testStartDownloading_whenPartialFile_whenMetadataPresent_reusesPartialFile()423 public void testStartDownloading_whenPartialFile_whenMetadataPresent_reusesPartialFile() 424 throws Exception { 425 Uri fileUri = tmpUri.newUri(); 426 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 427 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 428 429 // Write partial content to file. 430 try (InputStream inStream = 431 fileStorage.open( 432 Uri.parse("file://" + PARTIAL_TEST_DATA_PATH), ReadStreamOpener.create()); 433 OutputStream outStream = fileStorage.open(fileUri, WriteStreamOpener.create())) { 434 ByteStreams.copy(inStream, outStream); 435 } 436 437 // Write existing metadata to file. 438 fakeDownloadMetadataStore 439 .upsert(fileUri, DownloadMetadata.create("test", 0)) 440 .get(MAX_CONNECTION_WAIT_SECS, SECONDS); 441 442 ListenableFuture<Void> downloadFuture = 443 fileDownloader.startDownloading( 444 DownloadRequest.newBuilder() 445 .setFileUri(fileUri) 446 .setUrlToDownload(urlToDownload) 447 .setDownloadConstraints( 448 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 449 .NONE) 450 .build()); 451 452 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 453 454 // Check that full file is requested (no HTTP range headers) 455 assertThat(testUrlEngine.storedRequests().get(0).headers()).containsKey(HttpHeaders.RANGE); 456 assertThat(testUrlEngine.storedRequests().get(0).headers()).containsKey(HttpHeaders.IF_RANGE); 457 458 // Check DownloadMetadataStore calls 459 assertThat(fakeDownloadMetadataStore.readCalls).contains(fileUri); 460 assertThat(fakeDownloadMetadataStore.upsertCalls).containsKey(fileUri); 461 assertThat(fakeDownloadMetadataStore.deleteCalls).contains(fileUri); 462 assertThat(fakeDownloadMetadataStore.read(fileUri).get(MAX_CONNECTION_WAIT_SECS, SECONDS)) 463 .isAbsent(); 464 } 465 466 @Test testCancelDownload_notFinishedFuture()467 public void testCancelDownload_notFinishedFuture() throws Exception { 468 // Build a file uri so it's not created by TemporaryUri -- we can then make assertions on the 469 // existence of the file. 470 Uri fileUri = tmpUri.newUriBuilder().appendPath("unique").build(); 471 472 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 473 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 474 475 // Block download using connectivity check 476 CountDownLatch blockingLatch = new CountDownLatch(1); 477 fakeConnectivityHandler.customHandler = 478 unused -> 479 PropagatedFutures.submitAsync( 480 () -> { 481 blockingLatch.await(); 482 return immediateVoidFuture(); 483 }, 484 CONTROL_EXECUTOR); 485 486 ListenableFuture<Void> downloadFuture = 487 fileDownloader.startDownloading( 488 DownloadRequest.newBuilder() 489 .setFileUri(fileUri) 490 .setUrlToDownload(urlToDownload) 491 .setDownloadConstraints( 492 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 493 .NETWORK_UNMETERED) 494 .build()); 495 496 assertThat(downloadFuture.isDone()).isFalse(); 497 assertThat(fileStorage.exists(fileUri)).isFalse(); 498 499 downloadFuture.cancel(true); 500 501 assertThat(downloadFuture.isCancelled()).isTrue(); 502 assertThat(fileStorage.exists(fileUri)).isFalse(); 503 504 // count down latch to clean up test. 505 blockingLatch.countDown(); 506 } 507 508 @Test testCancelDownload_onAlreadySucceededFuture()509 public void testCancelDownload_onAlreadySucceededFuture() throws Exception { 510 // Build a file uri so it's not created by TemporaryUri -- we can then make assertions on the 511 // existence of the file. 512 Uri fileUri = tmpUri.getRootUriBuilder().appendPath("unique").build(); 513 String urlToDownload = testUrlPrefix.path(TEST_DATA_ENDPOINT).toString(); 514 testHttpServer.registerTextFile(TEST_DATA_ENDPOINT, TEST_DATA_PATH); 515 516 ListenableFuture<Void> downloadFuture = 517 fileDownloader.startDownloading( 518 DownloadRequest.newBuilder() 519 .setFileUri(fileUri) 520 .setUrlToDownload(urlToDownload) 521 .setDownloadConstraints( 522 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 523 .NETWORK_UNMETERED) 524 .build()); 525 526 downloadFuture.get(MAX_CONNECTION_WAIT_SECS, SECONDS); 527 528 // Assert that on device file is created and remains even after cancel. 529 assertThat(fileStorage.exists(fileUri)).isTrue(); 530 531 downloadFuture.cancel(true); 532 533 assertThat(fileStorage.exists(fileUri)).isTrue(); 534 } 535 536 @Test testCancelDownload_onAlreadyFailedFuture()537 public void testCancelDownload_onAlreadyFailedFuture() throws Exception { 538 // Build a file uri so it's not created by TemporaryUri -- we can then make assertions on the 539 // existence of the file. 540 Uri fileUri = tmpUri.getRootUriBuilder().appendPath("unique").build(); 541 542 // Simulate failure due to bad url; 543 ListenableFuture<Void> downloadFuture = 544 fileDownloader.startDownloading( 545 DownloadRequest.newBuilder() 546 .setFileUri(fileUri) 547 .setUrlToDownload("https://BADURL") 548 .setDownloadConstraints( 549 com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 550 .NETWORK_UNMETERED) 551 .build()); 552 553 Exception unused = assertThrows(ExecutionException.class, downloadFuture::get); 554 555 // Assert that on device file is not created and doesn't get created after cancel. 556 assertThat(fileStorage.exists(fileUri)).isFalse(); 557 558 downloadFuture.cancel(true); 559 560 assertThat(fileStorage.exists(fileUri)).isFalse(); 561 } 562 563 /** Custom {@link ConnectivityHandler} that allows custom logic to be used for each test. */ 564 static final class FakeConnectivityHandler implements ConnectivityHandler { 565 private static final AsyncFunction<DownloadConstraints, Void> DEFAULT_HANDLER = 566 unused -> immediateVoidFuture(); 567 568 private AsyncFunction<DownloadConstraints, Void> customHandler = DEFAULT_HANDLER; 569 570 private boolean invokedCustomHandler = false; 571 572 @Override checkConnectivity(DownloadConstraints constraints)573 public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) { 574 ListenableFuture<Void> returnFuture; 575 try { 576 returnFuture = customHandler.apply(constraints); 577 } catch (Exception e) { 578 returnFuture = immediateFailedFuture(e); 579 } 580 581 invokedCustomHandler = true; 582 return returnFuture; 583 } 584 invokedCustomHandler()585 public boolean invokedCustomHandler() { 586 return invokedCustomHandler; 587 } 588 589 /** 590 * Reset inner state to initial values. 591 * 592 * <p>This prevents failures caused by cross test pollution. 593 */ reset()594 public void reset() { 595 customHandler = DEFAULT_HANDLER; 596 invokedCustomHandler = false; 597 } 598 } 599 600 /** Custom {@link OAuthTokenProvider} that allows custom logic for each test. */ 601 static final class FakeOAuthTokenProvider implements OAuthTokenProvider { 602 private static final AsyncFunction<URI, String> DEFAULT_HANDLER = 603 unused -> immediateFuture(null); 604 605 private AsyncFunction<URI, String> customHandler = DEFAULT_HANDLER; 606 607 private boolean invokedCustomHandler = false; 608 609 @Override provideOAuthToken(URI uri)610 public ListenableFuture<String> provideOAuthToken(URI uri) { 611 ListenableFuture<String> returnFuture; 612 try { 613 returnFuture = customHandler.apply(uri); 614 } catch (Exception e) { 615 returnFuture = immediateFailedFuture(e); 616 } 617 618 invokedCustomHandler = true; 619 return returnFuture; 620 } 621 622 /** 623 * Reset inner state to initial values. 624 * 625 * <p>This prevents failures caused by cross test pollution. 626 */ reset()627 public void reset() { 628 customHandler = DEFAULT_HANDLER; 629 invokedCustomHandler = false; 630 } 631 } 632 633 private static final class FakeTrafficStatsTagger 634 implements PlatformUrlEngine.TrafficStatsTagger { 635 private final List<Integer> storedTrafficTags = new ArrayList<>(); 636 637 @Override getAndSetThreadStatsTag(int tag)638 public int getAndSetThreadStatsTag(int tag) { 639 int prevTag = storedTrafficTags.isEmpty() ? 0 : Iterables.getLast(storedTrafficTags); 640 storedTrafficTags.add(tag); 641 return prevTag; 642 } 643 644 @Override restoreThreadStatsTag(int tag)645 public void restoreThreadStatsTag(int tag) { 646 storedTrafficTags.add(tag); 647 } 648 reset()649 public void reset() { 650 storedTrafficTags.clear(); 651 } 652 } 653 654 private static final class FakeDownloadMetadataStore implements DownloadMetadataStore { 655 656 // Backing storage structure for metadata. 657 private final Map<Uri, DownloadMetadata> storedMetadata = new HashMap<>(); 658 659 // Tracking of what calls are made on this fake. 660 final List<Uri> readCalls = new ArrayList<>(); 661 final Map<Uri, List<DownloadMetadata>> upsertCalls = new HashMap<>(); 662 final List<Uri> deleteCalls = new ArrayList<>(); 663 664 @Override read(Uri uri)665 public ListenableFuture<Optional<DownloadMetadata>> read(Uri uri) { 666 readCalls.add(uri); 667 668 return immediateFuture(Optional.fromNullable(storedMetadata.get(uri))); 669 } 670 671 @Override upsert(Uri uri, DownloadMetadata downloadMetadata)672 public ListenableFuture<Void> upsert(Uri uri, DownloadMetadata downloadMetadata) { 673 upsertCalls.putIfAbsent(uri, new ArrayList<>()); 674 upsertCalls.get(uri).add(downloadMetadata); 675 676 storedMetadata.put(uri, downloadMetadata); 677 return immediateVoidFuture(); 678 } 679 680 @Override delete(Uri uri)681 public ListenableFuture<Void> delete(Uri uri) { 682 deleteCalls.add(uri); 683 684 storedMetadata.remove(uri); 685 return immediateVoidFuture(); 686 } 687 reset()688 public void reset() { 689 storedMetadata.clear(); 690 691 readCalls.clear(); 692 upsertCalls.clear(); 693 deleteCalls.clear(); 694 } 695 } 696 } 697