Commit 434a2733 by Oliver Woodman

OkHttp extension modifications.

1. Change package name (rm "datasource")
2. Require injection of OkHttpClient through all constructors, and
   remove allowCrossProtocolRedirect/connectTimeout/readTimeout
   constructor arguments. The client should be configured with these
   prior to injection.
3. Fix code style to be consistent with the project.
4. Simplify call to get contentLength. I'm pretty sure okhttp returns
   the correct value when gzip is enabled, so there's no need to check
   this in the data source.
5. Misc cleanups.
parent 2112bc24
...@@ -13,67 +13,49 @@ ...@@ -13,67 +13,49 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.ext.datasource.okhttp; package com.google.android.exoplayer.ext.okhttp;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.TransferListener; import com.google.android.exoplayer.upstream.TransferListener;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate; import com.google.android.exoplayer.util.Predicate;
import com.squareup.okhttp.CacheControl; import com.squareup.okhttp.CacheControl;
import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request; import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response; import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static com.squareup.okhttp.internal.Util.closeQuietly;
/** /**
* A {@link HttpDataSource} that uses Square's {@link OkHttpClient}. * An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}.
* <p/>
* By default this implementation will follow cross-protocol redirects (i.e. redirects from
* HTTP to HTTPS or vice versa). Cross-protocol redirects can be disabled by using the
* {@link #OkHttpDataSource(String, Predicate, TransferListener, int, int, boolean, OkHttpClient, CacheControl)}
* constructor and passing {@code false} as the sixth argument.
*/ */
public class OkHttpDataSource implements HttpDataSource { public class OkHttpDataSource implements HttpDataSource {
/**
* The default connection timeout, in milliseconds.
*/
private static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
/**
* The default read timeout, in milliseconds.
*/
private static final int DEFAULT_READ_TIMEOUT_MILLIS = DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
private static final String TAG = "OkHttpDataSource";
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>(); private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final String userAgent;
private final OkHttpClient okHttpClient;
private final String userAgent;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final HashMap<String, String> requestProperties;
private final CacheControl cacheControl;
private final TransferListener listener; private final TransferListener listener;
private final CacheControl cacheControl;
private final HashMap<String, String> requestProperties;
private DataSpec dataSpec; private DataSpec dataSpec;
private static OkHttpClient okHttpClient;
private Response response; private Response response;
private InputStream responseByteStream;
private boolean opened; private boolean opened;
private long bytesToSkip; private long bytesToSkip;
...@@ -83,84 +65,53 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -83,84 +65,53 @@ public class OkHttpDataSource implements HttpDataSource {
private long bytesRead; private long bytesRead;
/** /**
* @param client An {@link OkHttpClient} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* rejected by the predicate then a {@link InvalidContentTypeException} is * predicate then a
* {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}. * thrown from {@link #open(DataSpec)}.
*/ */
public OkHttpDataSource(String userAgent, Predicate<String> contentTypePredicate) { public OkHttpDataSource(OkHttpClient client, String userAgent,
this(userAgent, contentTypePredicate, null); Predicate<String> contentTypePredicate) {
this(client, userAgent, contentTypePredicate, null);
} }
/** /**
* @param client An {@link OkHttpClient} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* rejected by the predicate then a {@link InvalidContentTypeException} is * predicate then a
* {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}. * thrown from {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
*/ */
public OkHttpDataSource(String userAgent, Predicate<String> contentTypePredicate, public OkHttpDataSource(OkHttpClient client, String userAgent,
TransferListener listener) { Predicate<String> contentTypePredicate, TransferListener listener) {
this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, this(client, userAgent, contentTypePredicate, listener, null);
DEFAULT_READ_TIMEOUT_MILLIS);
} }
/** /**
* @param client An {@link OkHttpClient} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* rejected by the predicate then a {@link InvalidContentTypeException} is * predicate then a
* {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}. * thrown from {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
* @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is * @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control
* interpreted as an infinite timeout. * header. For example, you could force the network response for all requests.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted *
* as an infinite timeout.
*/
public OkHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, true, null, null);
}
/**
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is
* rejected by the predicate then a {@link InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
* @param listener An optional listener.
* @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
* interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
* the default value.
* @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
* as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
* @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
* to HTTPS and vice versa) are enabled.
* @param httpClient An optional {@link OkHttpClient}. Most applications can use a single OkHttpClient for all of
* their HTTP requests. Pass an {@link OkHttpClient} if you already have an
* {@link OkHttpClient} in your application, or you want some customized feature, such as
* monitor calls using {@link Interceptor}.
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control header. For example,
* you could force the network response for all requests.
*/ */
public OkHttpDataSource(String userAgent, Predicate<String> contentTypePredicate, public OkHttpDataSource(OkHttpClient client, String userAgent,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis, Predicate<String> contentTypePredicate, TransferListener listener,
boolean allowCrossProtocolRedirects, OkHttpClient httpClient, CacheControl cacheControl) { CacheControl cacheControl) {
this.okHttpClient = Assertions.checkNotNull(client);
this.userAgent = Assertions.checkNotEmpty(userAgent); this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.listener = listener; this.listener = listener;
this.requestProperties = new HashMap<>();
if (httpClient != null) {
okHttpClient = httpClient;
} else if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
okHttpClient.setConnectTimeout(connectTimeoutMillis, TimeUnit.MILLISECONDS);
okHttpClient.setReadTimeout(readTimeoutMillis, TimeUnit.MILLISECONDS);
if (!allowCrossProtocolRedirects) {
okHttpClient.setFollowSslRedirects(allowCrossProtocolRedirects);
}
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
this.requestProperties = new HashMap<>();
} }
@Override @Override
...@@ -205,6 +156,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -205,6 +156,7 @@ public class OkHttpDataSource implements HttpDataSource {
Request request = makeRequest(dataSpec); Request request = makeRequest(dataSpec);
try { try {
response = okHttpClient.newCall(request).execute(); response = okHttpClient.newCall(request).execute();
responseByteStream = response.body().byteStream();
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec); dataSpec);
...@@ -215,14 +167,14 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -215,14 +167,14 @@ public class OkHttpDataSource implements HttpDataSource {
// Check for a valid response code. // Check for a valid response code.
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
Map<String, List<String>> headers = request.headers().toMultimap(); Map<String, List<String>> headers = request.headers().toMultimap();
closeConnection(); closeConnectionQuietly();
throw new InvalidResponseCodeException(responseCode, headers, dataSpec); throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
} }
// Check for a valid content type. // Check for a valid content type.
String contentType = response.body().contentType().toString(); String contentType = response.body().contentType().toString();
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
closeConnection(); closeConnectionQuietly();
throw new InvalidContentTypeException(contentType, dataSpec); throw new InvalidContentTypeException(contentType, dataSpec);
} }
...@@ -232,23 +184,14 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -232,23 +184,14 @@ public class OkHttpDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping. // Determine the length of the data to be read, after skipping.
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
long contentLength = 0;
try { try {
contentLength = response.body().contentLength(); long contentLength = response.body().contentLength();
} catch (IOException e) {
closeConnection();
throw new HttpDataSourceException(e, dataSpec);
}
bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length
: contentLength != C.LENGTH_UNBOUNDED ? contentLength - bytesToSkip : contentLength != -1 ? contentLength - bytesToSkip
: C.LENGTH_UNBOUNDED; : C.LENGTH_UNBOUNDED;
} else { } catch (IOException e) {
// Gzip is enabled. If the server opts to use gzip then the content length in the response closeConnectionQuietly();
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a throw new HttpDataSourceException(e, dataSpec);
// reliable way to determine whether the gzip was used or not. Always use the dataSpec length
// in this case.
bytesToRead = dataSpec.length;
} }
opened = true; opened = true;
...@@ -276,20 +219,11 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -276,20 +219,11 @@ public class OkHttpDataSource implements HttpDataSource {
if (listener != null) { if (listener != null) {
listener.onTransferEnd(); listener.onTransferEnd();
} }
closeConnection(); closeConnectionQuietly();
} }
} }
/** /**
* Returns the current connection, or null if the source is not currently opened.
*
* @return The current open connection, or null.
*/
protected final OkHttpClient getOkHttpClient() {
return okHttpClient;
}
/**
* Returns the number of bytes that have been skipped since the most recent call to * Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* *
...@@ -311,7 +245,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -311,7 +245,7 @@ public class OkHttpDataSource implements HttpDataSource {
/** /**
* Returns the number of bytes that are still to be read for the current {@link DataSpec}. * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
* <p/> * <p>
* If the total length of the data being read is known, then this length minus {@code bytesRead()} * If the total length of the data being read is known, then this length minus {@code bytesRead()}
* is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned.
* *
...@@ -321,13 +255,16 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -321,13 +255,16 @@ public class OkHttpDataSource implements HttpDataSource {
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead; return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
} }
/**
* Establishes a connection.
*/
private Request makeRequest(DataSpec dataSpec) { private Request makeRequest(DataSpec dataSpec) {
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder().url(url);
.url(url);
if (cacheControl != null) { if (cacheControl != null) {
builder.cacheControl(cacheControl); builder.cacheControl(cacheControl);
} }
...@@ -354,37 +291,8 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -354,37 +291,8 @@ public class OkHttpDataSource implements HttpDataSource {
} }
/** /**
* Handles a redirect.
*
* @param originalUrl The original URL.
* @param location The Location header in the response.
* @return The next URL.
* @throws IOException If redirection isn't possible.
*/
private static URL handleRedirect(URL originalUrl, String location) throws IOException {
if (location == null) {
throw new ProtocolException("Null location redirect");
}
// Form the new url.
URL url = new URL(originalUrl, location);
// Check that the protocol of the new url is supported.
String protocol = url.getProtocol();
if (!"https".equals(protocol) && !"http".equals(protocol)) {
throw new ProtocolException("Unsupported protocol redirect: " + protocol);
}
// Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
// below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
// redirects are disabled, we'll need to uncomment this block of code.
// if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
// throw new ProtocolException("Disallowed cross-protocol redirect ("
// + originalUrl.getProtocol() + " to " + protocol + ")");
// }
return url;
}
/**
* Skips any bytes that need skipping. Else does nothing. * Skips any bytes that need skipping. Else does nothing.
* <p/> * <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
* *
* @throws InterruptedIOException If the thread is interrupted during the operation. * @throws InterruptedIOException If the thread is interrupted during the operation.
...@@ -403,7 +311,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -403,7 +311,7 @@ public class OkHttpDataSource implements HttpDataSource {
while (bytesSkipped != bytesToSkip) { while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = response.body().byteStream().read(skipBuffer, 0, readLength); int read = responseByteStream.read(skipBuffer, 0, readLength);
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedIOException(); throw new InterruptedIOException();
} }
...@@ -423,7 +331,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -423,7 +331,7 @@ public class OkHttpDataSource implements HttpDataSource {
/** /**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}. * index {@code offset}.
* <p/> * <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is * 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. * detected, or an exception is thrown.
* *
...@@ -442,7 +350,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -442,7 +350,7 @@ public class OkHttpDataSource implements HttpDataSource {
return C.RESULT_END_OF_INPUT; return C.RESULT_END_OF_INPUT;
} }
int read = response.body().byteStream().read(buffer, offset, readLength); int read = responseByteStream.read(buffer, offset, readLength);
if (read == -1) { if (read == -1) {
if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) { if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data. // The server closed the connection having not sent sufficient data.
...@@ -459,10 +367,12 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -459,10 +367,12 @@ public class OkHttpDataSource implements HttpDataSource {
} }
/** /**
* Closes the current connection, if there is one. * Closes the current connection quietly, if there is one.
*/ */
private void closeConnection() { private void closeConnectionQuietly() {
closeQuietly(response.body()); Util.closeQuietly(response.body());
response = null; response = null;
responseByteStream = null;
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment