diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5816b1bd29..f3e6f46696 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -73,6 +73,8 @@ ([#1807](https://github.com/google/ExoPlayer/issues/1807)). * OkHttp extension: * Add `OkHttpDataSource.Factory` and deprecate `OkHttpDataSourceFactory`. +* Cronet extension: + * Add `CronetDataSource.Factory` and deprecate `CronetDataSourceFactory`. * Media2 extension * Make media2-extension depend on AndroidX media2:media2-session:1.1.0 to fix a deadlock while creating PlaybackStateCompat internally. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index 2d15dfcbb4..7774ee2bbe 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -20,7 +20,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.ExoDatabaseProvider; -import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory; +import com.google.android.exoplayer2.ext.cronet.CronetDataSource; import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper; import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; import com.google.android.exoplayer2.offline.DefaultDownloadIndex; @@ -81,7 +81,7 @@ public final class DemoUtil { context = context.getApplicationContext(); CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context); httpDataSourceFactory = - new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); + new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); } return httpDataSourceFactory; } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index c0f443d5df..b99512935e 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -21,6 +21,7 @@ dependencies { compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils') + testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion } 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 97ce860d01..2f65b574cf 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 @@ -25,9 +25,12 @@ import androidx.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.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; @@ -65,6 +68,186 @@ import org.chromium.net.UrlResponseInfo; */ public class CronetDataSource extends BaseDataSource implements HttpDataSource { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.cronet"); + } + + /** {@link DataSource.Factory} for {@link CronetDataSource} instances. */ + public static final class Factory implements HttpDataSource.Factory { + + private final CronetEngineWrapper cronetEngineWrapper; + private final Executor executor; + private final RequestProperties defaultRequestProperties; + private final DefaultHttpDataSource.Factory internalFallbackFactory; + + @Nullable private HttpDataSource.Factory fallbackFactory; + @Nullable private Predicate contentTypePredicate; + @Nullable private TransferListener transferListener; + private int connectTimeoutMs; + private int readTimeoutMs; + private boolean resetTimeoutOnRedirects; + private boolean handleSetCookieRequests; + + /** + * Creates an instance. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @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. + */ + public Factory(CronetEngineWrapper cronetEngineWrapper, Executor executor) { + this.cronetEngineWrapper = cronetEngineWrapper; + this.executor = executor; + defaultRequestProperties = new RequestProperties(); + internalFallbackFactory = new DefaultHttpDataSource.Factory(); + connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS; + readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS; + } + + /** @deprecated Use {@link #setDefaultRequestProperties(Map)} instead. */ + @Deprecated + @Override + public final RequestProperties getDefaultRequestProperties() { + return defaultRequestProperties; + } + + @Override + public final Factory setDefaultRequestProperties(Map defaultRequestProperties) { + this.defaultRequestProperties.clearAndSet(defaultRequestProperties); + internalFallbackFactory.setDefaultRequestProperties(defaultRequestProperties); + return this; + } + + /** + * Sets the connect timeout, in milliseconds. + * + *

The default is {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}. + * + * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used. + * @return This factory. + */ + public Factory setConnectionTimeoutMs(int connectTimeoutMs) { + this.connectTimeoutMs = connectTimeoutMs; + internalFallbackFactory.setConnectTimeoutMs(connectTimeoutMs); + return this; + } + + /** + * Sets whether the connect timeout is reset when a redirect occurs. + * + *

The default is {@code false}. + * + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @return This factory. + */ + public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) { + this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; + return this; + } + + /** + * Sets whether "Set-Cookie" requests on redirect should be forwarded to the redirect url in the + * "Cookie" header. + * + *

The default is {@code false}. + * + * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded + * to the redirect url in the "Cookie" header. + * @return This factory. + */ + public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) { + this.handleSetCookieRequests = handleSetCookieRequests; + return this; + } + + /** + * Sets the read timeout, in milliseconds. + * + *

The default is {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS}. + * + * @param readTimeoutMs The connect timeout, in milliseconds, that will be used. + * @return This factory. + */ + public Factory setReadTimeoutMs(int readTimeoutMs) { + this.readTimeoutMs = readTimeoutMs; + internalFallbackFactory.setReadTimeoutMs(readTimeoutMs); + return this; + } + + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + *

