diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index 1c2cc92362..4abdf6e663 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -930,8 +930,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link - * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G_SA}, {@link #NETWORK_TYPE_5G_NSA}, {@link + * #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -942,7 +942,8 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, - NETWORK_TYPE_5G, + NETWORK_TYPE_5G_SA, + NETWORK_TYPE_5G_NSA, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -960,8 +961,10 @@ public final class C { public static final int NETWORK_TYPE_3G = 4; /** Network type for a 4G cellular connection. */ public static final int NETWORK_TYPE_4G = 5; - /** Network type for a 5G cellular connection. */ - public static final int NETWORK_TYPE_5G = 9; + /** Network type for a 5G stand-alone (SA) cellular connection. */ + public static final int NETWORK_TYPE_5G_SA = 9; + /** Network type for a 5G non-stand-alone (NSA) cellular connection. */ + public static final int NETWORK_TYPE_5G_NSA = 10; /** * Network type for cellular connections which cannot be mapped to one of {@link * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 17f5f03a34..5668c6fdf2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -207,7 +207,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static Map getInitialBitrateEstimatesForCountry(String countryCode) { List groupIndices = getCountryGroupIndices(countryCode); - Map result = new HashMap<>(/* initialCapacity= */ 6); + Map result = new HashMap<>(/* initialCapacity= */ 8); result.put(C.NETWORK_TYPE_UNKNOWN, DEFAULT_INITIAL_BITRATE_ESTIMATE); result.put( C.NETWORK_TYPE_WIFI, @@ -222,7 +222,12 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G.get(groupIndices.get(COUNTRY_GROUP_INDEX_4G))); result.put( - C.NETWORK_TYPE_5G, + C.NETWORK_TYPE_5G_NSA, + DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA.get( + groupIndices.get(COUNTRY_GROUP_INDEX_5G_NSA))); + result.put( + C.NETWORK_TYPE_5G_SA, + // TODO: Retrieve actual 5G-SA estimates. DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA.get( groupIndices.get(COUNTRY_GROUP_INDEX_5G_NSA))); // Assume default Wifi speed for Ethernet to prevent using the slower fallback. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NetworkTypeObserver.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NetworkTypeObserver.java index c66bd94beb..d10c7e2b14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NetworkTypeObserver.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NetworkTypeObserver.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -23,6 +25,8 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Handler; import android.os.Looper; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; import android.telephony.TelephonyManager; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; @@ -196,7 +200,7 @@ public final class NetworkTypeObserver { case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; case TelephonyManager.NETWORK_TYPE_NR: - return Util.SDK_INT >= 29 ? C.NETWORK_TYPE_5G : C.NETWORK_TYPE_UNKNOWN; + return Util.SDK_INT >= 29 ? C.NETWORK_TYPE_5G_SA : C.NETWORK_TYPE_UNKNOWN; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: @@ -211,7 +215,36 @@ public final class NetworkTypeObserver { @Override public void onReceive(Context context, Intent intent) { @C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context); + if (networkType == C.NETWORK_TYPE_4G && Util.SDK_INT >= 29 && Util.SDK_INT < 31) { + // Delay update of the network type to check whether this is actually 5G-NSA. + try { + // We can't access TelephonyManager.getServiceState() directly as it requires special + // permissions. Attaching a listener is permission-free. + TelephonyManager telephonyManager = + checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); + ServiceStateListener listener = new ServiceStateListener(); + telephonyManager.listen(listener, PhoneStateListener.LISTEN_SERVICE_STATE); + // We are only interested in the initial response with the current state, so unregister + // the listener immediately. + telephonyManager.listen(listener, PhoneStateListener.LISTEN_NONE); + return; + } catch (RuntimeException e) { + // Ignore problems with listener registration and keep reporting as 4G. + } + } updateNetworkType(networkType); } } + + private class ServiceStateListener extends PhoneStateListener { + + @Override + public void onServiceStateChanged(@Nullable ServiceState serviceState) { + String serviceStateString = serviceState == null ? "" : serviceState.toString(); + boolean is5gNsa = + serviceStateString.contains("nrState=CONNECTED") + || serviceStateString.contains("nrState=NOT_RESTRICTED"); + updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java index 8a2dbac7b9..c4b50c9839 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java @@ -37,6 +37,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Shadows; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowNetworkInfo; @@ -45,7 +46,7 @@ import org.robolectric.shadows.ShadowNetworkInfo; public final class DefaultBandwidthMeterTest { private static final int SIMULATED_TRANSFER_COUNT = 100; - private static final String FAST_COUNTRY_ISO = "EE"; + private static final String FAST_COUNTRY_ISO = "TW"; private static final String SLOW_COUNTRY_ISO = "PG"; private TelephonyManager telephonyManager; @@ -55,6 +56,9 @@ public final class DefaultBandwidthMeterTest { private NetworkInfo networkInfo2g; private NetworkInfo networkInfo3g; private NetworkInfo networkInfo4g; + // TODO: Add tests covering 5G-NSA networks. Not testable right now because Robolectric's + // ShadowTelephonyManager doesn't handle requests to return the ServiceState. + private NetworkInfo networkInfo5gSa; private NetworkInfo networkInfoEthernet; @Before @@ -103,6 +107,13 @@ public final class DefaultBandwidthMeterTest { TelephonyManager.NETWORK_TYPE_LTE, /* isAvailable= */ true, CONNECTED); + networkInfo5gSa = + ShadowNetworkInfo.newInstance( + DetailedState.CONNECTED, + ConnectivityManager.TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_NR, + /* isAvailable= */ true, + CONNECTED); networkInfoEthernet = ShadowNetworkInfo.newInstance( DetailedState.CONNECTED, @@ -217,6 +228,22 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimate3g).isGreaterThan(initialEstimate2g); } + @Config(sdk = Config.NEWEST_SDK) // TODO: Remove once targetSDK >= 29 + @Test + public void defaultInitialBitrateEstimate_for5gSa_isGreaterThanEstimateFor4g() { + setActiveNetworkInfo(networkInfo4g); + DefaultBandwidthMeter bandwidthMeter4g = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate(); + + setActiveNetworkInfo(networkInfo5gSa); + DefaultBandwidthMeter bandwidthMeter5gSa = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimate5gSa = bandwidthMeter5gSa.getBitrateEstimate(); + + assertThat(initialEstimate5gSa).isGreaterThan(initialEstimate4g); + } + @Test public void defaultInitialBitrateEstimate_forOffline_isReasonable() { setActiveNetworkInfo(networkInfoOffline); @@ -313,6 +340,24 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow); } + @Config(sdk = Config.NEWEST_SDK) // TODO: Remove once targetSDK >= 29 + @Test + public void + defaultInitialBitrateEstimate_for5gSa_forFastCountry_isGreaterThanEstimateForSlowCountry() { + setActiveNetworkInfo(networkInfo5gSa); + setNetworkCountryIso(FAST_COUNTRY_ISO); + DefaultBandwidthMeter bandwidthMeterFast = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate(); + + setNetworkCountryIso(SLOW_COUNTRY_ISO); + DefaultBandwidthMeter bandwidthMeterSlow = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build(); + long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate(); + + assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow); + } + @Test public void initialBitrateEstimateOverwrite_whileConnectedToNetwork_setsInitialEstimate() { setActiveNetworkInfo(networkInfoWifi); @@ -463,6 +508,33 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimate).isNotEqualTo(123456789); } + @Config(sdk = Config.NEWEST_SDK) // TODO: Remove once targetSDK >= 29 + @Test + public void initialBitrateEstimateOverwrite_for5gSa_whileConnectedTo5gSa_setsInitialEstimate() { + setActiveNetworkInfo(networkInfo5gSa); + DefaultBandwidthMeter bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) + .setInitialBitrateEstimate(C.NETWORK_TYPE_5G_SA, 123456789) + .build(); + long initialEstimate = bandwidthMeter.getBitrateEstimate(); + + assertThat(initialEstimate).isEqualTo(123456789); + } + + @Config(sdk = Config.NEWEST_SDK) // TODO: Remove once targetSDK >= 29 + @Test + public void + initialBitrateEstimateOverwrite_for5gSa_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() { + setActiveNetworkInfo(networkInfoWifi); + DefaultBandwidthMeter bandwidthMeter = + new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()) + .setInitialBitrateEstimate(C.NETWORK_TYPE_5G_SA, 123456789) + .build(); + long initialEstimate = bandwidthMeter.getBitrateEstimate(); + + assertThat(initialEstimate).isNotEqualTo(123456789); + } + @Test public void initialBitrateEstimateOverwrite_forOffline_whileOffline_setsInitialEstimate() { setActiveNetworkInfo(networkInfoOffline);