mirror of
https://github.com/samsonjs/media.git
synced 2026-04-03 10:55:48 +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