The default is {@code null}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. + * @return This factory. + */ + public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + internalFallbackFactory.setContentTypePredicate(contentTypePredicate); + return this; + } + + /** + * Sets the {@link TransferListener} that will be used. + * + *

The default is {@code null}. + * + *

See {@link DataSource#addTransferListener(TransferListener)}. + * + * @param transferListener The listener that will be used. + * @return This factory. + */ + public Factory setTransferListener(@Nullable TransferListener transferListener) { + this.transferListener = transferListener; + internalFallbackFactory.setTransferListener(transferListener); + return this; + } + + /** + * Sets the fallback {@link HttpDataSource.Factory} that is used as a fallback if the {@link + * CronetEngineWrapper} fails to provide a {@link CronetEngine}. + * + *

By default a {@link DefaultHttpDataSource} is used as fallback factory. + * + * @param fallbackFactory The fallback factory that will be used. + * @return This factory. + */ + public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) { + this.fallbackFactory = fallbackFactory; + return this; + } + + @Override + public HttpDataSource createDataSource() { + @Nullable CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine(); + if (cronetEngine == null) { + return (fallbackFactory != null) + ? fallbackFactory.createDataSource() + : internalFallbackFactory.createDataSource(); + } + CronetDataSource dataSource = + new CronetDataSource( + cronetEngine, + executor, + connectTimeoutMs, + readTimeoutMs, + resetTimeoutOnRedirects, + handleSetCookieRequests, + defaultRequestProperties, + contentTypePredicate); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return dataSource; + } + } + /** * Thrown when an error is encountered when trying to open a {@link CronetDataSource}. */ @@ -85,20 +268,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { super(errorMessage, dataSpec, TYPE_OPEN); this.cronetConnectionStatus = cronetConnectionStatus; } - } - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.cronet"); - } - - /** - * The default connection timeout, in milliseconds. - */ + /** The default connection timeout, in milliseconds. */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; - /** - * The default read timeout, in milliseconds. - */ + /** The default read timeout, in milliseconds. */ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; /* package */ final UrlRequest.Callback urlRequestCallback; @@ -149,16 +323,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; - /** - * Creates an instance. - * - * @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. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ + @SuppressWarnings("deprecation") + @Deprecated public CronetDataSource(CronetEngine cronetEngine, Executor executor) { this( cronetEngine, @@ -169,21 +336,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* defaultRequestProperties= */ null); } - /** - * Creates an instance. - * - * @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 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 Optional default {@link RequestProperties} to be sent to the - * server as HTTP headers on every request. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -197,28 +351,13 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, + /* handleSetCookieRequests= */ false, defaultRequestProperties, - /* handleSetCookieRequests= */ false); + /* contentTypePredicate= */ null); } - /** - * Creates an instance. - * - * @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 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 Optional default {@link RequestProperties} to be sent to the - * server as HTTP headers on every request. - * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to - * the redirect url in the "Cookie" header. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ + @Deprecated public CronetDataSource( CronetEngine cronetEngine, Executor executor, @@ -233,26 +372,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, + handleSetCookieRequests, defaultRequestProperties, - handleSetCookieRequests); + /* contentTypePredicate= */ null); } - /** - * Creates an instance. - * - * @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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link - * #setContentTypePredicate(Predicate)}. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ @SuppressWarnings("deprecation") @Deprecated public CronetDataSource( @@ -269,26 +394,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* defaultRequestProperties= */ null); } - /** - * Creates an instance. - * - * @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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @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 Optional default {@link RequestProperties} to be sent to the - * server as HTTP headers on every request. - * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean, - * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ @SuppressWarnings("deprecation") @Deprecated public CronetDataSource( @@ -310,28 +416,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { /* handleSetCookieRequests= */ false); } - /** - * Creates an instance. - * - * @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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then an {@link InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @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 Optional default {@link RequestProperties} to be sent to the - * server as HTTP headers on every request. - * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to - * the redirect url in the "Cookie" header. - * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean, - * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}. - */ + /** @deprecated Use {@link CronetDataSource.Factory} instead. */ @Deprecated public CronetDataSource( CronetEngine cronetEngine, @@ -348,31 +433,31 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, - Clock.DEFAULT, + handleSetCookieRequests, defaultRequestProperties, - handleSetCookieRequests); - this.contentTypePredicate = contentTypePredicate; + contentTypePredicate); } - /* package */ CronetDataSource( + private CronetDataSource( CronetEngine cronetEngine, Executor executor, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, - Clock clock, + boolean handleSetCookieRequests, @Nullable RequestProperties defaultRequestProperties, - boolean handleSetCookieRequests) { + @Nullable Predicate contentTypePredicate) { super(/* isNetwork= */ true); - this.urlRequestCallback = new UrlRequestCallback(); this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; - this.clock = Assertions.checkNotNull(clock); this.defaultRequestProperties = defaultRequestProperties; this.handleSetCookieRequests = handleSetCookieRequests; + this.contentTypePredicate = contentTypePredicate; + clock = Clock.DEFAULT; + urlRequestCallback = new UrlRequestCallback(); requestProperties = new RequestProperties(); operation = new ConditionVariable(); } @@ -384,6 +469,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a * predicate that was previously set. */ + @Deprecated public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { this.contentTypePredicate = contentTypePredicate; } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 57e75de3dc..443d8049a4 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -21,14 +21,14 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; -import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.TransferListener; import java.util.concurrent.Executor; import org.chromium.net.CronetEngine; -/** - * A {@link Factory} that produces {@link CronetDataSource}. - */ +/** @deprecated Use {@link CronetDataSource.Factory} instead. */ +// Uses deprecated DefaultHttpDataSourceFactory +@SuppressWarnings("deprecation") +@Deprecated public final class CronetDataSourceFactory extends BaseFactory { /** 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 b0b80a8e12..3faf133c85 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet; import static com.google.common.truth.Truth.assertThat; import static java.lang.Math.min; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -32,13 +33,15 @@ import static org.mockito.Mockito.when; import android.net.Uri; import android.os.ConditionVariable; import android.os.SystemClock; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InterruptedIOException; @@ -53,8 +56,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import okhttp3.Headers; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.chromium.net.CronetEngine; import org.chromium.net.NetworkException; import org.chromium.net.UrlRequest; @@ -100,21 +107,18 @@ public final class CronetDataSourceTest { public void setUp() { MockitoAnnotations.initMocks(this); - HttpDataSource.RequestProperties defaultRequestProperties = - new HttpDataSource.RequestProperties(); - defaultRequestProperties.set("defaultHeader1", "defaultValue1"); - defaultRequestProperties.set("defaultHeader2", "defaultValue2"); + Map defaultRequestProperties = new HashMap<>(); + defaultRequestProperties.put("defaultHeader1", "defaultValue1"); + defaultRequestProperties.put("defaultHeader2", "defaultValue2"); dataSourceUnderTest = - new CronetDataSource( - mockCronetEngine, - mockExecutor, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - /* resetTimeoutOnRedirects= */ true, - Clock.DEFAULT, - defaultRequestProperties, - /* handleSetCookieRequests= */ false); + (CronetDataSource) + new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor) + .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS) + .setReadTimeoutMs(TEST_READ_TIMEOUT_MS) + .setResetTimeoutOnRedirects(true) + .setDefaultRequestProperties(defaultRequestProperties) + .createDataSource(); dataSourceUnderTest.addTransferListener(mockTransferListener); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) @@ -1134,15 +1138,13 @@ public final class CronetDataSourceTest { testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() throws HttpDataSourceException { dataSourceUnderTest = - new CronetDataSource( - mockCronetEngine, - mockExecutor, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - Clock.DEFAULT, - null, - true); + (CronetDataSource) + new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor) + .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS) + .setReadTimeoutMs(TEST_READ_TIMEOUT_MS) + .setResetTimeoutOnRedirects(true) + .setHandleSetCookieRequests(true) + .createDataSource(); dataSourceUnderTest.addTransferListener(mockTransferListener); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); @@ -1164,15 +1166,13 @@ public final class CronetDataSourceTest { throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); dataSourceUnderTest = - new CronetDataSource( - mockCronetEngine, - mockExecutor, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - /* resetTimeoutOnRedirects= */ true, - Clock.DEFAULT, - /* defaultRequestProperties= */ null, - /* handleSetCookieRequests= */ true); + (CronetDataSource) + new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor) + .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS) + .setReadTimeoutMs(TEST_READ_TIMEOUT_MS) + .setResetTimeoutOnRedirects(true) + .setHandleSetCookieRequests(true) + .createDataSource(); dataSourceUnderTest.addTransferListener(mockTransferListener); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); @@ -1202,15 +1202,13 @@ public final class CronetDataSourceTest { public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() throws HttpDataSourceException { dataSourceUnderTest = - new CronetDataSource( - mockCronetEngine, - mockExecutor, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - /* resetTimeoutOnRedirects= */ true, - Clock.DEFAULT, - /* defaultRequestProperties= */ null, - /* handleSetCookieRequests= */ true); + (CronetDataSource) + new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor) + .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS) + .setReadTimeoutMs(TEST_READ_TIMEOUT_MS) + .setResetTimeoutOnRedirects(true) + .setHandleSetCookieRequests(true) + .createDataSource(); dataSourceUnderTest.addTransferListener(mockTransferListener); mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -1356,6 +1354,99 @@ public final class CronetDataSourceTest { verify(mockUrlRequestBuilder).allowDirectExecutor(); } + @Test + public void factorySetFallbackHttpDataSourceFactory_cronetNotAvailable_usesFallbackFactory() + throws HttpDataSourceException, InterruptedException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse()); + CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null); + DefaultHttpDataSource.Factory fallbackFactory = + new DefaultHttpDataSource.Factory().setUserAgent("customFallbackFactoryUserAgent"); + HttpDataSource dataSourceUnderTest = + new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor()) + .setFallbackFactory(fallbackFactory) + .createDataSource(); + + dataSourceUnderTest.open( + new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build()); + + Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders(); + assertThat(headers.get("user-agent")).isEqualTo("customFallbackFactoryUserAgent"); + } + + @Test + public void + factory_noFallbackFactoryCronetNotAvailable_delegateTransferListenerToInternalFallbackFactory() + throws HttpDataSourceException, InterruptedException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse()); + CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null); + HttpDataSource dataSourceUnderTest = + new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor()) + .setTransferListener(mockTransferListener) + .createDataSource(); + DataSpec dataSpec = + new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build(); + + dataSourceUnderTest.open(dataSpec); + + Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders(); + assertThat(headers.get("user-agent")).isEqualTo(ExoPlayerLibraryInfo.DEFAULT_USER_AGENT); + verify(mockTransferListener) + .onTransferInitializing(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true)); + verify(mockTransferListener) + .onTransferStart(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true)); + } + + @Test + public void + factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactory() + throws HttpDataSourceException, InterruptedException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse()); + CronetEngineWrapper cronetEngineWrapper = + new CronetEngineWrapper(ApplicationProvider.getApplicationContext()); + Map defaultRequestProperties = new HashMap<>(); + defaultRequestProperties.put("0", "defaultRequestProperty0"); + HttpDataSource dataSourceUnderTest = + new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor()) + .setDefaultRequestProperties(defaultRequestProperties) + .createDataSource(); + + dataSourceUnderTest.open( + new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build()); + + Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders(); + assertThat(headers.get("0")).isEqualTo("defaultRequestProperty0"); + assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class); + } + + @Test + public void + factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactoryAfterCreation() + throws HttpDataSourceException, InterruptedException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.enqueue(new MockResponse()); + CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null); + Map defaultRequestProperties = new HashMap<>(); + defaultRequestProperties.put("0", "defaultRequestProperty0"); + CronetDataSource.Factory factory = + new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); + HttpDataSource dataSourceUnderTest = + factory.setDefaultRequestProperties(defaultRequestProperties).createDataSource(); + defaultRequestProperties.clear(); + defaultRequestProperties.put("1", "defaultRequestPropertyAfterCreation"); + factory.setDefaultRequestProperties(defaultRequestProperties); + + dataSourceUnderTest.open( + new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build()); + + Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders(); + assertThat(headers.get("0")).isNull(); + assertThat(headers.get("1")).isEqualTo("defaultRequestPropertyAfterCreation"); + assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class); + } + // Helper methods. private void mockStatusResponse() {