Make sure Cronet extension/tests are pushed to GitHub

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=131825185
This commit is contained in:
olly 2016-08-31 07:07:39 -07:00 committed by Oliver Woodman
parent 50527c0a7d
commit 9adce6b007
7 changed files with 1691 additions and 0 deletions

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.cronet">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
<application android:debuggable="true"
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.exoplayer.ext.cronet"
tools:replace="android:targetPackage"/>
</manifest>

View file

@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.chromium.net.UploadDataSink;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
/**
* Tests for {@link ByteArrayUploadDataProvider}.
*/
@RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
@Mock private UploadDataSink mockUploadDataSink;
private ByteBuffer byteBuffer;
private ByteArrayUploadDataProvider byteArrayUploadDataProvider;
@Before
public void setUp() {
System.setProperty("dexmaker.dexcache",
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
}
@Test
public void testGetLength() {
assertEquals(TEST_DATA.length, byteArrayUploadDataProvider.getLength());
}
@Test
public void testReadFullBuffer() throws IOException {
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
}
@TargetApi(VERSION_CODES.GINGERBREAD)
@Test
public void testReadPartialBuffer() throws IOException {
byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2);
byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
// Read half of the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(firstHalf, byteBuffer.array());
// Read the second half of the data.
byteBuffer.rewind();
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(secondHalf, byteBuffer.array());
verify(mockUploadDataSink, times(2)).onReadSucceeded(false);
}
@Test
public void testRewind() throws IOException {
// Read all the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
// Rewind and make sure it can be read again.
byteBuffer.clear();
byteArrayUploadDataProvider.rewind(mockUploadDataSink);
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
verify(mockUploadDataSink).onRewindSucceeded();
}
}

View file

