• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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