diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 5e771e8949..98d2eed076 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -16,10 +16,13 @@ package com.google.android.exoplayer2.ext.cronet; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -48,9 +51,10 @@ import org.chromium.net.UrlResponseInfo; /** * DataSource without intermediate buffer based on Cronet API set using UrlRequest. + * *

This class's methods are organized in the sequence of expected calls. */ -public class CronetDataSource extends UrlRequest.Callback implements HttpDataSource { +public class CronetDataSource extends BaseDataSource implements HttpDataSource { /** * Thrown when an error is encountered when trying to open a {@link CronetDataSource}. @@ -96,6 +100,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou */ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + /* package */ final UrlRequest.Callback urlRequestCallback; + private static final String TAG = "CronetDataSource"; private static final String CONTENT_TYPE = "Content-Type"; private static final String SET_COOKIE = "Set-Cookie"; @@ -109,7 +115,6 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou private final CronetEngine cronetEngine; private final Executor executor; private final Predicate contentTypePredicate; - private final TransferListener listener; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; @@ -144,41 +149,49 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener) { + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener listener) { this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, null, false); } /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @param defaultRequestProperties The default request properties to be used. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, RequestProperties defaultRequestProperties) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, false); @@ -186,14 +199,14 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will handle responses. - * This may be a direct executor (i.e. executes tasks on the calling thread) in order - * to avoid a thread hop from Cronet's internal network thread to the response handling - * thread. However, to avoid slowing down overall network performance, care must be taken - * to make sure response handling is a fast operation when using a direct executor. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may + * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread + * hop from Cronet's internal network thread to the response handling thread. However, to + * avoid slowing down overall network performance, care must be taken to make sure response + * handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then an {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. * @param connectTimeoutMs The connection timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds. @@ -202,23 +215,37 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to * the redirect url in the "Cookie" header. */ - public CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { + public CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + RequestProperties defaultRequestProperties, + boolean handleSetCookieRequests) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, handleSetCookieRequests); } - /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, - Predicate contentTypePredicate, TransferListener listener, - int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, - RequestProperties defaultRequestProperties, boolean handleSetCookieRequests) { + /* package */ CronetDataSource( + CronetEngine cronetEngine, + Executor executor, + Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMs, + int readTimeoutMs, + boolean resetTimeoutOnRedirects, + Clock clock, + RequestProperties defaultRequestProperties, + boolean handleSetCookieRequests) { + super(DataSource.TYPE_REMOTE); + this.urlRequestCallback = new UrlRequestCallback(); this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; @@ -227,6 +254,9 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou this.handleSetCookieRequests = handleSetCookieRequests; requestProperties = new RequestProperties(); operation = new ConditionVariable(); + if (listener != null) { + addTransferListener(listener); + } } // HttpDataSource implementation. @@ -324,9 +354,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -392,9 +420,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @@ -413,107 +439,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou finished = false; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } - // UrlRequest.Callback implementation - - @Override - public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info, - String newLocationUrl) { - if (request != currentUrlRequest) { - return; - } - if (currentDataSpec.postBody != null) { - int responseCode = info.getHttpStatusCode(); - // The industry standard is to disregard POST redirects when the status code is 307 or 308. - // For other redirect response codes the POST request is converted to a GET request and the - // redirect is followed. - if (responseCode == 307 || responseCode == 308) { - exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(), - currentDataSpec); - operation.open(); - return; - } - } - if (resetTimeoutOnRedirects) { - resetConnectTimeout(); - } - - Map> headers = info.getAllHeaders(); - if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { - request.followRedirect(); - } else { - currentUrlRequest.cancel(); - DataSpec redirectUrlDataSpec = new DataSpec(Uri.parse(newLocationUrl), - currentDataSpec.postBody, currentDataSpec.absoluteStreamPosition, - currentDataSpec.position, currentDataSpec.length, currentDataSpec.key, - currentDataSpec.flags); - UrlRequest.Builder requestBuilder; - try { - requestBuilder = buildRequestBuilder(redirectUrlDataSpec); - } catch (IOException e) { - exception = e; - return; - } - String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE)); - attachCookies(requestBuilder, cookieHeadersValue); - currentUrlRequest = requestBuilder.build(); - currentUrlRequest.start(); - } - } - - @Override - public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) { - if (request != currentUrlRequest) { - return; - } - responseInfo = info; - operation.open(); - } - - @Override - public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info, - ByteBuffer buffer) { - if (request != currentUrlRequest) { - return; - } - operation.open(); - } - - @Override - public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) { - if (request != currentUrlRequest) { - return; - } - finished = true; - operation.open(); - } - - @Override - public synchronized void onFailed(UrlRequest request, UrlResponseInfo info, - CronetException error) { - if (request != currentUrlRequest) { - return; - } - if (error instanceof NetworkException - && ((NetworkException) error).getErrorCode() - == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) { - exception = new UnknownHostException(); - } else { - exception = error; - } - operation.open(); - } - // Internal methods. private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException { - UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder( - dataSpec.uri.toString(), this, executor).allowDirectExecutor(); + UrlRequest.Builder requestBuilder = + cronetEngine + .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor) + .allowDirectExecutor(); // Set the headers. boolean isContentTypeHeaderSet = false; if (defaultRequestProperties != null) { @@ -656,4 +592,99 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou return list == null || list.isEmpty(); } + private final class UrlRequestCallback extends UrlRequest.Callback { + + @Override + public synchronized void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + if (request != currentUrlRequest) { + return; + } + if (currentDataSpec.postBody != null) { + int responseCode = info.getHttpStatusCode(); + // The industry standard is to disregard POST redirects when the status code is 307 or 308. + // For other redirect response codes the POST request is converted to a GET request and the + // redirect is followed. + if (responseCode == 307 || responseCode == 308) { + exception = + new InvalidResponseCodeException(responseCode, info.getAllHeaders(), currentDataSpec); + operation.open(); + return; + } + } + if (resetTimeoutOnRedirects) { + resetConnectTimeout(); + } + + Map> headers = info.getAllHeaders(); + if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) { + request.followRedirect(); + } else { + currentUrlRequest.cancel(); + DataSpec redirectUrlDataSpec = + new DataSpec( + Uri.parse(newLocationUrl), + currentDataSpec.postBody, + currentDataSpec.absoluteStreamPosition, + currentDataSpec.position, + currentDataSpec.length, + currentDataSpec.key, + currentDataSpec.flags); + UrlRequest.Builder requestBuilder; + try { + requestBuilder = buildRequestBuilder(redirectUrlDataSpec); + } catch (IOException e) { + exception = e; + return; + } + String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE)); + attachCookies(requestBuilder, cookieHeadersValue); + currentUrlRequest = requestBuilder.build(); + currentUrlRequest.start(); + } + } + + @Override + public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + if (request != currentUrlRequest) { + return; + } + responseInfo = info; + operation.open(); + } + + @Override + public synchronized void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) { + if (request != currentUrlRequest) { + return; + } + operation.open(); + } + + @Override + public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) { + if (request != currentUrlRequest) { + return; + } + finished = true; + operation.open(); + } + + @Override + public synchronized void onFailed( + UrlRequest request, UrlResponseInfo info, CronetException error) { + if (request != currentUrlRequest) { + return; + } + if (error instanceof NetworkException + && ((NetworkException) error).getErrorCode() + == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) { + exception = new UnknownHostException(); + } else { + exception = error; + } + operation.open(); + } + } } diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4e990cd027..7342b8282a 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +32,7 @@ import android.net.Uri; import android.os.ConditionVariable; import android.os.SystemClock; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -87,7 +87,7 @@ public final class CronetDataSourceTest { @Mock private UrlRequest.Builder mockUrlRequestBuilder; @Mock private UrlRequest mockUrlRequest; @Mock private Predicate mockContentTypePredicate; - @Mock private TransferListener mockTransferListener; + @Mock private TransferListener mockTransferListener; @Mock private Executor mockExecutor; @Mock private NetworkException mockNetworkException; @Mock private CronetEngine mockCronetEngine; @@ -99,18 +99,17 @@ public final class CronetDataSourceTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - false)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + false); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) @@ -172,9 +171,10 @@ public final class CronetDataSourceTest { @Override public Object answer(InvocationOnMock invocation) throws Throwable { // Invoke the callback for the previous request. - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, testUrlResponseInfo, mockNetworkException); - dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest2, testUrlResponseInfo); return null; } }) @@ -601,7 +601,7 @@ public final class CronetDataSourceTest { } @Test - public void testConnectResponseBeforeTimeout() throws InterruptedException { + public void testConnectResponseBeforeTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch openLatch = new CountDownLatch(1); @@ -625,12 +625,12 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(openLatch); // The response arrives just in time. - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo); openLatch.await(); } @Test - public void testRedirectIncreasesConnectionTimeout() throws InterruptedException { + public void testRedirectIncreasesConnectionTimeout() throws Exception { long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); final CountDownLatch timedOutLatch = new CountDownLatch(1); @@ -659,7 +659,7 @@ public final class CronetDataSourceTest { ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1"); long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; @@ -667,7 +667,7 @@ public final class CronetDataSourceTest { // We should still be trying to open as we approach the new timeout. assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2"); newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; @@ -700,18 +700,17 @@ public final class CronetDataSourceTest { testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() throws HttpDataSourceException { dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -732,18 +731,17 @@ public final class CronetDataSourceTest { throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -772,18 +770,17 @@ public final class CronetDataSourceTest { public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() throws HttpDataSourceException { dataSourceUnderTest = - spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true)); + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true); mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -889,7 +886,8 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); return null; } }) @@ -902,7 +900,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, createUrlResponseInfo(307), // statusCode "http://redirect.location.com"); @@ -920,12 +918,13 @@ public final class CronetDataSourceTest { public Object answer(InvocationOnMock invocation) throws Throwable { if (!redirectCalled) { redirectCalled = true; - dataSourceUnderTest.onRedirectReceived( + dataSourceUnderTest.urlRequestCallback.onRedirectReceived( mockUrlRequest, createUrlResponseInfoWithUrl("http://example.com/video", 300), "http://example.com/video/redirect"); } else { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); } return null; } @@ -939,7 +938,8 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onResponseStarted( + mockUrlRequest, testUrlResponseInfo); return null; } }) @@ -952,7 +952,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, createUrlResponseInfo(500), // statusCode mockNetworkException); @@ -970,14 +970,15 @@ public final class CronetDataSourceTest { @Override public Void answer(InvocationOnMock invocation) throws Throwable { if (positionAndRemaining[1] == 0) { - dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); + dataSourceUnderTest.urlRequestCallback.onSucceeded( + mockUrlRequest, testUrlResponseInfo); } else { ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); positionAndRemaining[0] += readLength; positionAndRemaining[1] -= readLength; - dataSourceUnderTest.onReadCompleted( + dataSourceUnderTest.urlRequestCallback.onReadCompleted( mockUrlRequest, testUrlResponseInfo, inputBuffer); } return null; @@ -992,7 +993,7 @@ public final class CronetDataSourceTest { new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( + dataSourceUnderTest.urlRequestCallback.onFailed( mockUrlRequest, createUrlResponseInfo(500), // statusCode mockNetworkException); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index bc0c2e9047..b006e22e5d 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -20,6 +20,8 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -42,10 +44,8 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -/** - * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. - */ -public class OkHttpDataSource implements HttpDataSource { +/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */ +public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp"); @@ -58,7 +58,6 @@ public class OkHttpDataSource implements HttpDataSource { @Nullable private final String userAgent; @Nullable private final Predicate contentTypePredicate; - @Nullable private final TransferListener listener; @Nullable private final CacheControl cacheControl; @Nullable private final RequestProperties defaultRequestProperties; @@ -90,13 +89,15 @@ public class OkHttpDataSource implements HttpDataSource { * by the source. * @param userAgent An optional User-Agent string. * @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)}. + * predicate then a {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. */ - public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + public OkHttpDataSource( + @NonNull Call.Factory callFactory, + @Nullable String userAgent, @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener) { + @Nullable TransferListener listener) { this(callFactory, userAgent, contentTypePredicate, listener, null, null); } @@ -105,24 +106,30 @@ public class OkHttpDataSource implements HttpDataSource { * by the source. * @param userAgent An optional User-Agent string. * @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)}. + * predicate then a {@link InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to - * the server as HTTP headers on every request. + * the server as HTTP headers on every request. */ - public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + public OkHttpDataSource( + @NonNull Call.Factory callFactory, + @Nullable String userAgent, @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) { + @Nullable TransferListener listener, + @Nullable CacheControl cacheControl, + @Nullable RequestProperties defaultRequestProperties) { + super(DataSource.TYPE_REMOTE); this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.cacheControl = cacheControl; this.defaultRequestProperties = defaultRequestProperties; this.requestProperties = new RequestProperties(); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -203,9 +210,7 @@ public class OkHttpDataSource implements HttpDataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesToRead; } @@ -224,9 +229,7 @@ public class OkHttpDataSource implements HttpDataSource { public void close() throws HttpDataSourceException { if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); closeConnectionQuietly(); } } @@ -333,9 +336,7 @@ public class OkHttpDataSource implements HttpDataSource { throw new EOFException(); } bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); } // Release the shared skip buffer. @@ -378,9 +379,7 @@ public class OkHttpDataSource implements HttpDataSource { } bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); return read; } diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java index 0601af4a2f..f6b0705b0a 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; @@ -26,17 +27,13 @@ import java.io.IOException; import net.butterflytv.rtmp_client.RtmpClient; import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException; -/** - * A Real-Time Messaging Protocol (RTMP) {@link DataSource}. - */ -public final class RtmpDataSource implements DataSource { +/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */ +public final class RtmpDataSource extends BaseDataSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp"); } - @Nullable private final TransferListener listener; - private RtmpClient rtmpClient; private Uri uri; @@ -44,11 +41,12 @@ public final class RtmpDataSource implements DataSource { this(null); } - /** - * @param listener An optional listener. - */ - public RtmpDataSource(@Nullable TransferListener listener) { - this.listener = listener; + /** @param listener An optional listener. */ + public RtmpDataSource(@Nullable TransferListener listener) { + super(DataSource.TYPE_REMOTE); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -57,9 +55,7 @@ public final class RtmpDataSource implements DataSource { rtmpClient.open(dataSpec.uri.toString(), false); this.uri = dataSpec.uri; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return C.LENGTH_UNSET; } @@ -69,9 +65,7 @@ public final class RtmpDataSource implements DataSource { if (bytesRead == -1) { return C.RESULT_END_OF_INPUT; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @@ -79,9 +73,7 @@ public final class RtmpDataSource implements DataSource { public void close() { if (uri != null) { uri = null; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } if (rtmpClient != null) { rtmpClient.close(); diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java index 0510e9c7da..a1161878ff 100644 --- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java @@ -25,17 +25,14 @@ import com.google.android.exoplayer2.upstream.TransferListener; */ public final class RtmpDataSourceFactory implements DataSource.Factory { - @Nullable - private final TransferListener listener; + private final @Nullable TransferListener listener; public RtmpDataSourceFactory() { this(null); } - /** - * @param listener An optional listener. - */ - public RtmpDataSourceFactory(@Nullable TransferListener listener) { + /** @param listener An optional listener. */ + public RtmpDataSourceFactory(@Nullable TransferListener listener) { this.listener = listener; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index d0b18bb765..8409c8bacb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -18,15 +18,14 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -/** - * A {@link DataSource} for reading from a local asset. - */ -public final class AssetDataSource implements DataSource { +/** A {@link DataSource} for reading from a local asset. */ +public final class AssetDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading a local asset. @@ -40,10 +39,9 @@ public final class AssetDataSource implements DataSource { } private final AssetManager assetManager; - private final TransferListener listener; - private Uri uri; - private InputStream inputStream; + private @Nullable Uri uri; + private @Nullable InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -58,9 +56,12 @@ public final class AssetDataSource implements DataSource { * @param context A context. * @param listener An optional listener. */ - public AssetDataSource(Context context, TransferListener listener) { + public AssetDataSource(Context context, @Nullable TransferListener listener) { + super(DataSource.TYPE_LOCAL); this.assetManager = context.getAssets(); - this.listener = listener; + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -96,9 +97,7 @@ public final class AssetDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -129,14 +128,12 @@ public final class AssetDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -153,9 +150,7 @@ public final class AssetDataSource implements DataSource { inputStream = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index e5311e783b..726e72b1a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -16,25 +16,26 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -/** - * A {@link DataSource} for reading from a byte array. - */ -public final class ByteArrayDataSource implements DataSource { +/** A {@link DataSource} for reading from a byte array. */ +public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private Uri uri; + private @Nullable Uri uri; private int readPosition; private int bytesRemaining; + private boolean opened; /** * @param data The data to be read. */ public ByteArrayDataSource(byte[] data) { + super(DataSource.TYPE_LOCAL); Assertions.checkNotNull(data); Assertions.checkArgument(data.length > 0); this.data = data; @@ -50,6 +51,8 @@ public final class ByteArrayDataSource implements DataSource { throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length + "], length: " + data.length); } + opened = true; + transferStarted(dataSpec); return bytesRemaining; } @@ -65,16 +68,21 @@ public final class ByteArrayDataSource implements DataSource { System.arraycopy(data, readPosition, buffer, offset, readLength); readPosition += readLength; bytesRemaining -= readLength; + bytesTransferred(readLength); return readLength; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @Override public void close() throws IOException { + if (opened) { + opened = false; + transferEnded(); + } uri = null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 87642e0eba..ae37139889 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -19,6 +19,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.FileInputStream; @@ -26,10 +27,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.FileChannel; -/** - * A {@link DataSource} for reading from a content URI. - */ -public final class ContentDataSource implements DataSource { +/** A {@link DataSource} for reading from a content URI. */ +public final class ContentDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a content URI. @@ -43,11 +42,10 @@ public final class ContentDataSource implements DataSource { } private final ContentResolver resolver; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private FileInputStream inputStream; + private @Nullable Uri uri; + private @Nullable AssetFileDescriptor assetFileDescriptor; + private @Nullable FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -62,9 +60,13 @@ public final class ContentDataSource implements DataSource { * @param context A context. * @param listener An optional listener. */ - public ContentDataSource(Context context, TransferListener listener) { + public ContentDataSource( + Context context, @Nullable TransferListener listener) { + super(DataSource.TYPE_LOCAL); this.resolver = context.getContentResolver(); - this.listener = listener; + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -102,9 +104,7 @@ public final class ContentDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -136,14 +136,12 @@ public final class ContentDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -168,9 +166,7 @@ public final class ContentDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index 33d67f3f46..cd724ccd40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -23,16 +24,18 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.URLDecoder; -/** - * A {@link DataSource} for reading data URLs, as defined by RFC 2397. - */ -public final class DataSchemeDataSource implements DataSource { +/** A {@link DataSource} for reading data URLs, as defined by RFC 2397. */ +public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private DataSpec dataSpec; + private @Nullable DataSpec dataSpec; private int bytesRead; - private byte[] data; + private @Nullable byte[] data; + + public DataSchemeDataSource() { + super(DataSource.TYPE_LOCAL); + } @Override public long open(DataSpec dataSpec) throws IOException { @@ -57,6 +60,7 @@ public final class DataSchemeDataSource implements DataSource { // TODO: Add support for other charsets. data = URLDecoder.decode(dataString, C.ASCII_NAME).getBytes(); } + transferStarted(dataSpec); return data.length; } @@ -72,18 +76,22 @@ public final class DataSchemeDataSource implements DataSource { readLength = Math.min(readLength, remainingBytes); System.arraycopy(data, bytesRead, buffer, offset, readLength); bytesRead += readLength; + bytesTransferred(readLength); return readLength; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } @Override public void close() throws IOException { + if (data != null) { + data = null; + transferEnded(); + } dataSpec = null; - data = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 1d545e24e3..de5cc573b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; @@ -41,13 +42,13 @@ import java.util.regex.Pattern; /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. - *

- * 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 using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, + * + *

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 using the {@link + * #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, * RequestProperties)} constructor and passing {@code true} as the second last argument. */ -public class DefaultHttpDataSource implements HttpDataSource { +public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { /** * The default connection timeout, in milliseconds. @@ -69,14 +70,13 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final Predicate contentTypePredicate; - private final RequestProperties defaultRequestProperties; + private final @Nullable Predicate contentTypePredicate; + private final @Nullable RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private final TransferListener listener; - private DataSpec dataSpec; - private HttpURLConnection connection; - private InputStream inputStream; + private @Nullable DataSpec dataSpec; + private @Nullable HttpURLConnection connection; + private @Nullable InputStream inputStream; private boolean opened; private long bytesToSkip; @@ -88,22 +88,24 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @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 HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate) { + public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { this(userAgent, contentTypePredicate, 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 HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. * @param listener An optional listener. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener) { + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener) { this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); } @@ -111,16 +113,19 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @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 HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.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. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMillis, int readTimeoutMillis) { this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, null); @@ -129,35 +134,42 @@ public class DefaultHttpDataSource implements HttpDataSource { /** * @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 HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * predicate then a {@link HttpDataSource.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. + * 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 defaultRequestProperties The default request properties to be sent to the server as - * HTTP headers or {@code null} if not required. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects, - RequestProperties defaultRequestProperties) { + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(DataSource.TYPE_REMOTE); this.userAgent = Assertions.checkNotEmpty(userAgent); this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.requestProperties = new RequestProperties(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; this.defaultRequestProperties = defaultRequestProperties; + if (listener != null) { + addTransferListener(listener); + } } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } @@ -254,9 +266,7 @@ public class DefaultHttpDataSource implements HttpDataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesToRead; } @@ -287,9 +297,7 @@ public class DefaultHttpDataSource implements HttpDataSource { closeConnectionQuietly(); if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } @@ -534,9 +542,7 @@ public class DefaultHttpDataSource implements HttpDataSource { throw new EOFException(); } bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); } // Release the shared skip buffer. @@ -579,9 +585,7 @@ public class DefaultHttpDataSource implements HttpDataSource { } bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); return read; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 898d2169b3..1fad7fc5b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -16,15 +16,14 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; import java.io.RandomAccessFile; -/** - * A {@link DataSource} for reading local files. - */ -public final class FileDataSource implements DataSource { +/** A {@link DataSource} for reading local files. */ +public final class FileDataSource extends BaseDataSource { /** * Thrown when IOException is encountered during local file read operation. @@ -37,10 +36,8 @@ public final class FileDataSource implements DataSource { } - private final TransferListener listener; - - private RandomAccessFile file; - private Uri uri; + private @Nullable RandomAccessFile file; + private @Nullable Uri uri; private long bytesRemaining; private boolean opened; @@ -48,11 +45,12 @@ public final class FileDataSource implements DataSource { this(null); } - /** - * @param listener An optional listener. - */ - public FileDataSource(TransferListener listener) { - this.listener = listener; + /** @param listener An optional listener. */ + public FileDataSource(@Nullable TransferListener listener) { + super(DataSource.TYPE_LOCAL); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -71,9 +69,7 @@ public final class FileDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -94,9 +90,7 @@ public final class FileDataSource implements DataSource { if (bytesRead > 0) { bytesRemaining -= bytesRead; - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); } return bytesRead; @@ -104,7 +98,7 @@ public final class FileDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -121,9 +115,7 @@ public final class FileDataSource implements DataSource { file = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 2accbfc584..4578a285a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -15,18 +15,20 @@ */ package com.google.android.exoplayer2.upstream; +import android.support.annotation.Nullable; + /** * A {@link DataSource.Factory} that produces {@link FileDataSource}. */ public final class FileDataSourceFactory implements DataSource.Factory { - private final TransferListener listener; + private final @Nullable TransferListener listener; public FileDataSourceFactory() { this(null); } - public FileDataSourceFactory(TransferListener listener) { + public FileDataSourceFactory(@Nullable TransferListener listener) { this.listener = listener; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 1b58c7e095..3b101a9765 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import java.io.EOFException; @@ -28,12 +29,12 @@ import java.io.InputStream; /** * A {@link DataSource} for reading a raw resource inside the APK. - *

- * URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where + * + *

URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can * be used to build {@link Uri}s in this format. */ -public final class RawResourceDataSource implements DataSource { +public final class RawResourceDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a raw resource. @@ -62,11 +63,10 @@ public final class RawResourceDataSource implements DataSource { public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + private @Nullable Uri uri; + private @Nullable AssetFileDescriptor assetFileDescriptor; + private @Nullable InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -81,10 +81,13 @@ public final class RawResourceDataSource implements DataSource { * @param context A context. * @param listener An optional listener. */ - public RawResourceDataSource(Context context, - TransferListener listener) { + public RawResourceDataSource( + Context context, @Nullable TransferListener listener) { + super(DataSource.TYPE_LOCAL); this.resources = context.getResources(); - this.listener = listener; + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -124,9 +127,7 @@ public final class RawResourceDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -158,14 +159,12 @@ public final class RawResourceDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -190,9 +189,7 @@ public final class RawResourceDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index 68a04d9182..5fb4f952e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; import java.net.DatagramPacket; @@ -25,10 +26,8 @@ import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.SocketException; -/** - * A UDP {@link DataSource}. - */ -public final class UdpDataSource implements DataSource { +/** A UDP {@link DataSource}. */ +public final class UdpDataSource extends BaseDataSource { /** * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}. @@ -51,24 +50,21 @@ public final class UdpDataSource implements DataSource { */ public static final int DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; - private final TransferListener listener; private final int socketTimeoutMillis; private final byte[] packetBuffer; private final DatagramPacket packet; - private Uri uri; - private DatagramSocket socket; - private MulticastSocket multicastSocket; - private InetAddress address; - private InetSocketAddress socketAddress; + private @Nullable Uri uri; + private @Nullable DatagramSocket socket; + private @Nullable MulticastSocket multicastSocket; + private @Nullable InetAddress address; + private @Nullable InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; - /** - * @param listener An optional listener. - */ - public UdpDataSource(TransferListener listener) { + /** @param listener An optional listener. */ + public UdpDataSource(@Nullable TransferListener listener) { this(listener, DEFAULT_MAX_PACKET_SIZE); } @@ -76,7 +72,7 @@ public final class UdpDataSource implements DataSource { * @param listener An optional listener. * @param maxPacketSize The maximum datagram packet size, in bytes. */ - public UdpDataSource(TransferListener listener, int maxPacketSize) { + public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) { this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS); } @@ -86,12 +82,17 @@ public final class UdpDataSource implements DataSource { * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted * as an infinite timeout. */ - public UdpDataSource(TransferListener listener, int maxPacketSize, + public UdpDataSource( + @Nullable TransferListener listener, + int maxPacketSize, int socketTimeoutMillis) { - this.listener = listener; + super(DataSource.TYPE_REMOTE); this.socketTimeoutMillis = socketTimeoutMillis; packetBuffer = new byte[maxPacketSize]; packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); + if (listener != null) { + addTransferListener(listener); + } } @Override @@ -121,9 +122,7 @@ public final class UdpDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return C.LENGTH_UNSET; } @@ -141,9 +140,7 @@ public final class UdpDataSource implements DataSource { throw new UdpDataSourceException(e); } packetRemaining = packet.getLength(); - if (listener != null) { - listener.onBytesTransferred(this, packetRemaining); - } + bytesTransferred(packetRemaining); } int packetOffset = packet.getLength() - packetRemaining; @@ -154,7 +151,7 @@ public final class UdpDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return uri; } @@ -178,9 +175,7 @@ public final class UdpDataSource implements DataSource { packetRemaining = 0; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index de623b59c9..b7f9f6793c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -32,18 +33,20 @@ import java.util.ArrayList; * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} * instance which determines the response to data access calls. */ -public class FakeDataSource implements DataSource { +public class FakeDataSource extends BaseDataSource { /** * Factory to create a {@link FakeDataSource}. */ public static class Factory implements DataSource.Factory { - protected final TransferListener transferListener; + protected final TransferListener transferListener; protected FakeDataSet fakeDataSet; + protected @DataSource.Type int dataSourceType; - public Factory(@Nullable TransferListener transferListener) { + public Factory(@Nullable TransferListener transferListener) { this.transferListener = transferListener; + this.dataSourceType = DataSource.TYPE_LOCAL; } public final Factory setFakeDataSet(FakeDataSet fakeDataSet) { @@ -51,19 +54,23 @@ public class FakeDataSource implements DataSource { return this; } - @Override - public DataSource createDataSource() { - return new FakeDataSource(fakeDataSet, transferListener); + public final Factory setDataSourceType(@DataSource.Type int dataSourceType) { + this.dataSourceType = dataSourceType; + return this; } + @Override + public DataSource createDataSource() { + return new FakeDataSource(fakeDataSet, transferListener, dataSourceType); + } } private final FakeDataSet fakeDataSet; - private final TransferListener transferListener; private final ArrayList openedDataSpecs; private Uri uri; - private boolean opened; + private boolean openCalled; + private boolean sourceOpened; private FakeData fakeData; private int currentSegmentIndex; private long bytesRemaining; @@ -73,15 +80,20 @@ public class FakeDataSource implements DataSource { } public FakeDataSource(FakeDataSet fakeDataSet) { - this(fakeDataSet, null); + this(fakeDataSet, null, DataSource.TYPE_LOCAL); } - public FakeDataSource(FakeDataSet fakeDataSet, - @Nullable TransferListener transferListener) { + public FakeDataSource( + FakeDataSet fakeDataSet, + @Nullable TransferListener transferListener, + @DataSource.Type int dataSourceType) { + super(dataSourceType); Assertions.checkNotNull(fakeDataSet); this.fakeDataSet = fakeDataSet; - this.transferListener = transferListener; this.openedDataSpecs = new ArrayList<>(); + if (transferListener != null) { + addTransferListener(transferListener); + } } public final FakeDataSet getDataSet() { @@ -90,9 +102,9 @@ public class FakeDataSource implements DataSource { @Override public final long open(DataSpec dataSpec) throws IOException { - Assertions.checkState(!opened); + Assertions.checkState(!openCalled); + openCalled = true; // DataSpec requires a matching close call even if open fails. - opened = true; uri = dataSpec.uri; openedDataSpecs.add(dataSpec); @@ -129,9 +141,8 @@ public class FakeDataSource implements DataSource { currentSegmentIndex++; } } - if (transferListener != null) { - transferListener.onTransferStart(this, dataSpec); - } + sourceOpened = true; + transferStarted(dataSpec); // Configure bytesRemaining, and return. if (dataSpec.length == C.LENGTH_UNSET) { bytesRemaining = totalLength - dataSpec.position; @@ -144,7 +155,7 @@ public class FakeDataSource implements DataSource { @Override public final int read(byte[] buffer, int offset, int readLength) throws IOException { - Assertions.checkState(opened); + Assertions.checkState(sourceOpened); while (true) { if (currentSegmentIndex == fakeData.getSegments().size() || bytesRemaining == 0) { return C.RESULT_END_OF_INPUT; @@ -171,9 +182,7 @@ public class FakeDataSource implements DataSource { System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); } onDataRead(readLength); - if (transferListener != null) { - transferListener.onBytesTransferred(this, readLength); - } + bytesTransferred(readLength); bytesRemaining -= readLength; current.bytesRead += readLength; if (current.bytesRead == current.length) { @@ -191,8 +200,8 @@ public class FakeDataSource implements DataSource { @Override public final void close() throws IOException { - Assertions.checkState(opened); - opened = false; + Assertions.checkState(openCalled); + openCalled = false; uri = null; if (fakeData != null && currentSegmentIndex < fakeData.getSegments().size()) { Segment current = fakeData.getSegments().get(currentSegmentIndex); @@ -200,8 +209,9 @@ public class FakeDataSource implements DataSource { current.exceptionCleared = true; } } - if (transferListener != null) { - transferListener.onTransferEnd(this); + if (sourceOpened) { + sourceOpened = false; + transferEnded(); } fakeData = null; } @@ -219,7 +229,7 @@ public class FakeDataSource implements DataSource { /** Returns whether the data source is currently opened. */ public final boolean isOpened() { - return opened; + return sourceOpened; } protected void onDataRead(int bytesRead) throws IOException {