@ -0,0 +1,840 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri;
import android.os.ConditionVariable;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Predicate;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.chromium.net.CronetEngine;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests for {@link CronetDataSource}.
*/
@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
private static final int TEST_READ_TIMEOUT_MS = 50;
private static final String TEST_URL = "http://google.com";
private static final String TEST_CONTENT_TYPE = "test/test";
private static final byte[] TEST_POST_BODY = "test post body".getBytes();
private static final long TEST_CONTENT_LENGTH = 16000L;
private static final int TEST_BUFFER_SIZE = 16;
private static final int TEST_CONNECTION_STATUS = 5;
private DataSpec testDataSpec;
private DataSpec testPostDataSpec;
private Map<String, String> testResponseHeader;
private UrlResponseInfo testUrlResponseInfo;
/**
* MockableCronetEngine is an abstract class for helping creating new Requests.
*/
public abstract static class MockableCronetEngine extends CronetEngine {
@Override
public abstract UrlRequest createRequest(String url, UrlRequest.Callback callback,
Executor executor, int priority,
Collection<Object> connectionAnnotations,
boolean disableCache,
boolean disableConnectionMigration);
}
@Mock
private UrlRequest mockUrlRequest;
@Mock
private Predicate<String> mockContentTypePredicate;
@Mock
private TransferListener mockTransferListener;
@Mock
private Clock mockClock;
@Mock
private Executor mockExecutor;
@Mock
private UrlRequestException mockUrlRequestException;
@Mock
private MockableCronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest;
@Before
public void setUp() throws Exception {
System.setProperty("dexmaker.dexcache",
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
dataSourceUnderTest = spy(
new CronetDataSource(
mockCronetEngine,
mockExecutor,
mockContentTypePredicate,
mockTransferListener,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
mockClock));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.createRequest(
anyString(),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest);
mockStatusResponse();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
testPostDataSpec = new DataSpec(
Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
testResponseHeader = new HashMap<>();
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
// This value can be anything since the DataSpec is unset.
testResponseHeader.put("Content-Length", Long.toString(TEST_CONTENT_LENGTH));
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
}
private UrlResponseInfo createUrlResponseInfo(int statusCode) {
ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
responseHeaderList.addAll(testResponseHeader.entrySet());
return new UrlResponseInfo(
Collections.singletonList(TEST_URL),
statusCode,
null, // httpStatusText
responseHeaderList,
false, // wasCached
null, // negotiatedProtocol
null); // proxyServer
}
@Test(expected = IllegalStateException.class)
public void testOpeningTwiceThrows() throws HttpDataSourceException, IllegalStateException {
mockResponesStartSuccess();
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
dataSourceUnderTest.open(testDataSpec);
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
dataSourceUnderTest.open(testDataSpec);
}
@Test
public void testCallbackFromPreviousRequest() throws HttpDataSourceException {
mockResponesStartSuccess();
dataSourceUnderTest.open(testDataSpec);
dataSourceUnderTest.close();
// Prepare a mock UrlRequest to be used in the second open() call.
final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
when(mockCronetEngine.createRequest(
anyString(),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest2);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
// Invoke the callback for the previous request.
dataSourceUnderTest.onFailed(
mockUrlRequest,
testUrlResponseInfo,
null);
dataSourceUnderTest.onResponseStarted(
mockUrlRequest2,
testUrlResponseInfo);
return null;
}
}).when(mockUrlRequest2).start();
dataSourceUnderTest.open(testDataSpec);
}
@Test
public void testRequestStartCalled() throws HttpDataSourceException {
mockResponesStartSuccess();
dataSourceUnderTest.open(testDataSpec);
verify(mockCronetEngine).createRequest(
eq(TEST_URL),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class));
verify(mockUrlRequest).start();
}
@Test
public void testRequestHeadersSet() throws HttpDataSourceException {
mockResponesStartSuccess();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
testResponseHeader.put("Content-Length", Long.toString(5000L));
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1.
verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999");
verify(mockUrlRequest).addHeader("firstHeader", "firstValue");
verify(mockUrlRequest).addHeader("secondHeader", "secondValue");
verify(mockUrlRequest).start();
}
@Test
public void testRequestOpen() throws HttpDataSourceException {
mockResponesStartSuccess();
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@Test
public void testRequestOpenFail() {
mockResponseStartFailure();
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
assertFalse(e.getCause() instanceof UnknownHostException);
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
}
@Test
public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure();
when(mockUrlRequestException.getErrorCode()).thenReturn(
UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED);
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
assertTrue(e.getCause() instanceof UnknownHostException);
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
}
@Test
public void testRequestOpenValidatesStatusCode() {
mockResponesStartSuccess();
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException);
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
}
@Test
public void testRequestOpenValidatesContentTypePredicate() {
mockResponesStartSuccess();
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
assertTrue(e instanceof HttpDataSource.InvalidContentTypeException);
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
}
}
@Test
public void testRequestOpenValidatesContentLength() {
mockResponesStartSuccess();
// Data spec's requested length, 5000. Test response's length, 16,000.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999");
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testPostDataSpec);
}
}
@Test
public void testPostRequestOpen() throws HttpDataSourceException {
mockResponesStartSuccess();
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
}
@Test
public void testPostRequestOpenValidatesContentType() {
mockResponesStartSuccess();
try {
dataSourceUnderTest.open(testPostDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
verify(mockUrlRequest, never()).start();
}
}
@Test
public void testPostRequestOpenRejects307Redirects() {
mockResponesStartSuccess();
mockResponseStartRedirect();
try {
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
dataSourceUnderTest.open(testPostDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
verify(mockUrlRequest, never()).followRedirect();
}
}
@Test
public void testRequestReadTwice() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertEquals(8, bytesRead);
returnedBuffer = new byte[8];
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(8, 8), returnedBuffer);
assertEquals(8, bytesRead);
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
verify(mockTransferListener, times(2)).onBytesTransferred(dataSourceUnderTest, 8);
}
@Test
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
byte[] returnedBuffer = new byte[8];
// First request.
testResponseHeader.put("Content-Length", Long.toString(1L));
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
dataSourceUnderTest.open(testDataSpec);
dataSourceUnderTest.read(returnedBuffer, 0, 1);
dataSourceUnderTest.close();
// Second request. There's no Content-Length response header.
testResponseHeader.remove("Content-Length");
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
dataSourceUnderTest.open(testDataSpec);
returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(10, bytesRead);
mockResponseFinished();
// Should read whats left in the buffer first.
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(6, bytesRead);
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(C.RESULT_END_OF_INPUT, bytesRead);
}
@Test
public void testReadWithOffset() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
assertEquals(8, bytesRead);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@Test
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[24];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
assertArrayEquals(suffixZeros(buildTestDataArray(0, 16), 24), returnedBuffer);
assertEquals(16, bytesRead);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@Test
public void testClosedMeansClosed() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
int bytesRead = 0;
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertEquals(8, bytesRead);
dataSourceUnderTest.close();
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
try {
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
fail();
} catch (IllegalStateException e) {
// Expected.
}
// 16 bytes were attempted but only 8 should have been successfully read.
assertEquals(8, bytesRead);
}
@Test
public void testOverread() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadSuccess();
// Ask for 16 bytes
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 10000, 16, null);
// Let the response promise to give 16 bytes back.
testResponseHeader.put("Content-Length", Long.toString(16L));
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertEquals(8, bytesRead);
// The current buffer is kept if not completely consumed by DataSource reader.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
assertEquals(14, bytesRead);
// 2 bytes left at this point.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
assertEquals(16, bytesRead);
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 8);
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 6);
verify(mockTransferListener, times(1)).onBytesTransferred(dataSourceUnderTest, 2);
// Now we already returned the 16 bytes initially asked.
// Try to read again even though all requested 16 bytes are already returned.
// Return C.RESULT_END_OF_INPUT
returnedBuffer = new byte[16];
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertEquals(C.RESULT_END_OF_INPUT, bytesOverRead);
assertArrayEquals(new byte[16], returnedBuffer);
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest,
C.RESULT_END_OF_INPUT);
// There should still be only one call to read on cronet.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
assertEquals(16, bytesRead);
}
@Test
public void testConnectTimeout() {
when(mockClock.elapsedRealtime()).thenReturn(0L);
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable();
new Thread() {
@Override
public void run() {
try {
dataSourceUnderTest.open(testDataSpec);
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e instanceof CronetDataSource.OpenException);
assertTrue(e.getCause() instanceof SocketTimeoutException);
assertEquals(
TEST_CONNECTION_STATUS,
((CronetDataSource.OpenException) e).cronetConnectionStatus);
timedOutCondition.open();
}
}
}.start();
startCondition.block();
// We should still be trying to open.
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
timedOutCondition.block();
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@Test
public void testConnectResponseBeforeTimeout() {
when(mockClock.elapsedRealtime()).thenReturn(0L);
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable openCondition = new ConditionVariable();
new Thread() {
@Override
public void run() {
try {
dataSourceUnderTest.open(testDataSpec);
openCondition.open();
} catch (HttpDataSourceException e) {
fail();
}
}
}.start();
startCondition.block();
// We should still be trying to open.
assertFalse(openCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(openCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// The response arrives just in time.
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openCondition.block();
assertEquals(CronetDataSource.OPEN_CONNECTION, dataSourceUnderTest.connectionState);
}
@Test
public void testRedirectIncreasesConnectionTimeout() throws InterruptedException {
when(mockClock.elapsedRealtime()).thenReturn(0L);
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
final ConditionVariable timedOutCondition = new ConditionVariable();
final AtomicInteger openExceptions = new AtomicInteger(0);
new Thread() {
@Override
public void run() {
try {
dataSourceUnderTest.open(testDataSpec);
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e instanceof CronetDataSource.OpenException);
assertTrue(e.getCause() instanceof SocketTimeoutException);
openExceptions.getAndIncrement();
timedOutCondition.open();
}
}
}.start();
startCondition.block();
// We should still be trying to open.
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl1");
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
assertFalse(timedOutCondition.block(newTimeoutMs));
// We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl2");
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
assertFalse(timedOutCondition.block(newTimeoutMs));
// We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
timedOutCondition.block();
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
assertEquals(1, openExceptions.get());
}
@Test
public void testExceptionFromTransferListener() throws HttpDataSourceException {
mockResponesStartSuccess();
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
// the subsequent open() call succeeds.
doThrow(new NullPointerException()).when(mockTransferListener).onTransferEnd(
dataSourceUnderTest);
dataSourceUnderTest.open(testDataSpec);
try {
dataSourceUnderTest.close();
fail("NullPointerException expected");
} catch (NullPointerException e) {
// Expected.
}
// Open should return successfully.
dataSourceUnderTest.open(testDataSpec);
}
@Test
public void testReadFailure() throws HttpDataSourceException {
mockResponesStartSuccess();
mockReadFailure();
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[8];
try {
dataSourceUnderTest.read(returnedBuffer, 0, 8);
fail("dataSourceUnderTest.read() returned, but IOException expected");
} catch (IOException e) {
// Expected.
}
}
// Helper methods.
private void mockStatusResponse() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
UrlRequest.StatusListener statusListener =
(UrlRequest.StatusListener) invocation.getArguments()[0];
statusListener.onStatus(TEST_CONNECTION_STATUS);
return null;
}
}).when(mockUrlRequest).getStatus(any(UrlRequest.StatusListener.class));
}
private void mockResponesStartSuccess() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onResponseStarted(
mockUrlRequest,
testUrlResponseInfo);
return null;
}
}).when(mockUrlRequest).start();
}
private void mockResponseStartRedirect() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onRedirectReceived(
mockUrlRequest,
createUrlResponseInfo(307), // statusCode
"http://redirect.location.com");
return null;
}
}).when(mockUrlRequest).start();
}
private void mockResponseStartFailure() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
mockUrlRequestException);
return null;
}
}).when(mockUrlRequest).start();
}
private void mockReadSuccess() {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
inputBuffer.put(buildTestDataBuffer());
dataSourceUnderTest.onReadCompleted(
mockUrlRequest,
testUrlResponseInfo,
inputBuffer);
return null;
}
}).when(mockUrlRequest).read(any(ByteBuffer.class));
}
private void mockReadFailure() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
null);
return null;
}
}).when(mockUrlRequest).read(any(ByteBuffer.class));
}
private void mockResponseFinished() {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
return null;
}
}).when(mockUrlRequest).read(any(ByteBuffer.class));
}
private ConditionVariable buildUrlRequestStartedCondition() {
final ConditionVariable startedCondition = new ConditionVariable();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
startedCondition.open();
return null;
}
}).when(mockUrlRequest).start();
return startedCondition;
}
private static byte[] buildTestDataArray(int start, int length) {
return Arrays.copyOfRange(buildTestDataBuffer().array(), start, start + length);
}
public static byte[] prefixZeros(byte[] data, int requiredLength) {
byte[] prefixedData = new byte[requiredLength];
System.arraycopy(data, 0, prefixedData, requiredLength - data.length, data.length);
return prefixedData;
}
public static byte[] suffixZeros(byte[] data, int requiredLength) {
return Arrays.copyOf(data, requiredLength);
}
private static ByteBuffer buildTestDataBuffer() {
ByteBuffer testBuffer = ByteBuffer.allocate(TEST_BUFFER_SIZE);
for (byte i = 1; i <= TEST_BUFFER_SIZE; i++) {
testBuffer.put(i);
}
testBuffer.flip();
return testBuffer;
}
private void assertConnectionState(int state) {
assertEquals(state, dataSourceUnderTest.connectionState);
}
}

