/*
* 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 androidx.media3.datasource;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
import static java.lang.Math.min;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec.HttpMethod;
import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.NoRouteToHostException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
*
* <p>By default this implementation will not follow cross-protocol redirects (i.e. redirects from
* HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by passing {@code true} to
* {@link DefaultHttpDataSource.Factory#setAllowCrossProtocolRedirects(boolean)}.
*
* <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
* priority) the {@code dataSpec}, {@link #setRequestProperty} and the default properties that can
* be passed to {@link HttpDataSource.Factory#setDefaultRequestProperties(Map)}.
*/
public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource {
/** {@link DataSource.Factory} for {@link DefaultHttpDataSource} instances. */
public static final class Factory implements HttpDataSource.Factory {
private final RequestProperties defaultRequestProperties;
@Nullable private TransferListener transferListener;
@Nullable private Predicate<String> contentTypePredicate;
@Nullable private String userAgent;
private int connectTimeoutMs;
private int readTimeoutMs;
private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects;
/** Creates an instance. */
public Factory() {
defaultRequestProperties = new RequestProperties();
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}
@UnstableApi
@Override
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
return this;
}
/**
* Sets the user agent that will be used.
*
* <p>The default is {@code null}, which causes the default user agent of the underlying
* platform to be used.
*
* @param userAgent The user agent that will be used, or {@code null} to use the default user
* agent of the underlying platform.
* @return This factory.
*/
@UnstableApi
public Factory setUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
return this;
}
/**
* Sets the connect timeout, in milliseconds.
*
* <p>The default is {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
*
* @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@UnstableApi
public Factory setConnectTimeoutMs(int connectTimeoutMs) {
this.connectTimeoutMs = connectTimeoutMs;
return this;
}
/**
* Sets the read timeout, in milliseconds.
*
* <p>The default is {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
*
* @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@UnstableApi
public Factory setReadTimeoutMs(int readTimeoutMs) {
this.readTimeoutMs = readTimeoutMs;
return this;
}
/**
* Sets whether to allow cross protocol redirects.
*
* <p>The default is {@code false}.
*
* @param allowCrossProtocolRedirects Whether to allow cross protocol redirects.
* @return This factory.
*/
@UnstableApi
public Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) {
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
return this;
}
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
* DefaultHttpDataSource#open(DataSpec)}.
*
* <p>The default is {@code null}.
*
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
* @return This factory.
*/
@UnstableApi
public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
return this;
}
/**
* Sets the {@link TransferListener} that will be used.
*
* <p>The default is {@code null}.
*
* <p>See {@link DataSource#addTransferListener(TransferListener)}.
*
* @param transferListener The listener that will be used.
* @return This factory.
*/
@UnstableApi
public Factory setTransferListener(@Nullable TransferListener transferListener) {
this.transferListener = transferListener;
return this;
}
/**
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
* POST request.
*/
@UnstableApi
public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
this.keepPostFor302Redirects = keepPostFor302Redirects;
return this;
}
@UnstableApi
@Override
public DefaultHttpDataSource createDataSource() {
DefaultHttpDataSource dataSource =
new DefaultHttpDataSource(
userAgent,
connectTimeoutMs,
readTimeoutMs,
allowCrossProtocolRedirects,
defaultRequestProperties,
contentTypePredicate,
keepPostFor302Redirects);
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
return dataSource;
}
}
/** The default connection timeout, in milliseconds. */
@UnstableApi public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
/** The default read timeout, in milliseconds. */
@UnstableApi public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
private static final String TAG = "DefaultHttpDataSource";
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
private static final long MAX_BYTES_TO_DRAIN = 2048;
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
@Nullable private final String userAgent;
@Nullable private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final boolean keepPostFor302Redirects;
@Nullable private Predicate<String> contentTypePredicate;
@Nullable private DataSpec dataSpec;
@Nullable private HttpURLConnection connection;
@Nullable private InputStream inputStream;
private boolean opened;
private int responseCode;
private long bytesToRead;
private long bytesRead;
/**
* @deprecated Use {@link DefaultHttpDataSource.Factory} instead.
*/
@UnstableApi
@SuppressWarnings("deprecation")
@Deprecated
public DefaultHttpDataSource() {
this(/* userAgent= */ null, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* @deprecated Use {@link DefaultHttpDataSource.Factory} instead.
*/
@UnstableApi
@SuppressWarnings("deprecation")
@Deprecated
public DefaultHttpDataSource(@Nullable String userAgent) {
this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS);
}
/**
* @deprecated Use {@link DefaultHttpDataSource.Factory} instead.
*/
@UnstableApi
@SuppressWarnings("deprecation")
@Deprecated
public DefaultHttpDataSource(
@Nullable String userAgent, int connectTimeoutMillis, int readTimeoutMillis) {
this(
userAgent,
connectTimeoutMillis,
readTimeoutMillis,
/* allowCrossProtocolRedirects= */ false,
/* defaultRequestProperties= */ null);
}
/**
* @deprecated Use {@link DefaultHttpDataSource.Factory} instead.
*/
@UnstableApi
@Deprecated
public DefaultHttpDataSource(
@Nullable String userAgent,
int connectTimeoutMillis,
int readTimeoutMillis,
boolean allowCrossProtocolRedirects,
@Nullable RequestProperties defaultRequestProperties) {
this(
userAgent,
connectTimeoutMillis,
readTimeoutMillis,
allowCrossProtocolRedirects,
defaultRequestProperties,
/* contentTypePredicate= */ null,
/* keepPostFor302Redirects= */ false);
}
private DefaultHttpDataSource(
@Nullable String userAgent,
int connectTimeoutMillis,
int readTimeoutMillis,
boolean allowCrossProtocolRedirects,
@Nullable RequestProperties defaultRequestProperties,
@Nullable Predicate<String> contentTypePredicate,
boolean keepPostFor302Redirects) {
super(/* isNetwork= */ true);
this.userAgent = userAgent;
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
this.defaultRequestProperties = defaultRequestProperties;
this.contentTypePredicate = contentTypePredicate;
this.requestProperties = new RequestProperties();
this.keepPostFor302Redirects = keepPostFor302Redirects;
}
/**
* @deprecated Use {@link DefaultHttpDataSource.Factory#setContentTypePredicate(Predicate)}
* instead.
*/
@UnstableApi
@Deprecated
public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
}
@UnstableApi
@Override
@Nullable
public Uri getUri() {
return connection == null ? null : Uri.parse(connection.getURL().toString());
}
@UnstableApi
@Override
public int getResponseCode() {
return connection == null || responseCode <= 0 ? -1 : responseCode;
}
@UnstableApi
@Override
public Map<String, List<String>> getResponseHeaders() {
if (connection == null) {
return ImmutableMap.of();
}
// connection.getHeaderFields() always contains a null key with a value like
// ["HTTP/1.1 200 OK"]. The response code is available from HttpURLConnection#getResponseCode()
// and the HTTP version is fixed when establishing the connection.
// DataSource#getResponseHeaders() doesn't allow null keys in the returned map, so we need to
// remove it.
// connection.getHeaderFields() returns a special unmodifiable case-insensitive Map
// so we can't just remove the null key or make a copy without the null key. Instead we wrap it
// in a ForwardingMap subclass that ignores and filters out null keys in the read methods.
return new NullFilteringHeadersMap(connection.getHeaderFields());
}
@UnstableApi
@Override
public void setRequestProperty(String name, String value) {
checkNotNull(name);
checkNotNull(value);
requestProperties.set(name, value);
}
@UnstableApi
@Override
public void clearRequestProperty(String name) {
checkNotNull(name);
requestProperties.remove(name);
}
@UnstableApi
@Override
public void clearAllRequestProperties() {
requestProperties.clear();
}
/** Opens the source to read the specified data. */
@UnstableApi
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
bytesRead = 0;
bytesToRead = 0;
transferInitializing(dataSpec);
String responseMessage;
HttpURLConnection connection;
try {
this.connection = makeConnection(dataSpec);
connection = this.connection;
responseCode = connection.getResponseCode();
responseMessage = connection.getResponseMessage();
} catch (IOException e) {
closeConnectionQuietly();
throw HttpDataSourceException.createForIOException(
e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
Map<String, List<String>> headers = connection.getHeaderFields();
if (responseCode == 416) {
long documentSize =
HttpUtil.getDocumentSize(connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
if (dataSpec.position == documentSize) {
opened = true;
transferStarted(dataSpec);
return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
}
}
@Nullable InputStream errorStream = connection.getErrorStream();
byte[] errorResponseBody;
try {
errorResponseBody =
errorStream != null ? Util.toByteArray(errorStream) : Util.EMPTY_BYTE_ARRAY;
} catch (IOException e) {
errorResponseBody = Util.EMPTY_BYTE_ARRAY;
}
closeConnectionQuietly();
@Nullable
IOException cause =
responseCode == 416
? new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
: null;
throw new InvalidResponseCodeException(
responseCode, responseMessage, cause, headers, dataSpec, errorResponseBody);
}
// Check for a valid content type.
String contentType = connection.getContentType();
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
closeConnectionQuietly();
throw new InvalidContentTypeException(contentType, dataSpec);
}
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
boolean isCompressed = isCompressed(connection);
if (!isCompressed) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
long contentLength =
HttpUtil.getContentLength(
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
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. Always use the dataSpec
// length in this case.
bytesToRead = dataSpec.length;
}
try {
inputStream = connection.getInputStream();
if (isCompressed) {
inputStream = new GZIPInputStream(inputStream);
}
} catch (IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
opened = true;
transferStarted(dataSpec);
try {
skipFully(bytesToSkip, dataSpec);
} catch (IOException e) {
closeConnectionQuietly();
if (e instanceof HttpDataSourceException) {
throw (HttpDataSourceException) e;
}
throw new HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
return bytesToRead;
}
@UnstableApi
@Override
public int read(byte[] buffer, int offset, int length) throws HttpDataSourceException {
try {
return readInternal(buffer, offset, length);
} catch (IOException e) {
throw HttpDataSourceException.createForIOException(
e, castNonNull(dataSpec), HttpDataSourceException.TYPE_READ);
}
}
@UnstableApi
@Override
public void close() throws HttpDataSourceException {
try {
@Nullable InputStream inputStream = this.inputStream;
if (inputStream != null) {
long bytesRemaining =
bytesToRead == C.LENGTH_UNSET ? C.LENGTH_UNSET : bytesToRead - bytesRead;
maybeTerminateInputStream(connection, bytesRemaining);
try {
inputStream.close();
} catch (IOException e) {
throw new HttpDataSourceException(
e,
castNonNull(dataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE);
}
}
} finally {
inputStream = null;
closeConnectionQuietly();
if (opened) {
opened = false;
transferEnded();
}
}
}
/** Establishes a connection, following redirects to do so where permitted. */
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
@HttpMethod int httpMethod = dataSpec.httpMethod;
@Nullable byte[] httpBody = dataSpec.httpBody;
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
return makeConnection(
url,
httpMethod,
httpBody,
position,
length,
allowGzip,
/* followRedirects= */ true,
dataSpec.httpRequestHeaders);
}
// We need to handle redirects ourselves to allow cross-protocol redirects or to keep the POST
// request method for 302.
int redirectCount = 0;
while (redirectCount++ <= MAX_REDIRECTS) {
HttpURLConnection connection =
makeConnection(
url,
httpMethod,
httpBody,
position,
length,
allowGzip,
/* followRedirects= */ false,
dataSpec.httpRequestHeaders);
int responseCode = connection.getResponseCode();
String location = connection.getHeaderField("Location");
if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
|| responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {
connection.disconnect();
url = handleRedirect(url, location, dataSpec);
} else if (httpMethod == DataSpec.HTTP_METHOD_POST
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER)) {
connection.disconnect();
boolean shouldKeepPost =
keepPostFor302Redirects && responseCode == HttpURLConnection.HTTP_MOVED_TEMP;
if (!shouldKeepPost) {
// POST request follows the redirect and is transformed into a GET request.
httpMethod = DataSpec.HTTP_METHOD_GET;
httpBody = null;
}
url = handleRedirect(url, location, dataSpec);
} else {
return connection;
}
}
// If we get here we've been redirected more times than are permitted.
throw new HttpDataSourceException(
new NoRouteToHostException("Too many redirects: " + redirectCount),
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
/**
* Configures a connection and opens it.
*
* @param url The url to connect to.
* @param httpMethod The http method.
* @param httpBody The body data, or {@code null} if not required.
* @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
* @param allowGzip Whether to allow the use of gzip.
* @param followRedirects Whether to follow redirects.
* @param requestParameters parameters (HTTP headers) to include in request.
*/
private HttpURLConnection makeConnection(
URL url,
@HttpMethod int httpMethod,
@Nullable byte[] httpBody,
long position,
long length,
boolean allowGzip,
boolean followRedirects,
Map<String, String> requestParameters)
throws IOException {
HttpURLConnection connection = openConnection(url);
connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis);
Map<String, String> requestHeaders = new HashMap<>();
if (defaultRequestProperties != null) {
requestHeaders.putAll(defaultRequestProperties.getSnapshot());
}
requestHeaders.putAll(requestProperties.getSnapshot());
requestHeaders.putAll(requestParameters);
for (Map.Entry<String, String> property : requestHeaders.entrySet()) {
connection.setRequestProperty(property.getKey(), property.getValue());
}
@Nullable String rangeHeader = buildRangeRequestHeader(position, length);
if (rangeHeader != null) {
connection.setRequestProperty(HttpHeaders.RANGE, rangeHeader);
}
if (userAgent != null) {
connection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent);
}
connection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity");
connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
if (httpBody != null) {
connection.setFixedLengthStreamingMode(httpBody.length);
connection.connect();
OutputStream os = connection.getOutputStream();
os.write(httpBody);
os.close();
} else {
connection.connect();
}
return connection;
}
/** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */
@VisibleForTesting
/* package */ HttpURLConnection openConnection(URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}
/**
* Handles a redirect.
*
* @param originalUrl The original URL.
* @param location The Location header in the response. May be {@code null}.
* @param dataSpec The {@link DataSpec}.
* @return The next URL.
* @throws HttpDataSourceException If redirection isn't possible.
*/
private URL handleRedirect(URL originalUrl, @Nullable String location, DataSpec dataSpec)
throws HttpDataSourceException {
if (location == null) {
throw new HttpDataSourceException(
"Null location redirect",
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
// Form the new url.
URL url;
try {
url = new URL(originalUrl, location);
} catch (MalformedURLException e) {
throw new HttpDataSourceException(
e,
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
// Check that the protocol of the new url is supported.
String protocol = url.getProtocol();
if (!"https".equals(protocol) && !"http".equals(protocol)) {
throw new HttpDataSourceException(
"Unsupported protocol redirect: " + protocol,
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
throw new HttpDataSourceException(
"Disallowed cross-protocol redirect ("
+ originalUrl.getProtocol()
+ " to "
+ protocol
+ ")",
dataSpec,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
return url;
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @param dataSpec The {@link DataSpec}.
* @throws IOException If the thread is interrupted during the operation, or if the data ended
* before skipping the specified number of bytes.
*/
private void skipFully(long bytesToSkip, DataSpec dataSpec) throws IOException {
if (bytesToSkip == 0) {
return;
}
byte[] skipBuffer = new byte[4096];
while (bytesToSkip > 0) {
int readLength = (int) min(bytesToSkip, skipBuffer.length);
int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new HttpDataSourceException(
new InterruptedIOException(),
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
if (read == -1) {
throw new HttpDataSourceException(
dataSpec,
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
HttpDataSourceException.TYPE_OPEN);
}
bytesToSkip -= read;
bytesTransferred(read);
}
}
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at index
* {@code offset}.
*
* <p>This method blocks until at least one byte of data can be read, the end of the opened range
* is detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
* range is reached.
* @throws IOException If an error occurs reading from the source.
*/
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
if (readLength == 0) {
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) min(readLength, bytesRemaining);
}
int read = castNonNull(inputStream).read(buffer, offset, readLength);
if (read == -1) {
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
bytesTransferred(read);
return read;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNSET} otherwise.
*/
private static void maybeTerminateInputStream(
@Nullable HttpURLConnection connection, long bytesRemaining) {
if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
return;
}
try {
InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNSET) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
// re-used.
return;
}
String className = inputStream.getClass().getName();
if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className)
|| "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
.equals(className)) {
Class<?> superclass = inputStream.getClass().getSuperclass();
Method unexpectedEndOfInput =
checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (Exception e) {
// If an IOException then the connection didn't ever have an input stream, or it was closed
// already. If another type of exception then something went wrong, most likely the device
// isn't using okhttp.
}
}
/** Closes the current connection quietly, if there is one. */
private void closeConnectionQuietly() {
if (connection != null) {
try {
connection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Unexpected error while disconnecting", e);
}
connection = null;
}
}
private static boolean isCompressed(HttpURLConnection connection) {
String contentEncoding = connection.getHeaderField("Content-Encoding");
return "gzip".equalsIgnoreCase(contentEncoding);
}
private static class NullFilteringHeadersMap extends ForwardingMap<String, List<String>> {
private final Map<String, List<String>> headers;
public NullFilteringHeadersMap(Map<String, List<String>> headers) {
this.headers = headers;
}
@Override
protected Map<String, List<String>> delegate() {
return headers;
}
@Override
public boolean containsKey(@Nullable Object key) {
return key != null && super.containsKey(key);
}
@Nullable
@Override
public List<String> get(@Nullable Object key) {
return key == null ? null : super.get(key);
}
@Override
public Set<String> keySet() {
return Sets.filter(super.keySet(), key -> key != null);
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
return Sets.filter(super.entrySet(), entry -> entry.getKey() != null);
}
@Override
public int size() {
return super.size() - (super.containsKey(null) ? 1 : 0);
}
@Override
public boolean isEmpty() {
return super.isEmpty() || (super.size() == 1 && super.containsKey(null));
}
@Override
public boolean containsValue(@Nullable Object value) {
return super.standardContainsValue(value);
}
@Override
public boolean equals(@Nullable Object object) {
return object != null && super.standardEquals(object);
}
@Override
public int hashCode() {
return super.standardHashCode();
}
}
}