mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
50527c0a7d
commit
9adce6b007
7 changed files with 1691 additions and 0 deletions
34
extensions/cronet/src/androidTest/AndroidManifest.xml
Normal file
34
extensions/cronet/src/androidTest/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
extensions/cronet/src/main/AndroidManifest.xml
Normal file
23
extensions/cronet/src/main/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue