Skip to content

Commit

Permalink
User-defined fallback if Cronet is not available
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tonihei authored and ojw28 committed May 4, 2017
1 parent ab30d71 commit d33a6b4
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -47,30 +50,129 @@ 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.
* <p>
* 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<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener,
HttpDataSource.Factory fallbackFactory) {
this(cronetEngineFactory, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
}

/**
* Constructs a CronetDataSourceFactory.
* <p>
* 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<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener) {
TransferListener<? super DataSource> transferListener, String userAgent) {
this(cronetEngineFactory, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
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.
* <p>
* 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<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
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.
* <p>
* 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<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
int readTimeoutMs, boolean resetTimeoutOnRedirects) {
int readTimeoutMs, boolean resetTimeoutOnRedirects,
HttpDataSource.Factory fallbackFactory) {
this.cronetEngineFactory = cronetEngineFactory;
this.executor = executor;
this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CronetProvider> 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<CronetProvider> {

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;
}
}

}

0 comments on commit d33a6b4

Please sign in to comment.