• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
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.exoplayer2.ext.cronet;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static org.junit.Assert.fail;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.ArgumentMatchers.anyString;
22 import static org.mockito.ArgumentMatchers.eq;
23 import static org.mockito.Mockito.doAnswer;
24 import static org.mockito.Mockito.doThrow;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.never;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import android.net.Uri;
32 import android.os.ConditionVariable;
33 import android.os.SystemClock;
34 import androidx.test.ext.junit.runners.AndroidJUnit4;
35 import com.google.android.exoplayer2.C;
36 import com.google.android.exoplayer2.upstream.DataSpec;
37 import com.google.android.exoplayer2.upstream.HttpDataSource;
38 import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
39 import com.google.android.exoplayer2.upstream.TransferListener;
40 import com.google.android.exoplayer2.util.Clock;
41 import com.google.android.exoplayer2.util.Util;
42 import java.io.IOException;
43 import java.net.SocketTimeoutException;
44 import java.net.UnknownHostException;
45 import java.nio.ByteBuffer;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.Map;
51 import java.util.concurrent.CountDownLatch;
52 import java.util.concurrent.Executor;
53 import java.util.concurrent.atomic.AtomicInteger;
54 import java.util.concurrent.atomic.AtomicReference;
55 import org.chromium.net.CronetEngine;
56 import org.chromium.net.NetworkException;
57 import org.chromium.net.UrlRequest;
58 import org.chromium.net.UrlResponseInfo;
59 import org.chromium.net.impl.UrlResponseInfoImpl;
60 import org.junit.Before;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 import org.mockito.ArgumentMatchers;
64 import org.mockito.Mock;
65 import org.mockito.MockitoAnnotations;
66 import org.robolectric.annotation.LooperMode;
67 import org.robolectric.annotation.LooperMode.Mode;
68 import org.robolectric.shadows.ShadowLooper;
69 
70 /** Tests for {@link CronetDataSource}. */
71 @RunWith(AndroidJUnit4.class)
72 @LooperMode(Mode.PAUSED)
73 public final class CronetDataSourceTest {
74 
75   private static final int TEST_CONNECT_TIMEOUT_MS = 100;
76   private static final int TEST_READ_TIMEOUT_MS = 100;
77   private static final String TEST_URL = "http://google.com";
78   private static final String TEST_CONTENT_TYPE = "test/test";
79   private static final byte[] TEST_POST_BODY = Util.getUtf8Bytes("test post body");
80   private static final long TEST_CONTENT_LENGTH = 16000L;
81   private static final int TEST_CONNECTION_STATUS = 5;
82   private static final int TEST_INVALID_CONNECTION_STATUS = -1;
83 
84   private DataSpec testDataSpec;
85   private DataSpec testPostDataSpec;
86   private DataSpec testHeadDataSpec;
87   private Map<String, String> testResponseHeader;
88   private UrlResponseInfo testUrlResponseInfo;
89 
90   @Mock private UrlRequest.Builder mockUrlRequestBuilder;
91   @Mock private UrlRequest mockUrlRequest;
92   @Mock private TransferListener mockTransferListener;
93   @Mock private Executor mockExecutor;
94   @Mock private NetworkException mockNetworkException;
95   @Mock private CronetEngine mockCronetEngine;
96 
97   private CronetDataSource dataSourceUnderTest;
98   private boolean redirectCalled;
99 
100   @Before
setUp()101   public void setUp() {
102     MockitoAnnotations.initMocks(this);
103 
104     HttpDataSource.RequestProperties defaultRequestProperties =
105         new HttpDataSource.RequestProperties();
106     defaultRequestProperties.set("defaultHeader1", "defaultValue1");
107     defaultRequestProperties.set("defaultHeader2", "defaultValue2");
108 
109     dataSourceUnderTest =
110         new CronetDataSource(
111             mockCronetEngine,
112             mockExecutor,
113             TEST_CONNECT_TIMEOUT_MS,
114             TEST_READ_TIMEOUT_MS,
115             /* resetTimeoutOnRedirects= */ true,
116             Clock.DEFAULT,
117             defaultRequestProperties,
118             /* handleSetCookieRequests= */ false);
119     dataSourceUnderTest.addTransferListener(mockTransferListener);
120     when(mockCronetEngine.newUrlRequestBuilder(
121             anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
122         .thenReturn(mockUrlRequestBuilder);
123     when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
124     when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
125     mockStatusResponse();
126 
127     testDataSpec = new DataSpec(Uri.parse(TEST_URL));
128     testPostDataSpec =
129         new DataSpec.Builder()
130             .setUri(TEST_URL)
131             .setHttpMethod(DataSpec.HTTP_METHOD_POST)
132             .setHttpBody(TEST_POST_BODY)
133             .build();
134     testHeadDataSpec =
135         new DataSpec.Builder().setUri(TEST_URL).setHttpMethod(DataSpec.HTTP_METHOD_HEAD).build();
136     testResponseHeader = new HashMap<>();
137     testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
138     // This value can be anything since the DataSpec is unset.
139     testResponseHeader.put("Content-Length", Long.toString(TEST_CONTENT_LENGTH));
140     testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
141   }
142 
createUrlResponseInfo(int statusCode)143   private UrlResponseInfo createUrlResponseInfo(int statusCode) {
144     return createUrlResponseInfoWithUrl(TEST_URL, statusCode);
145   }
146 
createUrlResponseInfoWithUrl(String url, int statusCode)147   private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) {
148     ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
149     responseHeaderList.addAll(testResponseHeader.entrySet());
150     return new UrlResponseInfoImpl(
151         Collections.singletonList(url),
152         statusCode,
153         null, // httpStatusText
154         responseHeaderList,
155         false, // wasCached
156         null, // negotiatedProtocol
157         null); // proxyServer
158   }
159 
160   @Test
openingTwiceThrows()161   public void openingTwiceThrows() throws HttpDataSourceException {
162     mockResponseStartSuccess();
163     dataSourceUnderTest.open(testDataSpec);
164     try {
165       dataSourceUnderTest.open(testDataSpec);
166       fail("Expected IllegalStateException.");
167     } catch (IllegalStateException e) {
168       // Expected.
169     }
170   }
171 
172   @Test
callbackFromPreviousRequest()173   public void callbackFromPreviousRequest() throws HttpDataSourceException {
174     mockResponseStartSuccess();
175 
176     dataSourceUnderTest.open(testDataSpec);
177     dataSourceUnderTest.close();
178     // Prepare a mock UrlRequest to be used in the second open() call.
179     final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
180     when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
181     doAnswer(
182             invocation -> {
183               // Invoke the callback for the previous request.
184               dataSourceUnderTest.urlRequestCallback.onFailed(
185                   mockUrlRequest, testUrlResponseInfo, mockNetworkException);
186               dataSourceUnderTest.urlRequestCallback.onResponseStarted(
187                   mockUrlRequest2, testUrlResponseInfo);
188               return null;
189             })
190         .when(mockUrlRequest2)
191         .start();
192     dataSourceUnderTest.open(testDataSpec);
193   }
194 
195   @Test
requestStartCalled()196   public void requestStartCalled() throws HttpDataSourceException {
197     mockResponseStartSuccess();
198 
199     dataSourceUnderTest.open(testDataSpec);
200     verify(mockCronetEngine)
201         .newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class));
202     verify(mockUrlRequest).start();
203   }
204 
205   @Test
requestSetsRangeHeader()206   public void requestSetsRangeHeader() throws HttpDataSourceException {
207     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
208     mockResponseStartSuccess();
209 
210     dataSourceUnderTest.open(testDataSpec);
211     // The header value to add is current position to current position + length - 1.
212     verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
213   }
214 
215   @Test
requestHeadersSet()216   public void requestHeadersSet() throws HttpDataSourceException {
217     Map<String, String> headersSet = new HashMap<>();
218     doAnswer(
219             (invocation) -> {
220               String key = invocation.getArgument(0);
221               String value = invocation.getArgument(1);
222               headersSet.put(key, value);
223               return null;
224             })
225         .when(mockUrlRequestBuilder)
226         .addHeader(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
227 
228     dataSourceUnderTest.setRequestProperty("defaultHeader2", "dataSourceOverridesDefault");
229     dataSourceUnderTest.setRequestProperty("dataSourceHeader1", "dataSourceValue1");
230     dataSourceUnderTest.setRequestProperty("dataSourceHeader2", "dataSourceValue2");
231 
232     Map<String, String> dataSpecRequestProperties = new HashMap<>();
233     dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
234     dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
235     dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
236 
237     testDataSpec =
238         new DataSpec.Builder()
239             .setUri(TEST_URL)
240             .setPosition(1000)
241             .setLength(5000)
242             .setHttpRequestHeaders(dataSpecRequestProperties)
243             .build();
244     mockResponseStartSuccess();
245 
246     dataSourceUnderTest.open(testDataSpec);
247 
248     assertThat(headersSet.get("defaultHeader1")).isEqualTo("defaultValue1");
249     assertThat(headersSet.get("defaultHeader2")).isEqualTo("dataSourceOverridesDefault");
250     assertThat(headersSet.get("defaultHeader3")).isEqualTo("dataSpecOverridesAll");
251     assertThat(headersSet.get("dataSourceHeader1")).isEqualTo("dataSourceValue1");
252     assertThat(headersSet.get("dataSourceHeader2")).isEqualTo("dataSpecOverridesDataSource");
253     assertThat(headersSet.get("dataSpecHeader1")).isEqualTo("dataSpecValue1");
254 
255     verify(mockUrlRequest).start();
256   }
257 
258   @Test
requestOpen()259   public void requestOpen() throws HttpDataSourceException {
260     mockResponseStartSuccess();
261     assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
262     verify(mockTransferListener)
263         .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
264   }
265 
266   @Test
requestOpenGzippedCompressedReturnsDataSpecLength()267   public void requestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException {
268     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000);
269     testResponseHeader.put("Content-Encoding", "gzip");
270     testResponseHeader.put("Content-Length", Long.toString(50L));
271     mockResponseStartSuccess();
272 
273     assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
274     verify(mockTransferListener)
275         .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
276   }
277 
278   @Test
requestOpenFail()279   public void requestOpenFail() {
280     mockResponseStartFailure();
281 
282     try {
283       dataSourceUnderTest.open(testDataSpec);
284       fail("HttpDataSource.HttpDataSourceException expected");
285     } catch (HttpDataSourceException e) {
286       // Check for connection not automatically closed.
287       assertThat(e.getCause() instanceof UnknownHostException).isFalse();
288       verify(mockUrlRequest, never()).cancel();
289       verify(mockTransferListener, never())
290           .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
291     }
292   }
293 
294   @Test
open_ifBodyIsSetWithoutContentTypeHeader_fails()295   public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
296     testDataSpec =
297         new DataSpec.Builder()
298             .setUri(TEST_URL)
299             .setHttpMethod(DataSpec.HTTP_METHOD_POST)
300             .setHttpBody(new byte[1024])
301             .setPosition(200)
302             .setLength(1024)
303             .setKey("key")
304             .build();
305 
306     try {
307       dataSourceUnderTest.open(testDataSpec);
308       fail();
309     } catch (IOException expected) {
310       // Expected
311     }
312   }
313 
314   @Test
requestOpenFailDueToDnsFailure()315   public void requestOpenFailDueToDnsFailure() {
316     mockResponseStartFailure();
317     when(mockNetworkException.getErrorCode())
318         .thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
319 
320     try {
321       dataSourceUnderTest.open(testDataSpec);
322       fail("HttpDataSource.HttpDataSourceException expected");
323     } catch (HttpDataSourceException e) {
324       // Check for connection not automatically closed.
325       assertThat(e.getCause() instanceof UnknownHostException).isTrue();
326       verify(mockUrlRequest, never()).cancel();
327       verify(mockTransferListener, never())
328           .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
329     }
330   }
331 
332   @Test
requestOpenValidatesStatusCode()333   public void requestOpenValidatesStatusCode() {
334     mockResponseStartSuccess();
335     testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
336 
337     try {
338       dataSourceUnderTest.open(testDataSpec);
339       fail("HttpDataSource.HttpDataSourceException expected");
340     } catch (HttpDataSourceException e) {
341       assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
342       // Check for connection not automatically closed.
343       verify(mockUrlRequest, never()).cancel();
344       verify(mockTransferListener, never())
345           .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
346     }
347   }
348 
349   @Test
requestOpenValidatesContentTypePredicate()350   public void requestOpenValidatesContentTypePredicate() {
351     mockResponseStartSuccess();
352 
353     ArrayList<String> testedContentTypes = new ArrayList<>();
354     dataSourceUnderTest.setContentTypePredicate(
355         (String input) -> {
356           testedContentTypes.add(input);
357           return false;
358         });
359 
360     try {
361       dataSourceUnderTest.open(testDataSpec);
362       fail("HttpDataSource.HttpDataSourceException expected");
363     } catch (HttpDataSourceException e) {
364       assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
365       // Check for connection not automatically closed.
366       verify(mockUrlRequest, never()).cancel();
367       assertThat(testedContentTypes).hasSize(1);
368       assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
369     }
370   }
371 
372   @Test
postRequestOpen()373   public void postRequestOpen() throws HttpDataSourceException {
374     mockResponseStartSuccess();
375 
376     dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
377     assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
378     verify(mockTransferListener)
379         .onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);
380   }
381 
382   @Test
postRequestOpenValidatesContentType()383   public void postRequestOpenValidatesContentType() {
384     mockResponseStartSuccess();
385 
386     try {
387       dataSourceUnderTest.open(testPostDataSpec);
388       fail("HttpDataSource.HttpDataSourceException expected");
389     } catch (HttpDataSourceException e) {
390       verify(mockUrlRequest, never()).start();
391     }
392   }
393 
394   @Test
postRequestOpenRejects307Redirects()395   public void postRequestOpenRejects307Redirects() {
396     mockResponseStartSuccess();
397     mockResponseStartRedirect();
398 
399     try {
400       dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
401       dataSourceUnderTest.open(testPostDataSpec);
402       fail("HttpDataSource.HttpDataSourceException expected");
403     } catch (HttpDataSourceException e) {
404       verify(mockUrlRequest, never()).followRedirect();
405     }
406   }
407 
408   @Test
headRequestOpen()409   public void headRequestOpen() throws HttpDataSourceException {
410     mockResponseStartSuccess();
411     dataSourceUnderTest.open(testHeadDataSpec);
412     verify(mockTransferListener)
413         .onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);
414     dataSourceUnderTest.close();
415   }
416 
417   @Test
requestReadTwice()418   public void requestReadTwice() throws HttpDataSourceException {
419     mockResponseStartSuccess();
420     mockReadSuccess(0, 16);
421 
422     dataSourceUnderTest.open(testDataSpec);
423 
424     byte[] returnedBuffer = new byte[8];
425     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
426     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
427     assertThat(bytesRead).isEqualTo(8);
428 
429     returnedBuffer = new byte[8];
430     bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
431     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));
432     assertThat(bytesRead).isEqualTo(8);
433 
434     // Should have only called read on cronet once.
435     verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
436     verify(mockTransferListener, times(2))
437         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
438   }
439 
440   @Test
secondRequestNoContentLength()441   public void secondRequestNoContentLength() throws HttpDataSourceException {
442     mockResponseStartSuccess();
443     testResponseHeader.put("Content-Length", Long.toString(1L));
444     mockReadSuccess(0, 16);
445 
446     // First request.
447     dataSourceUnderTest.open(testDataSpec);
448     byte[] returnedBuffer = new byte[8];
449     dataSourceUnderTest.read(returnedBuffer, 0, 1);
450     dataSourceUnderTest.close();
451 
452     testResponseHeader.remove("Content-Length");
453     mockReadSuccess(0, 16);
454 
455     // Second request.
456     dataSourceUnderTest.open(testDataSpec);
457     returnedBuffer = new byte[16];
458     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
459     assertThat(bytesRead).isEqualTo(10);
460     bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
461     assertThat(bytesRead).isEqualTo(6);
462     bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
463     assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
464   }
465 
466   @Test
readWithOffset()467   public void readWithOffset() throws HttpDataSourceException {
468     mockResponseStartSuccess();
469     mockReadSuccess(0, 16);
470 
471     dataSourceUnderTest.open(testDataSpec);
472 
473     byte[] returnedBuffer = new byte[16];
474     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
475     assertThat(bytesRead).isEqualTo(8);
476     assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
477     verify(mockTransferListener)
478         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
479   }
480 
481   @Test
rangeRequestWith206Response()482   public void rangeRequestWith206Response() throws HttpDataSourceException {
483     mockResponseStartSuccess();
484     mockReadSuccess(1000, 5000);
485     testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
486     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
487 
488     dataSourceUnderTest.open(testDataSpec);
489 
490     byte[] returnedBuffer = new byte[16];
491     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
492     assertThat(bytesRead).isEqualTo(16);
493     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
494     verify(mockTransferListener)
495         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
496   }
497 
498   @Test
rangeRequestWith200Response()499   public void rangeRequestWith200Response() throws HttpDataSourceException {
500     mockResponseStartSuccess();
501     mockReadSuccess(0, 7000);
502     testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
503     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
504 
505     dataSourceUnderTest.open(testDataSpec);
506 
507     byte[] returnedBuffer = new byte[16];
508     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
509     assertThat(bytesRead).isEqualTo(16);
510     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
511     verify(mockTransferListener)
512         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
513   }
514 
515   @Test
readWithUnsetLength()516   public void readWithUnsetLength() throws HttpDataSourceException {
517     testResponseHeader.remove("Content-Length");
518     mockResponseStartSuccess();
519     mockReadSuccess(0, 16);
520 
521     dataSourceUnderTest.open(testDataSpec);
522 
523     byte[] returnedBuffer = new byte[16];
524     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
525     assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
526     assertThat(bytesRead).isEqualTo(8);
527     verify(mockTransferListener)
528         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
529   }
530 
531   @Test
readReturnsWhatItCan()532   public void readReturnsWhatItCan() throws HttpDataSourceException {
533     mockResponseStartSuccess();
534     mockReadSuccess(0, 16);
535 
536     dataSourceUnderTest.open(testDataSpec);
537 
538     byte[] returnedBuffer = new byte[24];
539     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
540     assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
541     assertThat(bytesRead).isEqualTo(16);
542     verify(mockTransferListener)
543         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
544   }
545 
546   @Test
closedMeansClosed()547   public void closedMeansClosed() throws HttpDataSourceException {
548     mockResponseStartSuccess();
549     mockReadSuccess(0, 16);
550 
551     int bytesRead = 0;
552     dataSourceUnderTest.open(testDataSpec);
553 
554     byte[] returnedBuffer = new byte[8];
555     bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
556     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
557     assertThat(bytesRead).isEqualTo(8);
558 
559     dataSourceUnderTest.close();
560     verify(mockTransferListener)
561         .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
562 
563     try {
564       bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
565       fail();
566     } catch (IllegalStateException e) {
567       // Expected.
568     }
569 
570     // 16 bytes were attempted but only 8 should have been successfully read.
571     assertThat(bytesRead).isEqualTo(8);
572   }
573 
574   @Test
overread()575   public void overread() throws HttpDataSourceException {
576     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
577     testResponseHeader.put("Content-Length", Long.toString(16L));
578     mockResponseStartSuccess();
579     mockReadSuccess(0, 16);
580 
581     dataSourceUnderTest.open(testDataSpec);
582 
583     byte[] returnedBuffer = new byte[8];
584     int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
585     assertThat(bytesRead).isEqualTo(8);
586     assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
587 
588     // The current buffer is kept if not completely consumed by DataSource reader.
589     returnedBuffer = new byte[8];
590     bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
591     assertThat(bytesRead).isEqualTo(14);
592     assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));
593 
594     // 2 bytes left at this point.
595     returnedBuffer = new byte[8];
596     bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
597     assertThat(bytesRead).isEqualTo(16);
598     assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));
599 
600     // Should have only called read on cronet once.
601     verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
602     verify(mockTransferListener, times(1))
603         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
604     verify(mockTransferListener, times(1))
605         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
606     verify(mockTransferListener, times(1))
607         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
608 
609     // Now we already returned the 16 bytes initially asked.
610     // Try to read again even though all requested 16 bytes are already returned.
611     // Return C.RESULT_END_OF_INPUT
612     returnedBuffer = new byte[16];
613     int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
614     assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
615     assertThat(returnedBuffer).isEqualTo(new byte[16]);
616     // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
617     verify(mockTransferListener, never())
618         .onBytesTransferred(
619             dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
620     // There should still be only one call to read on cronet.
621     verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
622     // Check for connection not automatically closed.
623     verify(mockUrlRequest, never()).cancel();
624     assertThat(bytesRead).isEqualTo(16);
625   }
626 
627   @Test
requestReadByteBufferTwice()628   public void requestReadByteBufferTwice() throws HttpDataSourceException {
629     mockResponseStartSuccess();
630     mockReadSuccess(0, 16);
631 
632     dataSourceUnderTest.open(testDataSpec);
633 
634     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
635     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
636     assertThat(bytesRead).isEqualTo(8);
637     returnedBuffer.flip();
638     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
639 
640     // Use a wrapped ByteBuffer instead of direct for coverage.
641     returnedBuffer.rewind();
642     bytesRead = dataSourceUnderTest.read(returnedBuffer);
643     returnedBuffer.flip();
644     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
645     assertThat(bytesRead).isEqualTo(8);
646 
647     // Separate cronet calls for each read.
648     verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
649     verify(mockTransferListener, times(2))
650         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
651   }
652 
653   @Test
requestIntermixRead()654   public void requestIntermixRead() throws HttpDataSourceException {
655     mockResponseStartSuccess();
656     // Chunking reads into parts 6, 7, 8, 9.
657     mockReadSuccess(0, 30);
658 
659     dataSourceUnderTest.open(testDataSpec);
660 
661     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
662     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
663     returnedBuffer.flip();
664     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
665     assertThat(bytesRead).isEqualTo(6);
666 
667     byte[] returnedBytes = new byte[7];
668     bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
669     assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
670     assertThat(bytesRead).isEqualTo(6 + 7);
671 
672     returnedBuffer = ByteBuffer.allocateDirect(8);
673     bytesRead += dataSourceUnderTest.read(returnedBuffer);
674     returnedBuffer.flip();
675     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
676     assertThat(bytesRead).isEqualTo(6 + 7 + 8);
677 
678     returnedBytes = new byte[9];
679     bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
680     assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
681     assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
682 
683     // First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
684     verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
685     verify(mockTransferListener, times(1))
686         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
687     verify(mockTransferListener, times(1))
688         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
689     verify(mockTransferListener, times(1))
690         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
691     verify(mockTransferListener, times(1))
692         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
693   }
694 
695   @Test
secondRequestNoContentLengthReadByteBuffer()696   public void secondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
697     mockResponseStartSuccess();
698     testResponseHeader.put("Content-Length", Long.toString(1L));
699     mockReadSuccess(0, 16);
700 
701     // First request.
702     dataSourceUnderTest.open(testDataSpec);
703     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
704     dataSourceUnderTest.read(returnedBuffer);
705     dataSourceUnderTest.close();
706 
707     testResponseHeader.remove("Content-Length");
708     mockReadSuccess(0, 16);
709 
710     // Second request.
711     dataSourceUnderTest.open(testDataSpec);
712     returnedBuffer = ByteBuffer.allocateDirect(16);
713     returnedBuffer.limit(10);
714     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
715     assertThat(bytesRead).isEqualTo(10);
716     returnedBuffer.limit(returnedBuffer.capacity());
717     bytesRead = dataSourceUnderTest.read(returnedBuffer);
718     assertThat(bytesRead).isEqualTo(6);
719     returnedBuffer.rewind();
720     bytesRead = dataSourceUnderTest.read(returnedBuffer);
721     assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
722   }
723 
724   @Test
rangeRequestWith206ResponseReadByteBuffer()725   public void rangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
726     mockResponseStartSuccess();
727     mockReadSuccess(1000, 5000);
728     testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
729     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
730 
731     dataSourceUnderTest.open(testDataSpec);
732 
733     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
734     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
735     assertThat(bytesRead).isEqualTo(16);
736     returnedBuffer.flip();
737     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
738     verify(mockTransferListener)
739         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
740   }
741 
742   @Test
rangeRequestWith200ResponseReadByteBuffer()743   public void rangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
744     // Tests for skipping bytes.
745     mockResponseStartSuccess();
746     mockReadSuccess(0, 7000);
747     testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
748     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
749 
750     dataSourceUnderTest.open(testDataSpec);
751 
752     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
753     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
754     assertThat(bytesRead).isEqualTo(16);
755     returnedBuffer.flip();
756     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
757     verify(mockTransferListener)
758         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
759   }
760 
761   @Test
readByteBufferWithUnsetLength()762   public void readByteBufferWithUnsetLength() throws HttpDataSourceException {
763     testResponseHeader.remove("Content-Length");
764     mockResponseStartSuccess();
765     mockReadSuccess(0, 16);
766 
767     dataSourceUnderTest.open(testDataSpec);
768 
769     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
770     returnedBuffer.limit(8);
771     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
772     returnedBuffer.flip();
773     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
774     assertThat(bytesRead).isEqualTo(8);
775     verify(mockTransferListener)
776         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
777   }
778 
779   @Test
readByteBufferReturnsWhatItCan()780   public void readByteBufferReturnsWhatItCan() throws HttpDataSourceException {
781     mockResponseStartSuccess();
782     mockReadSuccess(0, 16);
783 
784     dataSourceUnderTest.open(testDataSpec);
785 
786     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
787     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
788     returnedBuffer.flip();
789     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
790     assertThat(bytesRead).isEqualTo(16);
791     verify(mockTransferListener)
792         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
793   }
794 
795   @Test
overreadByteBuffer()796   public void overreadByteBuffer() throws HttpDataSourceException {
797     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
798     testResponseHeader.put("Content-Length", Long.toString(16L));
799     mockResponseStartSuccess();
800     mockReadSuccess(0, 16);
801 
802     dataSourceUnderTest.open(testDataSpec);
803 
804     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
805     int bytesRead = dataSourceUnderTest.read(returnedBuffer);
806     assertThat(bytesRead).isEqualTo(8);
807     returnedBuffer.flip();
808     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
809 
810     // The current buffer is kept if not completely consumed by DataSource reader.
811     returnedBuffer = ByteBuffer.allocateDirect(6);
812     bytesRead += dataSourceUnderTest.read(returnedBuffer);
813     assertThat(bytesRead).isEqualTo(14);
814     returnedBuffer.flip();
815     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
816 
817     // 2 bytes left at this point.
818     returnedBuffer = ByteBuffer.allocateDirect(8);
819     bytesRead += dataSourceUnderTest.read(returnedBuffer);
820     assertThat(bytesRead).isEqualTo(16);
821     returnedBuffer.flip();
822     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
823 
824     // Called on each.
825     verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
826     verify(mockTransferListener, times(1))
827         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
828     verify(mockTransferListener, times(1))
829         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
830     verify(mockTransferListener, times(1))
831         .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
832 
833     // Now we already returned the 16 bytes initially asked.
834     // Try to read again even though all requested 16 bytes are already returned.
835     // Return C.RESULT_END_OF_INPUT
836     returnedBuffer = ByteBuffer.allocateDirect(16);
837     int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
838     assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
839     assertThat(returnedBuffer.position()).isEqualTo(0);
840     // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
841     verify(mockTransferListener, never())
842         .onBytesTransferred(
843             dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
844     // Number of calls to cronet should not have increased.
845     verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
846     // Check for connection not automatically closed.
847     verify(mockUrlRequest, never()).cancel();
848     assertThat(bytesRead).isEqualTo(16);
849   }
850 
851   @Test
closedMeansClosedReadByteBuffer()852   public void closedMeansClosedReadByteBuffer() throws HttpDataSourceException {
853     mockResponseStartSuccess();
854     mockReadSuccess(0, 16);
855 
856     int bytesRead = 0;
857     dataSourceUnderTest.open(testDataSpec);
858 
859     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
860     returnedBuffer.limit(8);
861     bytesRead += dataSourceUnderTest.read(returnedBuffer);
862     returnedBuffer.flip();
863     assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
864     assertThat(bytesRead).isEqualTo(8);
865 
866     dataSourceUnderTest.close();
867     verify(mockTransferListener)
868         .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
869 
870     try {
871       bytesRead += dataSourceUnderTest.read(returnedBuffer);
872       fail();
873     } catch (IllegalStateException e) {
874       // Expected.
875     }
876 
877     // 16 bytes were attempted but only 8 should have been successfully read.
878     assertThat(bytesRead).isEqualTo(8);
879   }
880 
881   @Test
connectTimeout()882   public void connectTimeout() throws InterruptedException {
883     long startTimeMs = SystemClock.elapsedRealtime();
884     final ConditionVariable startCondition = buildUrlRequestStartedCondition();
885     final CountDownLatch timedOutLatch = new CountDownLatch(1);
886 
887     new Thread() {
888       @Override
889       public void run() {
890         try {
891           dataSourceUnderTest.open(testDataSpec);
892           fail();
893         } catch (HttpDataSourceException e) {
894           // Expected.
895           assertThat(e instanceof CronetDataSource.OpenException).isTrue();
896           assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
897           assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
898               .isEqualTo(TEST_CONNECTION_STATUS);
899           timedOutLatch.countDown();
900         }
901       }
902     }.start();
903     startCondition.block();
904 
905     // We should still be trying to open.
906     assertNotCountedDown(timedOutLatch);
907     // We should still be trying to open as we approach the timeout.
908     setSystemClockInMsAndTriggerPendingMessages(
909         /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
910     assertNotCountedDown(timedOutLatch);
911     // Now we timeout.
912     setSystemClockInMsAndTriggerPendingMessages(
913         /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
914     timedOutLatch.await();
915 
916     verify(mockTransferListener, never())
917         .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
918   }
919 
920   @Test
connectInterrupted()921   public void connectInterrupted() throws InterruptedException {
922     long startTimeMs = SystemClock.elapsedRealtime();
923     final ConditionVariable startCondition = buildUrlRequestStartedCondition();
924     final CountDownLatch timedOutLatch = new CountDownLatch(1);
925 
926     Thread thread =
927         new Thread() {
928           @Override
929           public void run() {
930             try {
931               dataSourceUnderTest.open(testDataSpec);
932               fail();
933             } catch (HttpDataSourceException e) {
934               // Expected.
935               assertThat(e instanceof CronetDataSource.OpenException).isTrue();
936               assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
937               assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
938                   .isEqualTo(TEST_INVALID_CONNECTION_STATUS);
939               timedOutLatch.countDown();
940             }
941           }
942         };
943     thread.start();
944     startCondition.block();
945 
946     // We should still be trying to open.
947     assertNotCountedDown(timedOutLatch);
948     // We should still be trying to open as we approach the timeout.
949     setSystemClockInMsAndTriggerPendingMessages(
950         /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
951     assertNotCountedDown(timedOutLatch);
952     // Now we interrupt.
953     thread.interrupt();
954     timedOutLatch.await();
955 
956     verify(mockTransferListener, never())
957         .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
958   }
959 
960   @Test
connectResponseBeforeTimeout()961   public void connectResponseBeforeTimeout() throws Exception {
962     long startTimeMs = SystemClock.elapsedRealtime();
963     final ConditionVariable startCondition = buildUrlRequestStartedCondition();
964     final CountDownLatch openLatch = new CountDownLatch(1);
965 
966     AtomicReference<Exception> exceptionOnTestThread = new AtomicReference<>();
967     new Thread() {
968       @Override
969       public void run() {
970         try {
971           dataSourceUnderTest.open(testDataSpec);
972         } catch (HttpDataSourceException e) {
973           exceptionOnTestThread.set(e);
974         } finally {
975           openLatch.countDown();
976         }
977       }
978     }.start();
979     startCondition.block();
980 
981     // We should still be trying to open.
982     assertNotCountedDown(openLatch);
983     // We should still be trying to open as we approach the timeout.
984     setSystemClockInMsAndTriggerPendingMessages(
985         /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
986     assertNotCountedDown(openLatch);
987     // The response arrives just in time.
988     dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
989     openLatch.await();
990     assertThat(exceptionOnTestThread.get()).isNull();
991   }
992 
993   @Test
redirectIncreasesConnectionTimeout()994   public void redirectIncreasesConnectionTimeout() throws Exception {
995     long startTimeMs = SystemClock.elapsedRealtime();
996     final ConditionVariable startCondition = buildUrlRequestStartedCondition();
997     final CountDownLatch timedOutLatch = new CountDownLatch(1);
998     final AtomicInteger openExceptions = new AtomicInteger(0);
999 
1000     new Thread() {
1001       @Override
1002       public void run() {
1003         try {
1004           dataSourceUnderTest.open(testDataSpec);
1005           fail();
1006         } catch (HttpDataSourceException e) {
1007           // Expected.
1008           assertThat(e instanceof CronetDataSource.OpenException).isTrue();
1009           assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
1010           openExceptions.getAndIncrement();
1011           timedOutLatch.countDown();
1012         }
1013       }
1014     }.start();
1015     startCondition.block();
1016 
1017     // We should still be trying to open.
1018     assertNotCountedDown(timedOutLatch);
1019     // We should still be trying to open as we approach the timeout.
1020     setSystemClockInMsAndTriggerPendingMessages(
1021         /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
1022     assertNotCountedDown(timedOutLatch);
1023     // A redirect arrives just in time.
1024     dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
1025         mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
1026 
1027     long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
1028     setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
1029     // We should still be trying to open as we approach the new timeout.
1030     assertNotCountedDown(timedOutLatch);
1031     // A redirect arrives just in time.
1032     dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
1033         mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
1034 
1035     newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
1036     setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
1037     // We should still be trying to open as we approach the new timeout.
1038     assertNotCountedDown(timedOutLatch);
1039     // Now we timeout.
1040     setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs + 10);
1041     timedOutLatch.await();
1042 
1043     verify(mockTransferListener, never())
1044         .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
1045     assertThat(openExceptions.get()).isEqualTo(1);
1046   }
1047 
1048   @Test
redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()1049   public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
1050       throws HttpDataSourceException {
1051     mockSingleRedirectSuccess();
1052     mockFollowRedirectSuccess();
1053 
1054     testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
1055 
1056     dataSourceUnderTest.open(testDataSpec);
1057     verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
1058     verify(mockUrlRequest).followRedirect();
1059   }
1060 
1061   @Test
1062   public void
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()1063       testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
1064           throws HttpDataSourceException {
1065     dataSourceUnderTest =
1066         new CronetDataSource(
1067             mockCronetEngine,
1068             mockExecutor,
1069             TEST_CONNECT_TIMEOUT_MS,
1070             TEST_READ_TIMEOUT_MS,
1071             true, // resetTimeoutOnRedirects
1072             Clock.DEFAULT,
1073             null,
1074             true);
1075     dataSourceUnderTest.addTransferListener(mockTransferListener);
1076     dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
1077 
1078     mockSingleRedirectSuccess();
1079 
1080     testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
1081 
1082     dataSourceUnderTest.open(testDataSpec);
1083     verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
1084     verify(mockUrlRequestBuilder, never()).addHeader(eq("Range"), any(String.class));
1085     verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
1086     verify(mockUrlRequest, never()).followRedirect();
1087     verify(mockUrlRequest, times(2)).start();
1088   }
1089 
1090   @Test
1091   public void
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()1092       testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
1093           throws HttpDataSourceException {
1094     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
1095     dataSourceUnderTest =
1096         new CronetDataSource(
1097             mockCronetEngine,
1098             mockExecutor,
1099             TEST_CONNECT_TIMEOUT_MS,
1100             TEST_READ_TIMEOUT_MS,
1101             /* resetTimeoutOnRedirects= */ true,
1102             Clock.DEFAULT,
1103             /* defaultRequestProperties= */ null,
1104             /* handleSetCookieRequests= */ true);
1105     dataSourceUnderTest.addTransferListener(mockTransferListener);
1106     dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
1107 
1108     mockSingleRedirectSuccess();
1109 
1110     testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
1111 
1112     dataSourceUnderTest.open(testDataSpec);
1113     verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
1114     verify(mockUrlRequestBuilder, times(2)).addHeader("Range", "bytes=1000-5999");
1115     verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
1116     verify(mockUrlRequest, never()).followRedirect();
1117     verify(mockUrlRequest, times(2)).start();
1118   }
1119 
1120   @Test
redirectNoSetCookieFollowsRedirect()1121   public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
1122     mockSingleRedirectSuccess();
1123     mockFollowRedirectSuccess();
1124 
1125     dataSourceUnderTest.open(testDataSpec);
1126     verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
1127     verify(mockUrlRequest).followRedirect();
1128   }
1129 
1130   @Test
redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()1131   public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
1132       throws HttpDataSourceException {
1133     dataSourceUnderTest =
1134         new CronetDataSource(
1135             mockCronetEngine,
1136             mockExecutor,
1137             TEST_CONNECT_TIMEOUT_MS,
1138             TEST_READ_TIMEOUT_MS,
1139             /* resetTimeoutOnRedirects= */ true,
1140             Clock.DEFAULT,
1141             /* defaultRequestProperties= */ null,
1142             /* handleSetCookieRequests= */ true);
1143     dataSourceUnderTest.addTransferListener(mockTransferListener);
1144     mockSingleRedirectSuccess();
1145     mockFollowRedirectSuccess();
1146 
1147     dataSourceUnderTest.open(testDataSpec);
1148     verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
1149     verify(mockUrlRequest).followRedirect();
1150   }
1151 
1152   @Test
exceptionFromTransferListener()1153   public void exceptionFromTransferListener() throws HttpDataSourceException {
1154     mockResponseStartSuccess();
1155 
1156     // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
1157     // the subsequent open() call succeeds.
1158     doThrow(new NullPointerException())
1159         .when(mockTransferListener)
1160         .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
1161     dataSourceUnderTest.open(testDataSpec);
1162     try {
1163       dataSourceUnderTest.close();
1164       fail("NullPointerException expected");
1165     } catch (NullPointerException e) {
1166       // Expected.
1167     }
1168     // Open should return successfully.
1169     dataSourceUnderTest.open(testDataSpec);
1170   }
1171 
1172   @Test
readFailure()1173   public void readFailure() throws HttpDataSourceException {
1174     mockResponseStartSuccess();
1175     mockReadFailure();
1176 
1177     dataSourceUnderTest.open(testDataSpec);
1178     byte[] returnedBuffer = new byte[8];
1179     try {
1180       dataSourceUnderTest.read(returnedBuffer, 0, 8);
1181       fail("dataSourceUnderTest.read() returned, but IOException expected");
1182     } catch (IOException e) {
1183       // Expected.
1184     }
1185   }
1186 
1187   @Test
readByteBufferFailure()1188   public void readByteBufferFailure() throws HttpDataSourceException {
1189     mockResponseStartSuccess();
1190     mockReadFailure();
1191 
1192     dataSourceUnderTest.open(testDataSpec);
1193     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
1194     try {
1195       dataSourceUnderTest.read(returnedBuffer);
1196       fail("dataSourceUnderTest.read() returned, but IOException expected");
1197     } catch (IOException e) {
1198       // Expected.
1199     }
1200   }
1201 
1202   @Test
readNonDirectedByteBufferFailure()1203   public void readNonDirectedByteBufferFailure() throws HttpDataSourceException {
1204     mockResponseStartSuccess();
1205     mockReadFailure();
1206 
1207     dataSourceUnderTest.open(testDataSpec);
1208     byte[] returnedBuffer = new byte[8];
1209     try {
1210       dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
1211       fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
1212     } catch (IllegalArgumentException e) {
1213       // Expected.
1214     }
1215   }
1216 
1217   @Test
readInterrupted()1218   public void readInterrupted() throws HttpDataSourceException, InterruptedException {
1219     mockResponseStartSuccess();
1220     dataSourceUnderTest.open(testDataSpec);
1221 
1222     final ConditionVariable startCondition = buildReadStartedCondition();
1223     final CountDownLatch timedOutLatch = new CountDownLatch(1);
1224     byte[] returnedBuffer = new byte[8];
1225     Thread thread =
1226         new Thread() {
1227           @Override
1228           public void run() {
1229             try {
1230               dataSourceUnderTest.read(returnedBuffer, 0, 8);
1231               fail();
1232             } catch (HttpDataSourceException e) {
1233               // Expected.
1234               assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
1235               timedOutLatch.countDown();
1236             }
1237           }
1238         };
1239     thread.start();
1240     startCondition.block();
1241 
1242     assertNotCountedDown(timedOutLatch);
1243     // Now we interrupt.
1244     thread.interrupt();
1245     timedOutLatch.await();
1246   }
1247 
1248   @Test
readByteBufferInterrupted()1249   public void readByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
1250     mockResponseStartSuccess();
1251     dataSourceUnderTest.open(testDataSpec);
1252 
1253     final ConditionVariable startCondition = buildReadStartedCondition();
1254     final CountDownLatch timedOutLatch = new CountDownLatch(1);
1255     ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
1256     Thread thread =
1257         new Thread() {
1258           @Override
1259           public void run() {
1260             try {
1261               dataSourceUnderTest.read(returnedBuffer);
1262               fail();
1263             } catch (HttpDataSourceException e) {
1264               // Expected.
1265               assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
1266               timedOutLatch.countDown();
1267             }
1268           }
1269         };
1270     thread.start();
1271     startCondition.block();
1272 
1273     assertNotCountedDown(timedOutLatch);
1274     // Now we interrupt.
1275     thread.interrupt();
1276     timedOutLatch.await();
1277   }
1278 
1279   @Test
allowDirectExecutor()1280   public void allowDirectExecutor() throws HttpDataSourceException {
1281     testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
1282     mockResponseStartSuccess();
1283 
1284     dataSourceUnderTest.open(testDataSpec);
1285     verify(mockUrlRequestBuilder).allowDirectExecutor();
1286   }
1287 
1288   // Helper methods.
1289 
mockStatusResponse()1290   private void mockStatusResponse() {
1291     doAnswer(
1292             invocation -> {
1293               UrlRequest.StatusListener statusListener =
1294                   (UrlRequest.StatusListener) invocation.getArguments()[0];
1295               statusListener.onStatus(TEST_CONNECTION_STATUS);
1296               return null;
1297             })
1298         .when(mockUrlRequest)
1299         .getStatus(any(UrlRequest.StatusListener.class));
1300   }
1301 
mockResponseStartSuccess()1302   private void mockResponseStartSuccess() {
1303     doAnswer(
1304             invocation -> {
1305               dataSourceUnderTest.urlRequestCallback.onResponseStarted(
1306                   mockUrlRequest, testUrlResponseInfo);
1307               return null;
1308             })
1309         .when(mockUrlRequest)
1310         .start();
1311   }
1312 
mockResponseStartRedirect()1313   private void mockResponseStartRedirect() {
1314     doAnswer(
1315             invocation -> {
1316               dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
1317                   mockUrlRequest,
1318                   createUrlResponseInfo(307), // statusCode
1319                   "http://redirect.location.com");
1320               return null;
1321             })
1322         .when(mockUrlRequest)
1323         .start();
1324   }
1325 
mockSingleRedirectSuccess()1326   private void mockSingleRedirectSuccess() {
1327     doAnswer(
1328             invocation -> {
1329               if (!redirectCalled) {
1330                 redirectCalled = true;
1331                 dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
1332                     mockUrlRequest,
1333                     createUrlResponseInfoWithUrl("http://example.com/video", 300),
1334                     "http://example.com/video/redirect");
1335               } else {
1336                 dataSourceUnderTest.urlRequestCallback.onResponseStarted(
1337                     mockUrlRequest, testUrlResponseInfo);
1338               }
1339               return null;
1340             })
1341         .when(mockUrlRequest)
1342         .start();
1343   }
1344 
mockFollowRedirectSuccess()1345   private void mockFollowRedirectSuccess() {
1346     doAnswer(
1347             invocation -> {
1348               dataSourceUnderTest.urlRequestCallback.onResponseStarted(
1349                   mockUrlRequest, testUrlResponseInfo);
1350               return null;
1351             })
1352         .when(mockUrlRequest)
1353         .followRedirect();
1354   }
1355 
mockResponseStartFailure()1356   private void mockResponseStartFailure() {
1357     doAnswer(
1358             invocation -> {
1359               dataSourceUnderTest.urlRequestCallback.onFailed(
1360                   mockUrlRequest,
1361                   createUrlResponseInfo(500), // statusCode
1362                   mockNetworkException);
1363               return null;
1364             })
1365         .when(mockUrlRequest)
1366         .start();
1367   }
1368 
mockReadSuccess(int position, int length)1369   private void mockReadSuccess(int position, int length) {
1370     final int[] positionAndRemaining = new int[] {position, length};
1371     doAnswer(
1372             invocation -> {
1373               if (positionAndRemaining[1] == 0) {
1374                 dataSourceUnderTest.urlRequestCallback.onSucceeded(
1375                     mockUrlRequest, testUrlResponseInfo);
1376               } else {
1377                 ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
1378                 int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
1379                 inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
1380                 positionAndRemaining[0] += readLength;
1381                 positionAndRemaining[1] -= readLength;
1382                 dataSourceUnderTest.urlRequestCallback.onReadCompleted(
1383                     mockUrlRequest, testUrlResponseInfo, inputBuffer);
1384               }
1385               return null;
1386             })
1387         .when(mockUrlRequest)
1388         .read(any(ByteBuffer.class));
1389   }
1390 
mockReadFailure()1391   private void mockReadFailure() {
1392     doAnswer(
1393             invocation -> {
1394               dataSourceUnderTest.urlRequestCallback.onFailed(
1395                   mockUrlRequest,
1396                   createUrlResponseInfo(500), // statusCode
1397                   mockNetworkException);
1398               return null;
1399             })
1400         .when(mockUrlRequest)
1401         .read(any(ByteBuffer.class));
1402   }
1403 
buildReadStartedCondition()1404   private ConditionVariable buildReadStartedCondition() {
1405     final ConditionVariable startedCondition = new ConditionVariable();
1406     doAnswer(
1407             invocation -> {
1408               startedCondition.open();
1409               return null;
1410             })
1411         .when(mockUrlRequest)
1412         .read(any(ByteBuffer.class));
1413     return startedCondition;
1414   }
1415 
buildUrlRequestStartedCondition()1416   private ConditionVariable buildUrlRequestStartedCondition() {
1417     final ConditionVariable startedCondition = new ConditionVariable();
1418     doAnswer(
1419             invocation -> {
1420               startedCondition.open();
1421               return null;
1422             })
1423         .when(mockUrlRequest)
1424         .start();
1425     return startedCondition;
1426   }
1427 
assertNotCountedDown(CountDownLatch countDownLatch)1428   private void assertNotCountedDown(CountDownLatch countDownLatch) throws InterruptedException {
1429     // We are asserting that another thread does not count down the latch. We therefore sleep some
1430     // time to give the other thread the chance to fail this test.
1431     Thread.sleep(50);
1432     assertThat(countDownLatch.getCount()).isGreaterThan(0L);
1433   }
1434 
buildTestDataArray(int position, int length)1435   private static byte[] buildTestDataArray(int position, int length) {
1436     return buildTestDataBuffer(position, length).array();
1437   }
1438 
prefixZeros(byte[] data, int requiredLength)1439   public static byte[] prefixZeros(byte[] data, int requiredLength) {
1440     byte[] prefixedData = new byte[requiredLength];
1441     System.arraycopy(data, 0, prefixedData, requiredLength - data.length, data.length);
1442     return prefixedData;
1443   }
1444 
suffixZeros(byte[] data, int requiredLength)1445   public static byte[] suffixZeros(byte[] data, int requiredLength) {
1446     return Arrays.copyOf(data, requiredLength);
1447   }
1448 
buildTestDataBuffer(int position, int length)1449   private static ByteBuffer buildTestDataBuffer(int position, int length) {
1450     ByteBuffer testBuffer = ByteBuffer.allocate(length);
1451     for (int i = 0; i < length; i++) {
1452       testBuffer.put((byte) (position + i));
1453     }
1454     testBuffer.flip();
1455     return testBuffer;
1456   }
1457 
1458   // Returns a copy of what is remaining in the src buffer from the current position to capacity.
copyByteBufferToArray(ByteBuffer src)1459   private static byte[] copyByteBufferToArray(ByteBuffer src) {
1460     if (src == null) {
1461       return null;
1462     }
1463     byte[] copy = new byte[src.remaining()];
1464     int index = 0;
1465     while (src.hasRemaining()) {
1466       copy[index++] = src.get();
1467     }
1468     return copy;
1469   }
1470 
setSystemClockInMsAndTriggerPendingMessages(long nowMs)1471   private static void setSystemClockInMsAndTriggerPendingMessages(long nowMs) {
1472     SystemClock.setCurrentTimeMillis(nowMs);
1473     ShadowLooper.idleMainLooper();
1474   }
1475 }
1476