mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
Sanitize threading in CronetDataSource
- Move nearly all logic onto the calling thread (i.e. the thread calling open/read/close), to make threading correctness more obvious. - Document which variables are read/written from which thread, and why the call sequences are safe. - Fix thread safety issue that I think could probably cause data corruption in the case of a read timeout followed by another request into the DataSource. Also: - Relaxed content length checking to be consistent with the other http DataSource implementations, and avoided parsing the headers where they're not used. - Fixed missing generics in CronetDataSourceFactory. - Added TODO to work with servers that don't support partial range requests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=135702217
This commit is contained in:
parent
4fab402274
commit
907b9bf4f6
5 changed files with 120 additions and 173 deletions
|
|
@ -174,10 +174,7 @@ public final class CronetDataSourceTest {
|
|||
@Test(expected = IllegalStateException.class)
|
||||
public void testOpeningTwiceThrows() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +202,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
null);
|
||||
mockUrlRequestException);
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest2,
|
||||
testUrlResponseInfo);
|
||||
|
|
@ -253,13 +250,10 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
|
||||
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
|
||||
throws HttpDataSourceException {
|
||||
|
|
@ -271,7 +265,6 @@ public final class CronetDataSourceTest {
|
|||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
|
||||
assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
|
||||
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +279,6 @@ public final class CronetDataSourceTest {
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -304,7 +296,6 @@ public final class CronetDataSourceTest {
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -321,7 +312,6 @@ public final class CronetDataSourceTest {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -338,37 +328,16 @@ public final class CronetDataSourceTest {
|
|||
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() {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
// 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 {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
|
||||
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
|
||||
}
|
||||
|
||||
|
|
@ -510,7 +479,6 @@ public final class CronetDataSourceTest {
|
|||
|
||||
dataSourceUnderTest.close();
|
||||
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
|
||||
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
|
||||
|
||||
try {
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
|
|
@ -572,7 +540,6 @@ public final class CronetDataSourceTest {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -603,15 +570,12 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -637,15 +601,12 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// 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
|
||||
|
|
@ -674,11 +635,9 @@ public final class CronetDataSourceTest {
|
|||
|
||||
// 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");
|
||||
|
|
@ -689,7 +648,6 @@ public final class CronetDataSourceTest {
|
|||
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");
|
||||
|
|
@ -700,11 +658,9 @@ public final class CronetDataSourceTest {
|
|||
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());
|
||||
|
|
@ -818,7 +774,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
null);
|
||||
mockUrlRequestException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
|
|
@ -869,8 +825,4 @@ public final class CronetDataSourceTest {
|
|||
return testBuffer;
|
||||
}
|
||||
|
||||
private void assertConnectionState(int state) {
|
||||
assertEquals(state, dataSourceUnderTest.connectionState);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
|
@ -91,11 +90,6 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
// 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;
|
||||
|
|
@ -105,20 +99,29 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
private final boolean resetTimeoutOnRedirects;
|
||||
private final Map<String, String> requestProperties;
|
||||
private final ConditionVariable operation;
|
||||
private final ByteBuffer readBuffer;
|
||||
private final Clock clock;
|
||||
|
||||
// Accessed by the calling thread only.
|
||||
private boolean opened;
|
||||
private long bytesRemaining;
|
||||
|
||||
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
|
||||
// to reads made by the Cronet thread.
|
||||
private UrlRequest currentUrlRequest;
|
||||
private DataSpec currentDataSpec;
|
||||
private UrlResponseInfo responseInfo;
|
||||
|
||||
/* package */ volatile int connectionState;
|
||||
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
|
||||
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
|
||||
// thread.
|
||||
private ByteBuffer readBuffer;
|
||||
|
||||
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
|
||||
// made by the calling thread.
|
||||
private UrlResponseInfo responseInfo;
|
||||
private IOException exception;
|
||||
private boolean finished;
|
||||
|
||||
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.
|
||||
|
|
@ -163,10 +166,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
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;
|
||||
}
|
||||
|
||||
// HttpDataSource implementation.
|
||||
|
|
@ -205,10 +206,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
@Override
|
||||
public long open(DataSpec dataSpec) throws HttpDataSourceException {
|
||||
Assertions.checkNotNull(dataSpec);
|
||||
synchronized (this) {
|
||||
Assertions.checkState(connectionState == IDLE_CONNECTION, "Connection already open");
|
||||
connectionState = OPENING_CONNECTION;
|
||||
}
|
||||
Assertions.checkState(!opened);
|
||||
|
||||
operation.close();
|
||||
resetConnectTimeout();
|
||||
|
|
@ -218,61 +216,99 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
boolean requestStarted = blockUntilConnectTimeout();
|
||||
|
||||
if (exception != null) {
|
||||
// An error occurred opening the connection.
|
||||
throw exception;
|
||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
||||
} else if (!requestStarted) {
|
||||
// The timeout was reached before the connection was opened.
|
||||
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
||||
}
|
||||
|
||||
// Connection was opened.
|
||||
// Check for a valid response code.
|
||||
int responseCode = responseInfo.getHttpStatusCode();
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
InvalidResponseCodeException exception = new InvalidResponseCodeException(responseCode,
|
||||
responseInfo.getAllHeaders(), currentDataSpec);
|
||||
if (responseCode == 416) {
|
||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
// Check for a valid content type.
|
||||
if (contentTypePredicate != null) {
|
||||
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
||||
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
||||
if (!contentTypePredicate.evaluate(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, currentDataSpec);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the case where we requested a range starting from a non-zero position and
|
||||
// received a 200 rather than a 206. This occurs if the server does not support partial
|
||||
// requests, and requires that the source skips to the requested position.
|
||||
|
||||
// Calculate the content length.
|
||||
if (!getIsCompressed(responseInfo)) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining = dataSpec.length;
|
||||
} else {
|
||||
bytesRemaining = getContentLength(responseInfo);
|
||||
}
|
||||
} else {
|
||||
// If the response is compressed then the content length will be that of the compressed data
|
||||
// which isn't what we want. Always use the dataSpec length in this case.
|
||||
bytesRemaining = currentDataSpec.length;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
if (listener != null) {
|
||||
listener.onTransferStart(this, dataSpec);
|
||||
}
|
||||
connectionState = OPEN_CONNECTION;
|
||||
return contentLength;
|
||||
|
||||
return bytesRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
|
||||
synchronized (this) {
|
||||
Assertions.checkState(connectionState == OPEN_CONNECTION);
|
||||
}
|
||||
Assertions.checkState(opened);
|
||||
|
||||
if (readLength == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (expectedBytesRemainingToRead != null && expectedBytesRemainingToRead.get() == 0) {
|
||||
} else if (bytesRemaining == 0) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
// Read more data from cronet.
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
}
|
||||
if (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
currentUrlRequest.read(readBuffer);
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
// We're timing out, but since the operation is still ongoing we'll need to replace
|
||||
// readBuffer to avoid the possibility of it being written to by this operation during a
|
||||
// subsequent request.
|
||||
readBuffer = null;
|
||||
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) {
|
||||
} else if (exception != null) {
|
||||
throw new HttpDataSourceException(exception, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
} else if (finished) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
readBuffer.flip();
|
||||
}
|
||||
}
|
||||
|
||||
int bytesRead = Math.min(readBuffer.remaining(), readLength);
|
||||
readBuffer.get(buffer, offset, bytesRead);
|
||||
if (!readBuffer.hasRemaining()) {
|
||||
hasData = false;
|
||||
}
|
||||
|
||||
if (expectedBytesRemainingToRead != null) {
|
||||
expectedBytesRemainingToRead.addAndGet(-bytesRead);
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onBytesTransferred(this, bytesRead);
|
||||
|
|
@ -286,26 +322,26 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
currentUrlRequest.cancel();
|
||||
currentUrlRequest = null;
|
||||
}
|
||||
if (readBuffer != null) {
|
||||
readBuffer.limit(0);
|
||||
}
|
||||
currentDataSpec = null;
|
||||
exception = null;
|
||||
contentLength = 0;
|
||||
hasData = false;
|
||||
responseInfo = null;
|
||||
expectedBytesRemainingToRead = null;
|
||||
responseFinished = false;
|
||||
try {
|
||||
if (listener != null && connectionState == OPEN_CONNECTION) {
|
||||
exception = null;
|
||||
finished = false;
|
||||
if (opened) {
|
||||
opened = false;
|
||||
if (listener != null) {
|
||||
listener.onTransferEnd(this);
|
||||
}
|
||||
} finally {
|
||||
connectionState = IDLE_CONNECTION;
|
||||
}
|
||||
}
|
||||
|
||||
// UrlRequest.Callback implementation
|
||||
|
||||
@Override
|
||||
public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
|
||||
public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info,
|
||||
String newLocationUrl) {
|
||||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -315,8 +351,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
// 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, getStatus(request));
|
||||
exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(),
|
||||
currentDataSpec);
|
||||
operation.open();
|
||||
return;
|
||||
}
|
||||
|
|
@ -332,51 +368,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check for a valid response code.
|
||||
int responseCode = info.getHttpStatusCode();
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
InvalidResponseCodeException exception = new InvalidResponseCodeException(
|
||||
responseCode, info.getAllHeaders(), currentDataSpec);
|
||||
if (responseCode == 416) {
|
||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
// Check for a valid content type.
|
||||
if (contentTypePredicate != null) {
|
||||
List<String> contentTypeHeaders = info.getAllHeaders().get(CONTENT_TYPE);
|
||||
String contentType = contentTypeHeaders == null || contentTypeHeaders.isEmpty() ? null
|
||||
: contentTypeHeaders.get(0);
|
||||
if (!contentTypePredicate.evaluate(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, currentDataSpec);
|
||||
}
|
||||
}
|
||||
|
||||
responseInfo = info;
|
||||
if (getIsCompressed(info)) {
|
||||
contentLength = currentDataSpec.length;
|
||||
} else {
|
||||
// Check content length.
|
||||
contentLength = getContentLength(info);
|
||||
// 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,
|
||||
getStatus(request));
|
||||
}
|
||||
}
|
||||
if (contentLength > 0) {
|
||||
expectedBytesRemainingToRead = new AtomicLong(contentLength);
|
||||
}
|
||||
|
||||
connectionState = CONNECTED_CONNECTION;
|
||||
} catch (HttpDataSourceException e) {
|
||||
exception = e;
|
||||
} finally {
|
||||
operation.open();
|
||||
}
|
||||
responseInfo = info;
|
||||
operation.open();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -385,17 +378,15 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
readBuffer.flip();
|
||||
hasData = true;
|
||||
operation.open();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
|
||||
public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
|
||||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
responseFinished = true;
|
||||
finished = true;
|
||||
operation.open();
|
||||
}
|
||||
|
||||
|
|
@ -405,14 +396,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
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, getStatus(request));
|
||||
} else if (connectionState == OPEN_CONNECTION) {
|
||||
exception = new HttpDataSourceException(error, currentDataSpec,
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
|
||||
? new UnknownHostException() : error;
|
||||
operation.open();
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +462,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
Map<String, List<String>> headers = info.getAllHeaders();
|
||||
List<String> contentLengthHeaders = headers.get("Content-Length");
|
||||
String contentLengthHeader = null;
|
||||
if (contentLengthHeaders != null && !contentLengthHeaders.isEmpty()) {
|
||||
if (!isEmpty(contentLengthHeaders)) {
|
||||
contentLengthHeader = contentLengthHeaders.get(0);
|
||||
if (!TextUtils.isEmpty(contentLengthHeader)) {
|
||||
try {
|
||||
|
|
@ -488,7 +473,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
}
|
||||
}
|
||||
List<String> contentRangeHeaders = headers.get("Content-Range");
|
||||
if (contentRangeHeaders != null && !contentRangeHeaders.isEmpty()) {
|
||||
if (!isEmpty(contentRangeHeaders)) {
|
||||
String contentRangeHeader = contentRangeHeaders.get(0);
|
||||
Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader);
|
||||
if (matcher.find()) {
|
||||
|
|
@ -530,4 +515,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
return statusHolder[0];
|
||||
}
|
||||
|
||||
private static boolean isEmpty(List<?> list) {
|
||||
return list == null || list.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public final class CronetDataSourceFactory implements Factory {
|
|||
private final CronetEngine cronetEngine;
|
||||
private final Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener transferListener;
|
||||
private final TransferListener<? super DataSource> transferListener;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
|
|
|
|||
|
|
@ -185,9 +185,12 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
long contentLength = response.body().contentLength();
|
||||
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length
|
||||
: (contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET);
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesToRead = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = response.body().contentLength();
|
||||
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
if (listener != null) {
|
||||
|
|
|
|||
|
|
@ -231,10 +231,13 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
|
||||
long contentLength = getContentLength(connection);
|
||||
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length
|
||||
: contentLength != C.LENGTH_UNSET ? contentLength - bytesToSkip
|
||||
: C.LENGTH_UNSET;
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesToRead = dataSpec.length;
|
||||
} else {
|
||||
long contentLength = getContentLength(connection);
|
||||
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
|
||||
: C.LENGTH_UNSET;
|
||||
}
|
||||
} else {
|
||||
// Gzip is enabled. If the server opts to use gzip then the content length in the response
|
||||
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
|
||||
|
|
|
|||
Loading…
Reference in a new issue