From d33a6b49f0ee3a36b9236bc76bc765fe0b7fd670 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 May 2017 05:15:56 -0700 Subject: [PATCH] User-defined fallback if Cronet is not available When using the CronetEngine.Builder class, it automatically selects the Cronet version preferring higher version codes and falling back to a Java Http implementation if no native or GMSCore version is available. This version selection has now been moved into the CronetEngineFactory class to always prefer GMSCore over natively bundled versions. We also ignore the Cronet internal Java implementation. Instead, users of CronetDataSourceFactory can provide their own fallback factory. If none is provided, we use DefaultHttpDataSourceFactory. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154821040 --- .../ext/cronet/CronetDataSourceFactory.java | 110 ++++++++++++++- .../ext/cronet/CronetEngineFactory.java | 133 +++++++++++++++++- 2 files changed, 235 insertions(+), 8 deletions(-) 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; + } + } + }