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 1af76c11a7..2e4c27a920 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
@@ -16,12 +16,15 @@
package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource;
+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.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
+import org.chromium.net.CronetEngine;
/**
* A {@link Factory} that produces {@link CronetDataSource}.
@@ -47,18 +50,112 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
+ private final HttpDataSource.Factory fallbackFactory;
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the
+ * provided fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineFactory A {@link CronetEngineFactory}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory,
Executor executor, Predicate contentTypePredicate,
- TransferListener super DataSource> transferListener) {
+ TransferListener super DataSource> transferListener,
+ HttpDataSource.Factory fallbackFactory) {
this(cronetEngineFactory, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
}
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param cronetEngineFactory A {@link CronetEngineFactory}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener An optional listener.
+ * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
+ public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, String userAgent) {
+ this(cronetEngineFactory, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a
+ * {@link DefaultHttpDataSourceFactory} will be used instead.
+ *
+ * @param cronetEngineFactory A {@link CronetEngineFactory}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener 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 userAgent A user agent used to create a fallback HttpDataSource if needed.
+ */
public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory,
Executor executor, Predicate contentTypePredicate,
TransferListener super DataSource> transferListener, int connectTimeoutMs,
- int readTimeoutMs, boolean resetTimeoutOnRedirects) {
+ int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
+ this(cronetEngineFactory, executor, contentTypePredicate, transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
+ readTimeoutMs, resetTimeoutOnRedirects));
+ }
+
+ /**
+ * Constructs a CronetDataSourceFactory.
+ *
+ * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the
+ * provided fallback {@link HttpDataSource.Factory} will be used instead.
+ *
+ * @param cronetEngineFactory A {@link CronetEngineFactory}.
+ * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then an {@link InvalidContentTypeException} is thrown from
+ * {@link CronetDataSource#open}.
+ * @param transferListener 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 fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
+ * no suitable CronetEngine can be build.
+ */
+ public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory,
+ Executor executor, Predicate contentTypePredicate,
+ TransferListener super DataSource> transferListener, int connectTimeoutMs,
+ int readTimeoutMs, boolean resetTimeoutOnRedirects,
+ HttpDataSource.Factory fallbackFactory) {
this.cronetEngineFactory = cronetEngineFactory;
this.executor = executor;
this.contentTypePredicate = contentTypePredicate;
@@ -66,11 +163,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
+ this.fallbackFactory = fallbackFactory;
}
@Override
- protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
+ protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
+ CronetEngine cronetEngine = cronetEngineFactory.createCronetEngine();
+ if (cronetEngine == null) {
+ return fallbackFactory.createDataSource();
+ }
return new CronetDataSource(cronetEngineFactory.createCronetEngine(), executor,
contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs,
resetTimeoutOnRedirects, defaultRequestProperties);
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java
index 0bd74256e4..7211ea64f4 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java
@@ -16,30 +16,155 @@
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
+import android.util.Log;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
import org.chromium.net.CronetEngine;
+import org.chromium.net.CronetProvider;
/**
* A factory class which creates or reuses a {@link CronetEngine}.
*/
public final class CronetEngineFactory {
+ private static final String TAG = "CronetEngineFactory";
+
private final Context context;
+ private final boolean preferGMSCoreCronet;
private CronetEngine cronetEngine = null;
/**
- * Creates the factory for a {@link CronetEngine}.
- * @param context The application context.
+ * Creates the factory for a {@link CronetEngine}. Sets factory to prefer natively bundled Cronet
+ * over GMSCore Cronet if both are available.
+ *
+ * @param context A context.
*/
public CronetEngineFactory(Context context) {
- this.context = context;
+ this(context, false);
}
+ /**
+ * Creates the factory for a {@link CronetEngine} and specifies whether Cronet from GMSCore should
+ * be preferred over natively bundled Cronet if both are available.
+ *
+ * @param context A context.
+ */
+ public CronetEngineFactory(Context context, boolean preferGMSCoreCronet) {
+ this.context = context.getApplicationContext();
+ this.preferGMSCoreCronet = preferGMSCoreCronet;
+ }
+
+ /**
+ * Create or reuse a {@link CronetEngine}. If no CronetEngine is available, the method returns
+ * null.
+ *
+ * @return The CronetEngine, or null if no CronetEngine is available.
+ */
/* package */ CronetEngine createCronetEngine() {
if (cronetEngine == null) {
- cronetEngine = new CronetEngine.Builder(context).build();
+ List cronetProviders = CronetProvider.getAllProviders(context);
+ // Remove disabled and fallback Cronet providers from list
+ for (int i = cronetProviders.size() - 1; i >= 0; i--) {
+ if (!cronetProviders.get(i).isEnabled()
+ || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
+ cronetProviders.remove(i);
+ }
+ }
+ // Sort remaining providers by type and version.
+ Collections.sort(cronetProviders, new CronetProviderComparator(preferGMSCoreCronet));
+ for (int i = 0; i < cronetProviders.size(); i++) {
+ String providerName = cronetProviders.get(i).getName();
+ try {
+ cronetEngine = cronetProviders.get(i).createBuilder().build();
+ Log.d(TAG, "CronetEngine built using " + providerName);
+ } catch (UnsatisfiedLinkError e) {
+ Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
+ + "bundled into your app.");
+ }
+ }
+ }
+ if (cronetEngine == null) {
+ Log.w(TAG, "Cronet not available. Using fallback provider.");
}
return cronetEngine;
}
+ private static class CronetProviderComparator implements Comparator {
+
+ private final String gmsCoreCronetName;
+ private final boolean preferGMSCoreCronet;
+
+ public CronetProviderComparator(boolean preferGMSCoreCronet) {
+ // GMSCore CronetProvider classes are only available in some configurations.
+ // Thus, we use reflection to copy static name.
+ String gmsCoreVersionString = null;
+ try {
+ Class> cronetProviderInstallerClass =
+ Class.forName("com.google.android.gms.net.CronetProviderInstaller");
+ Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
+ gmsCoreVersionString = (String) providerNameField.get(null);
+ } catch (ClassNotFoundException e) {
+ // GMSCore CronetProvider not available.
+ } catch (NoSuchFieldException e) {
+ // GMSCore CronetProvider not available.
+ } catch (IllegalAccessException e) {
+ // GMSCore CronetProvider not available.
+ }
+ gmsCoreCronetName = gmsCoreVersionString;
+ this.preferGMSCoreCronet = preferGMSCoreCronet;
+ }
+
+ @Override
+ public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
+ int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
+ int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
+ if (typePreferenceLeft != typePreferenceRight) {
+ return typePreferenceLeft - typePreferenceRight;
+ }
+ return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
+ }
+
+ /**
+ * Convert Cronet provider name into a sortable preference value.
+ * Smaller values are preferred.
+ */
+ private int evaluateCronetProviderType(String providerName) {
+ if (CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName)) {
+ return 1;
+ }
+ if (gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName)) {
+ return preferGMSCoreCronet ? 0 : 2;
+ }
+ // Unknown provider type.
+ return -1;
+ }
+
+ /**
+ * Compares version strings of format "12.123.35.23".
+ */
+ private static int compareVersionStrings(String versionLeft, String versionRight) {
+ if (versionLeft == null || versionRight == null) {
+ return 0;
+ }
+ String[] versionStringsLeft = versionLeft.split("\\.");
+ String[] versionStringsRight = versionRight.split("\\.");
+ int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
+ for (int i = 0; i < minLength; i++) {
+ if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
+ try {
+ int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
+ int versionIntRight = Integer.parseInt(versionStringsRight[i]);
+ return versionIntLeft - versionIntRight;
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ }
+ return 0;
+ }
+ }
+
}