View file

@ -0,0 +1,23 @@
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.cronet">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
</manifest>

View file

@ -0,0 +1,55 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.chromium.net.UploadDataProvider;
import org.chromium.net.UploadDataSink;
/**
* A {@link UploadDataProvider} implementation that provides data from a {@code byte[]}.
*/
/* package */ final class ByteArrayUploadDataProvider extends UploadDataProvider {
private final byte[] data;
private int position;
public ByteArrayUploadDataProvider(byte[] data) {
this.data = data;
}
@Override
public long getLength() {
return data.length;
}
@Override
public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {
int readLength = Math.min(byteBuffer.remaining(), data.length - position);
byteBuffer.put(data, position, readLength);
position += readLength;
uploadDataSink.onReadSucceeded(false);
}
@Override
public void rewind(UploadDataSink uploadDataSink) throws IOException {
position = 0;
uploadDataSink.onRewindSucceeded();
}
}

View file

@ -0,0 +1,556 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri;
import android.os.ConditionVariable;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.SystemClock;
import com.google.android.exoplayer2.util.TraceUtil;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.chromium.net.CronetEngine;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo;
/**
* DataSource without intermediate buffer based on Cronet API set using UrlRequest.
* <p>This class's methods are organized in the sequence of expected calls.
*/
public class CronetDataSource extends UrlRequest.Callback implements HttpDataSource {
/**
* Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
*/
public static final class OpenException extends HttpDataSourceException {
/**
* Returns the status of the connection establishment at the moment when the error occurred, as
* defined by {@link UrlRequest.Status}.
*/
public final int cronetConnectionStatus;
public OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus) {
super(cause, dataSpec, TYPE_OPEN);
this.cronetConnectionStatus = cronetConnectionStatus;
}
public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus) {
super(errorMessage, dataSpec, TYPE_OPEN);
this.cronetConnectionStatus = cronetConnectionStatus;
}
}
/**
* The default connection timeout, in milliseconds.
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
/**
* The default read timeout, in milliseconds.
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
private static final String TAG = "CronetDataSource";
private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
// The size of read buffer passed to cronet UrlRequest.read().
private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;
/* package */ static final int IDLE_CONNECTION = 5;
/* package */ static final int OPENING_CONNECTION = 2;
/* package */ static final int CONNECTED_CONNECTION = 3;
/* package */ static final int OPEN_CONNECTION = 4;
private final CronetEngine cronetEngine;
private final Executor executor;
private final Predicate<String> contentTypePredicate;
private final TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final Map<String, String> requestProperties;
private final ConditionVariable operation;
private final ByteBuffer readBuffer;
private final Clock clock;
private UrlRequest currentUrlRequest;
private DataSpec currentDataSpec;
private UrlResponseInfo responseInfo;
/* package */ volatile int connectionState;
private volatile String currentUrl;
private volatile long currentConnectTimeoutMs;
private volatile HttpDataSourceException exception;
private volatile long contentLength;
private volatile AtomicLong expectedBytesRemainingToRead;
private volatile boolean hasData;
private volatile boolean responseFinished;
/**
* @param cronetEngine A CronetEngine.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
* @param transferListener A listener.
*/
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener transferListener) {
this(cronetEngine, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
}
/**
* @param cronetEngine A CronetEngine.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
* @param transferListener A listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
*/
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener transferListener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) {
this(cronetEngine, executor, contentTypePredicate, transferListener, connectTimeoutMs,
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock());
}
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener transferListener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) {
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.clock = Assertions.checkNotNull(clock);
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
requestProperties = new HashMap<>();
operation = new ConditionVariable();
connectionState = IDLE_CONNECTION;
}
@Override
public void setRequestProperty(String name, String value) {
synchronized (requestProperties) {
requestProperties.put(name, value);
}
}
@Override
public void clearRequestProperty(String name) {
synchronized (requestProperties) {
requestProperties.remove(name);
}
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return responseInfo == null ? null : responseInfo.getAllHeaders();
}
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
TraceUtil.beginSection("CronetDataSource.open");
try {
Assertions.checkNotNull(dataSpec);
synchronized (this) {
Assertions.checkState(connectionState == IDLE_CONNECTION, "Connection already open");
connectionState = OPENING_CONNECTION;
}
operation.close();
resetConnectTimeout();
startRequest(dataSpec);
boolean requestStarted = blockUntilConnectTimeout();
if (exception != null) {
// An error occurred opening the connection.
throw exception;
} else if (!requestStarted) {
// The timeout was reached before the connection was opened.
throw new OpenException(new SocketTimeoutException(), dataSpec, getCurrentRequestStatus());
}
// Connection was opened.
if (transferListener != null) {
transferListener.onTransferStart(this, dataSpec);
}
connectionState = OPEN_CONNECTION;
return contentLength;
} finally {
TraceUtil.endSection();
}
}
private void startRequest(DataSpec dataSpec) throws HttpDataSourceException {
currentUrl = dataSpec.uri.toString();
currentDataSpec = dataSpec;
UrlRequest.Builder urlRequestBuilder = new UrlRequest.Builder(currentUrl, this, executor,
cronetEngine);
fillCurrentRequestHeader(urlRequestBuilder);
fillCurrentRequestPostBody(urlRequestBuilder, dataSpec);
currentUrlRequest = urlRequestBuilder.build();
currentUrlRequest.start();
}
private void fillCurrentRequestHeader(UrlRequest.Builder urlRequestBuilder) {
synchronized (requestProperties) {
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
urlRequestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
if (currentDataSpec.position == 0 && currentDataSpec.length == C.LENGTH_UNSET) {
// Not required.
return;
}
StringBuilder rangeValue = new StringBuilder();
rangeValue.append("bytes=");
rangeValue.append(currentDataSpec.position);
rangeValue.append("-");
if (currentDataSpec.length != C.LENGTH_UNSET) {
rangeValue.append(currentDataSpec.position + currentDataSpec.length - 1);
}
urlRequestBuilder.addHeader("Range", rangeValue.toString());
}
private void fillCurrentRequestPostBody(UrlRequest.Builder urlRequestBuilder, DataSpec dataSpec)
throws HttpDataSourceException {
if (dataSpec.postBody != null) {
if (!requestProperties.containsKey("Content-Type")) {
throw new OpenException("POST requests must set a Content-Type header", dataSpec,
getCurrentRequestStatus());
}
urlRequestBuilder.setUploadDataProvider(
new ByteArrayUploadDataProvider(dataSpec.postBody), executor);
}
}
@Override
public synchronized void onFailed(
UrlRequest request, UrlResponseInfo info, UrlRequestException error) {
if (request != currentUrlRequest) {
return;
}
if (connectionState == OPENING_CONNECTION) {
IOException cause = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
? new UnknownHostException() : error;
exception = new OpenException(cause, currentDataSpec, getCurrentRequestStatus());
} else if (connectionState == OPEN_CONNECTION) {
readBuffer.limit(0);
exception = new HttpDataSourceException(error, currentDataSpec,
HttpDataSourceException.TYPE_READ);
}
operation.open();
}
@Override
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) {
return;
}
TraceUtil.beginSection("CronetDataSource.onResponseStarted");
try {
validateResponse(info);
responseInfo = info;
// Check content length.
contentLength = getContentLength(info.getAllHeaders());
// If a specific length is requested and a specific length is returned but the 2 don't match
// it's an error.
if (currentDataSpec.length != C.LENGTH_UNSET
&& contentLength != C.LENGTH_UNSET
&& currentDataSpec.length != contentLength) {
throw new OpenException("Content length did not match requested length", currentDataSpec,
getCurrentRequestStatus());
}
if (contentLength > 0) {
expectedBytesRemainingToRead = new AtomicLong(contentLength);
}
// Keep track of redirects.
currentUrl = responseInfo.getUrl();
connectionState = CONNECTED_CONNECTION;
} catch (HttpDataSourceException e) {
exception = e;
} finally {
operation.open();
TraceUtil.endSection();
}
}
private void validateResponse(UrlResponseInfo info) throws HttpDataSourceException {
// Check for a valid response code.
int responseCode = info.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) {
throw new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec);
}
// Check for a valid content type.
try {
String contentType = info.getAllHeaders().get("Content-Type").get(0);
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
throw new InvalidContentTypeException(contentType, currentDataSpec);
}
} catch (IndexOutOfBoundsException e) {
throw new InvalidContentTypeException(null, currentDataSpec);
}
}
private long getContentLength(Map<String, List<String>> headers) {
// Logic copied from {@code DefaultHttpDataSource}
long contentLength = C.LENGTH_UNSET;
List<String> contentLengthHeader = headers.get("Content-Length");
if (contentLengthHeader != null
&& !contentLengthHeader.isEmpty()
&& !TextUtils.isEmpty(contentLengthHeader.get(0))) {
try {
contentLength = Long.parseLong(contentLengthHeader.get(0));
} catch (NumberFormatException e) {
log(Log.ERROR, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
List<String> contentRangeHeader = headers.get("Content-Range");
if (contentRangeHeader != null
&& !contentRangeHeader.isEmpty()
&& !TextUtils.isEmpty(contentRangeHeader.get(0))) {
Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader.get(0));
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody
// would increase it.
log(Log.WARN, "Inconsistent headers [" + contentLengthHeader + "] ["
+ contentRangeHeader + "]");
contentLength = Math.max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
log(Log.ERROR, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
TraceUtil.beginSection("CronetDataSource.read");
try {
synchronized (this) {
if (connectionState != OPEN_CONNECTION) {
throw new IllegalStateException("Connection not ready");
}
}
// If being asked to read beyond the amount of bytes initially requested, return
// RESULT_END_OF_INPUT.
if (expectedBytesRemainingToRead != null && expectedBytesRemainingToRead.get() <= 0) {
return C.RESULT_END_OF_INPUT;
}
if (!hasData) {
// Read more data from cronet.
operation.close();
currentUrlRequest.read(readBuffer);
if (!operation.block(readTimeoutMs)) {
throw new HttpDataSourceException(
new SocketTimeoutException(), currentDataSpec, HttpDataSourceException.TYPE_READ);
}
if (exception != null) {
throw exception;
}
// The expected response length is unknown, but cronet has indicated that the request
// already finished successfully.
if (responseFinished) {
return C.RESULT_END_OF_INPUT;
}
}
int bytesRead = Math.min(readBuffer.remaining(), readLength);
readBuffer.get(buffer, offset, bytesRead);
if (!readBuffer.hasRemaining()) {
readBuffer.clear();
hasData = false;
}
if (expectedBytesRemainingToRead != null) {
expectedBytesRemainingToRead.addAndGet(-bytesRead);
}
if (transferListener != null && bytesRead >= 0) {
transferListener.onBytesTransferred(this, bytesRead);
}
return bytesRead;
} finally {
TraceUtil.endSection();
}
}
@Override
public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
if (request != currentUrlRequest) {
return;
}
if (currentDataSpec.postBody != null) {
int responseCode = info.getHttpStatusCode();
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
// For other redirect response codes the POST request is converted to a GET request and the
// redirect is followed.
if (responseCode == 307 || responseCode == 308) {
exception = new OpenException("POST request redirected with 307 or 308 response code",
currentDataSpec, getCurrentRequestStatus());
operation.open();
return;
}
}
if (resetTimeoutOnRedirects) {
resetConnectTimeout();
}
request.followRedirect();
}
@Override
public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info,
ByteBuffer buffer) {
if (request != currentUrlRequest) {
return;
}
readBuffer.flip();
if (readBuffer.limit() > 0) {
hasData = true;
}
operation.open();
}
@Override
public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) {
return;
}
responseFinished = true;
operation.open();
}
@Override
public synchronized void close() {
TraceUtil.beginSection("CronetDataSource.close");
try {
if (currentUrlRequest != null) {
currentUrlRequest.cancel();
currentUrlRequest = null;
}
readBuffer.clear();
currentDataSpec = null;
currentUrl = null;
exception = null;
contentLength = 0;
hasData = false;
responseInfo = null;
expectedBytesRemainingToRead = null;
responseFinished = false;
if (transferListener != null && connectionState == OPEN_CONNECTION) {
transferListener.onTransferEnd(this);
}
} finally {
connectionState = IDLE_CONNECTION;
TraceUtil.endSection();
}
}
@Override
public Uri getUri() {
return Uri.parse(currentUrl);
}
private void log(int priority, String message) {
if (Log.isLoggable(TAG, priority)) {
Log.println(priority, TAG, message);
}
}
private int getCurrentRequestStatus() {
if (currentUrlRequest == null) {
return UrlRequest.Status.IDLE;
}
final ConditionVariable conditionVariable = new ConditionVariable();
final AtomicInteger result = new AtomicInteger();
currentUrlRequest.getStatus(new UrlRequest.StatusListener() {
@Override
public void onStatus(int status) {
result.set(status);
conditionVariable.open();
}
});
return result.get();
}
private boolean blockUntilConnectTimeout() {
long now = clock.elapsedRealtime();
boolean opened = false;
while (!opened && now < currentConnectTimeoutMs) {
opened = operation.block(currentConnectTimeoutMs - now + 5 /* fudge factor */);
now = clock.elapsedRealtime();
}
return opened;
}
private void resetConnectTimeout() {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSource.Factory;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
/**
* A {@link Factory} that produces {@link DefaultDataSource} instances that delegate to
* {@link CronetDataSource}s for non-file/asset/content URIs.
*/
public final class DefaultCronetDataSourceFactory implements Factory {
/**
* The default connection timeout, in milliseconds.
*/
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
/**
* The default read timeout, in milliseconds.
*/
public static final int DEFAULT_READ_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
private final Context context;
private final CronetEngine cronetEngine;
private final Executor executor;
private final Predicate<String> contentTypePredicate;
private final TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
public DefaultCronetDataSourceFactory(Context context, CronetEngine cronetEngine,
Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener) {
this(context, cronetEngine, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
}
public DefaultCronetDataSourceFactory(Context context, CronetEngine cronetEngine,
Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
int readTimeoutMs, boolean resetTimeoutOnRedirects) {
this.context = context;
this.cronetEngine = cronetEngine;
this.executor = executor;
this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
}
@Override
public DefaultDataSource createDataSource() {
DataSource cronetDataSource = new CronetDataSource(cronetEngine, executor, contentTypePredicate,
transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
return new DefaultDataSource(context, transferListener, cronetDataSource);
}
}