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 2e4c27a920..d6237fc988 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 @@ -43,7 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory { public static final int DEFAULT_READ_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; - private final CronetEngineFactory cronetEngineFactory; + private final CronetEngineWrapper cronetEngineWrapper; private final Executor executor; private final Predicate contentTypePredicate; private final TransferListener transferListener; @@ -55,14 +55,14 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

- * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the - * provided fallback {@link HttpDataSource.Factory} will be used instead. + * If the {@link CronetEngineWrapper} fails to provide a {@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 cronetEngineWrapper A {@link CronetEngineWrapper}. * @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 @@ -71,25 +71,25 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case * no suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, HttpDataSource.Factory fallbackFactory) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, 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 + * If the {@link CronetEngineWrapper} fails to provide a {@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 cronetEngineWrapper A {@link CronetEngineWrapper}. * @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 @@ -97,10 +97,10 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param transferListener An optional listener. * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, String userAgent) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + this(cronetEngineWrapper, 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)); @@ -109,10 +109,10 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

- * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, a + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a * {@link DefaultHttpDataSourceFactory} will be used instead. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @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 @@ -123,11 +123,11 @@ public final class CronetDataSourceFactory extends BaseFactory { * @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, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { - this(cronetEngineFactory, executor, contentTypePredicate, transferListener, + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects)); @@ -136,10 +136,10 @@ public final class CronetDataSourceFactory extends BaseFactory { /** * Constructs a CronetDataSourceFactory. *

- * If the {@link CronetEngineFactory} fails to provide a suitable {@link CronetEngine}, the - * provided fallback {@link HttpDataSource.Factory} will be used instead. + * If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided + * fallback {@link HttpDataSource.Factory} will be used instead. * - * @param cronetEngineFactory A {@link CronetEngineFactory}. + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @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 @@ -151,12 +151,12 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case * no suitable CronetEngine can be build. */ - public CronetDataSourceFactory(CronetEngineFactory cronetEngineFactory, + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor, Predicate contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, HttpDataSource.Factory fallbackFactory) { - this.cronetEngineFactory = cronetEngineFactory; + this.cronetEngineWrapper = cronetEngineWrapper; this.executor = executor; this.contentTypePredicate = contentTypePredicate; this.transferListener = transferListener; @@ -169,13 +169,12 @@ public final class CronetDataSourceFactory extends BaseFactory { @Override protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { - CronetEngine cronetEngine = cronetEngineFactory.createCronetEngine(); + CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine(); if (cronetEngine == null) { return fallbackFactory.createDataSource(); } - return new CronetDataSource(cronetEngineFactory.createCronetEngine(), executor, - contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs, - resetTimeoutOnRedirects, defaultRequestProperties); + return new CronetDataSource(cronetEngine, 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 deleted file mode 100644 index 7211ea64f4..0000000000 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineFactory.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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}. Sets factory to prefer natively bundled Cronet - * over GMSCore Cronet if both are available. - * - * @param context A context. - */ - public CronetEngineFactory(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) { - 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; - } - } - -} diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java new file mode 100644 index 0000000000..efe30d6525 --- /dev/null +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.cronet; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +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 wrapper class for a {@link CronetEngine}. + */ +public final class CronetEngineWrapper { + + private static final String TAG = "CronetEngineWrapper"; + + private final CronetEngine cronetEngine; + private final @CronetEngineSource int cronetEngineSource; + + /** + * Source of {@link CronetEngine}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE}) + public @interface CronetEngineSource {} + /** + * Natively bundled Cronet implementation. + */ + public static final int SOURCE_NATIVE = 0; + /** + * Cronet implementation from GMSCore. + */ + public static final int SOURCE_GMS = 1; + /** + * Other (unknown) Cronet implementation. + */ + public static final int SOURCE_UNKNOWN = 2; + /** + * User-provided Cronet engine. + */ + public static final int SOURCE_USER_PROVIDED = 3; + /** + * No Cronet implementation available. Fallback Http provider is used if possible. + */ + public static final int SOURCE_UNAVAILABLE = 4; + + /** + * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable + * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet + * if both are available. + * + * @param context A context. + */ + public CronetEngineWrapper(Context context) { + this(context, false); + } + + /** + * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable + * {@link CronetProvider} based on user preference. + * + * @param context A context. + * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively + * bundled Cronet if both are available. + */ + public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { + CronetEngine cronetEngine = null; + @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; + 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. + CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet); + Collections.sort(cronetProviders, providerComparator); + for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) { + String providerName = cronetProviders.get(i).getName(); + try { + cronetEngine = cronetProviders.get(i).createBuilder().build(); + if (providerComparator.isNativeProvider(providerName)) { + cronetEngineSource = SOURCE_NATIVE; + } else if (providerComparator.isGMSCoreProvider(providerName)) { + cronetEngineSource = SOURCE_GMS; + } else { + cronetEngineSource = SOURCE_UNKNOWN; + } + Log.d(TAG, "CronetEngine built using " + providerName); + } catch (SecurityException e) { + Log.w(TAG, "Failed to build CronetEngine. Please check if current process has " + + "android.permission.ACCESS_NETWORK_STATE."); + } 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."); + } + this.cronetEngine = cronetEngine; + this.cronetEngineSource = cronetEngineSource; + } + + /** + * Creates a wrapper for an existing CronetEngine. + * + * @param cronetEngine An existing CronetEngine. + */ + public CronetEngineWrapper(CronetEngine cronetEngine) { + this.cronetEngine = cronetEngine; + this.cronetEngineSource = SOURCE_USER_PROVIDED; + } + + /** + * Returns the source of the wrapped {@link CronetEngine}. + * + * @return A {@link CronetEngineSource} value. + */ + public @CronetEngineSource int getCronetEngineSource() { + return cronetEngineSource; + } + + /** + * Returns the wrapped {@link CronetEngine}. + * + * @return The CronetEngine, or null if no CronetEngine is available. + */ + /* package */ CronetEngine getCronetEngine() { + 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()); + } + + public boolean isNativeProvider(String providerName) { + return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName); + } + + public boolean isGMSCoreProvider(String providerName) { + return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName); + } + + /** + * Convert Cronet provider name into a sortable preference value. + * Smaller values are preferred. + */ + private int evaluateCronetProviderType(String providerName) { + if (isNativeProvider(providerName)) { + return 1; + } + if (isGMSCoreProvider(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; + } + } + +}