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 transferListener) { + TransferListener 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 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 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 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; + } + } + }