Add OkHttp extension to V2.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=118775995
This commit is contained in:
olly 2016-04-01 07:01:33 -07:00 committed by Oliver Woodman
parent b041b72bae
commit e4d815d164
7 changed files with 508 additions and 0 deletions

View file

@ -0,0 +1,9 @@
# ExoPlayer OkHttp Extension #
## Description ##
The OkHttp Extension is an [HttpDataSource][] implementation using Square's [OkHttp][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html
[OkHttp]: https://square.github.io/okhttp/

View file

@ -0,0 +1,42 @@
// Copyright (C) 2014 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.
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 9
targetSdkVersion 23
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
compile project(':library')
compile('com.squareup.okhttp3:okhttp:+') {
exclude group: 'org.json'
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ExoPlayerLib"/>
<classpathentry kind="src" path="java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ExoPlayerExt-OkHttp</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.okhttp">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23"/>
</manifest>

View file

@ -0,0 +1,376 @@
/*
* Copyright (C) 2014 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.exoplayer.ext.okhttp;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.TransferListener;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate;
import android.net.Uri;
import okhttp3.CacheControl;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}.
*/
public class OkHttpDataSource implements HttpDataSource {
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final OkHttpClient okHttpClient;
private final String userAgent;
private final Predicate<String> contentTypePredicate;
private final TransferListener listener;
private final CacheControl cacheControl;
private final HashMap<String, String> requestProperties;
private DataSpec dataSpec;
private Response response;
private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead;
/**
* @param client An {@link OkHttpClient} for use by the source.
* @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 com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
*/
public OkHttpDataSource(OkHttpClient client, String userAgent,
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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a
* {@link com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
* @param listener An optional listener.
*/
public OkHttpDataSource(OkHttpClient client, String userAgent,
Predicate<String> contentTypePredicate, TransferListener listener) {
this(client, userAgent, contentTypePredicate, listener, null);
}
/**
* @param client An {@link OkHttpClient} for use by the source.
* @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 com.google.android.exoplayer.upstream.HttpDataSource.InvalidContentTypeException} is
* thrown from {@link #open(DataSpec)}.
* @param listener An optional listener.
* @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(OkHttpClient client, String userAgent,
Predicate<String> contentTypePredicate, TransferListener listener,
CacheControl cacheControl) {
this.okHttpClient = Assertions.checkNotNull(client);
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.cacheControl = cacheControl;
this.requestProperties = new HashMap<>();
}
@Override
public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return response == null ? null : response.headers().toMultimap();
}
@Override
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (requestProperties) {
requestProperties.put(name, value);
}
}
@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (requestProperties) {
requestProperties.remove(name);
}
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
}
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
Request request = makeRequest(dataSpec);
try {
response = okHttpClient.newCall(request).execute();
responseByteStream = response.body().byteStream();
} catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
dataSpec);
}
int responseCode = response.code();
// Check for a valid response code.
if (!response.isSuccessful()) {
Map<String, List<String>> headers = request.headers().toMultimap();
closeConnectionQuietly();
throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
}
// Check for a valid content type.
MediaType mediaType = response.body().contentType();
String contentType = mediaType != null ? mediaType.toString() : null;
if (contentTypePredicate != null && !contentTypePredicate.evaluate(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.
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_UNBOUNDED ? dataSpec.length
: contentLength != -1 ? contentLength - bytesToSkip
: C.LENGTH_UNBOUNDED;
opened = true;
if (listener != null) {
listener.onTransferStart();
}
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(e, dataSpec);
}
}
@Override
public void close() throws HttpDataSourceException {
if (opened) {
opened = false;
if (listener != null) {
listener.onTransferEnd();
}
closeConnectionQuietly();
}
}
/**
* Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}.
*
* @return The number of bytes skipped.
*/
protected final long bytesSkipped() {
return bytesSkipped;
}
/**
* Returns the number of bytes that have been read since the most recent call to
* {@link #open(DataSpec)}.
*
* @return The number of bytes read.
*/
protected final long bytesRead() {
return bytesRead;
}
/**
* Returns the number of bytes that are still to be read for the current {@link DataSpec}.
* <p>
* 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.
*
* @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
*/
protected final long bytesRemaining() {
return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
}
/**
* Establishes a connection.
*/
private Request makeRequest(DataSpec dataSpec) {
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
Request.Builder builder = new Request.Builder().url(url);
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
synchronized (requestProperties) {
for (Map.Entry<String, String> property : requestProperties.entrySet()) {
builder.addHeader(property.getKey(), property.getValue());
}
}
if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNBOUNDED) {
rangeRequest += (position + length - 1);
}
builder.addHeader("Range", rangeRequest);
}
builder.addHeader("User-Agent", userAgent);
if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity");
}
if (dataSpec.postBody != null) {
builder.post(RequestBody.create(null, dataSpec.postBody));
}
return builder.build();
}
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
*
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
}
// Acquire the shared skip buffer.
byte[] skipBuffer = skipBufferReference.getAndSet(null);
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = responseByteStream.read(skipBuffer, 0, readLength);
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
}
bytesSkipped += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
}
// Release the shared skip buffer.
skipBufferReference.set(skipBuffer);
}
/**
* 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 {
readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength
: (int) Math.min(readLength, bytesToRead - bytesRead);
if (readLength == 0) {
// We've read all of the requested data.
return C.RESULT_END_OF_INPUT;
}
int read = responseByteStream.read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
return read;
}
/**
* Closes the current connection quietly, if there is one.
*/
private void closeConnectionQuietly() {
response.body().close();
response = null;
responseByteStream = null;
}
}

View file

@ -0,0 +1,16 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-23
android.library=true
android.library.reference.1=../../../../library/src/main