From 6b31b4620c29330a47233f1c2741c1db430fb35e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Jan 2025 01:14:11 -0800 Subject: [PATCH] Move NetworkTypeObserver operations off main thread PiperOrigin-RevId: 721291681 --- .../common/util/BackgroundExecutor.java | 53 +++ .../common/util/NetworkTypeObserver.java | 131 ++++++-- .../common/util/NetworkTypeObserverTest.java | 316 ++++++++++++++++++ .../upstream/DefaultBandwidthMeterTest.java | 8 +- .../ExperimentalBandwidthMeterTest.java | 8 +- 5 files changed, 476 insertions(+), 40 deletions(-) create mode 100644 libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java create mode 100644 libraries/common/src/test/java/androidx/media3/common/util/NetworkTypeObserverTest.java diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java new file mode 100644 index 0000000000..194805f8d0 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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 androidx.media3.common.util; + +import androidx.annotation.Nullable; +import java.util.concurrent.Executor; + +/** A utility class to obtain an {@link Executor} for background tasks. */ +@UnstableApi +public final class BackgroundExecutor { + + @SuppressWarnings("NonFinalStaticField") + @Nullable + private static Executor staticInstance; + + /** + * Returns an {@link Executor} for background tasks. + * + *

Must only be used for quick, high-priority tasks to ensure other background tasks are not + * blocked. + */ + public static synchronized Executor get() { + if (staticInstance == null) { + staticInstance = Util.newSingleThreadExecutor("ExoPlayer:BackgroundExecutor"); + } + return staticInstance; + } + + /** + * Sets the {@link Executor} to be returned from {@link #get()}. + * + * @param executor An {@link Executor} that runs tasks on background threads and should only be + * used for quick, high-priority tasks to ensure other background tasks are not blocked. + */ + public static synchronized void set(Executor executor) { + staticInstance = executor; + } + + private BackgroundExecutor() {} +} diff --git a/libraries/common/src/main/java/androidx/media3/common/util/NetworkTypeObserver.java b/libraries/common/src/main/java/androidx/media3/common/util/NetworkTypeObserver.java index ec89d03867..9dbddd31e4 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/NetworkTypeObserver.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/NetworkTypeObserver.java @@ -17,6 +17,7 @@ package androidx.media3.common.util; import static androidx.media3.common.util.Assertions.checkNotNull; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -34,8 +35,11 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; +import com.google.errorprone.annotations.InlineMe; import java.lang.ref.WeakReference; +import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; /** * Observer for network type changes. @@ -61,14 +65,16 @@ public final class NetworkTypeObserver { @Nullable private static NetworkTypeObserver staticInstance; - private final Handler mainHandler; - // This class needs to hold weak references as it doesn't require listeners to unregister. - private final CopyOnWriteArrayList> listeners; - private final Object networkTypeLock; + private final Executor backgroundExecutor; + private final CopyOnWriteArrayList listeners; + private final Object lock; - @GuardedBy("networkTypeLock") + @GuardedBy("lock") private @C.NetworkType int networkType; + @GuardedBy("lock") + private boolean isInitialized; + /** * Returns a network type observer instance. * @@ -88,13 +94,22 @@ public final class NetworkTypeObserver { } private NetworkTypeObserver(Context context) { - mainHandler = new Handler(Looper.getMainLooper()); + backgroundExecutor = BackgroundExecutor.get(); listeners = new CopyOnWriteArrayList<>(); - networkTypeLock = new Object(); + lock = new Object(); networkType = C.NETWORK_TYPE_UNKNOWN; - IntentFilter filter = new IntentFilter(); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - context.registerReceiver(new Receiver(), filter); + backgroundExecutor.execute(() -> init(context)); + } + + /** + * @deprecated Use {@link #register(Listener, Executor)} instead. + */ + @InlineMe( + replacement = "this.register(listener, new Handler(Looper.getMainLooper())::post)", + imports = {"android.os.Handler", "android.os.Looper"}) + @Deprecated + public void register(Listener listener) { + register(listener, /* executor= */ new Handler(Looper.getMainLooper())::post); } /** @@ -103,44 +118,68 @@ public final class NetworkTypeObserver { *

The current network type will be reported to the listener after registration. * * @param listener The {@link Listener}. + * @param executor The {@link Executor} to call the {@code listener} on. */ - public void register(Listener listener) { + public void register(Listener listener, Executor executor) { removeClearedReferences(); - listeners.add(new WeakReference<>(listener)); - // Simulate an initial update on the main thread (like the sticky broadcast we'd receive if - // we were to register a separate broadcast receiver for each listener). - mainHandler.post(() -> listener.onNetworkTypeChanged(getNetworkType())); + boolean isInitialized; + ListenerHolder listenerHolder = new ListenerHolder(listener, executor); + synchronized (lock) { + listeners.add(listenerHolder); + isInitialized = this.isInitialized; + } + if (isInitialized) { + // Simulate an initial update (like the sticky broadcast we'd receive if we were to register a + // separate broadcast receiver for each listener). + listenerHolder.callOnNetworkTypeChanged(); + } } /** Returns the current network type. */ public @C.NetworkType int getNetworkType() { - synchronized (networkTypeLock) { + synchronized (lock) { return networkType; } } + @SuppressLint("UnprotectedReceiver") // Protected system broadcasts must not specify protection. + private void init(Context context) { + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(new Receiver(), filter); + } + private void removeClearedReferences() { - for (WeakReference listenerReference : listeners) { - if (listenerReference.get() == null) { - listeners.remove(listenerReference); + for (ListenerHolder listener : listeners) { + if (listener.canBeRemoved()) { + listeners.remove(listener); } } } + private void handleConnectivityActionBroadcast(Context context) { + @C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context); + if (Util.SDK_INT >= 31 && networkType == C.NETWORK_TYPE_4G) { + // Delay update of the network type to check whether this is actually 5G-NSA. + Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this); + } else { + updateNetworkType(networkType); + } + } + private void updateNetworkType(@C.NetworkType int networkType) { - synchronized (networkTypeLock) { - if (this.networkType == networkType) { + removeClearedReferences(); + Iterator currentListeners; + synchronized (lock) { + if (isInitialized && this.networkType == networkType) { return; } + isInitialized = true; this.networkType = networkType; + currentListeners = listeners.iterator(); } - for (WeakReference listenerReference : listeners) { - @Nullable Listener listener = listenerReference.get(); - if (listener != null) { - listener.onNetworkTypeChanged(networkType); - } else { - listeners.remove(listenerReference); - } + while (currentListeners.hasNext()) { + currentListeners.next().callOnNetworkTypeChanged(); } } @@ -214,13 +253,7 @@ public final class NetworkTypeObserver { @Override public void onReceive(Context context, Intent intent) { - @C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context); - if (Util.SDK_INT >= 31 && networkType == C.NETWORK_TYPE_4G) { - // Delay update of the network type to check whether this is actually 5G-NSA. - Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this); - } else { - updateNetworkType(networkType); - } + backgroundExecutor.execute(() -> handleConnectivityActionBroadcast(context)); } } @@ -232,7 +265,7 @@ public final class NetworkTypeObserver { TelephonyManager telephonyManager = checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); DisplayInfoCallback callback = new DisplayInfoCallback(instance); - telephonyManager.registerTelephonyCallback(context.getMainExecutor(), callback); + telephonyManager.registerTelephonyCallback(instance.backgroundExecutor, callback); // We are only interested in the initial response with the current state, so unregister // the listener immediately. telephonyManager.unregisterTelephonyCallback(callback); @@ -262,4 +295,30 @@ public final class NetworkTypeObserver { } } } + + private final class ListenerHolder { + + // This class needs to hold weak references as it doesn't require listeners to unregister. + private final WeakReference listener; + private final Executor executor; + + public ListenerHolder(Listener listener, Executor executor) { + this.listener = new WeakReference<>(listener); + this.executor = executor; + } + + public boolean canBeRemoved() { + return listener.get() == null; + } + + public void callOnNetworkTypeChanged() { + executor.execute( + () -> { + Listener listener = this.listener.get(); + if (listener != null) { + listener.onNetworkTypeChanged(getNetworkType()); + } + }); + } + } } diff --git a/libraries/common/src/test/java/androidx/media3/common/util/NetworkTypeObserverTest.java b/libraries/common/src/test/java/androidx/media3/common/util/NetworkTypeObserverTest.java new file mode 100644 index 0000000000..e2d9a15975 --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/util/NetworkTypeObserverTest.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 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 androidx.media3.common.util; + +import static android.net.NetworkInfo.State.CONNECTED; +import static android.net.NetworkInfo.State.DISCONNECTED; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.telephony.TelephonyDisplayInfo; +import android.telephony.TelephonyManager; +import androidx.media3.common.C; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +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; +import org.robolectric.shadows.ShadowTelephonyManager; + +/** Unit test for {@link NetworkTypeObserver}. */ +@RunWith(AndroidJUnit4.class) +@Config(sdk = Config.ALL_SDKS) // Test all SDKs because network detection logic changed over time. +public class NetworkTypeObserverTest { + + private HandlerThread backgroundThread; + + @Before + public void setUp() { + NetworkTypeObserver.resetForTests(); + backgroundThread = new HandlerThread("NetworkTypeObserverTest"); + backgroundThread.start(); + BackgroundExecutor.set(new Handler(backgroundThread.getLooper())::post); + } + + @After + public void tearDown() { + backgroundThread.quit(); + } + + @Test + public void register_immediatelyAfterObtainingStaticInstance_callsOnNetworkTypeChanged() { + setActiveNetworkInfo(getWifiNetworkInfo()); + NetworkTypeObserver.Listener listener = mock(NetworkTypeObserver.Listener.class); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + // Do not wait for pending operations here. + + networkTypeObserver.register(listener, directExecutor()); + waitForPendingOperations(); + + verify(listener).onNetworkTypeChanged(C.NETWORK_TYPE_WIFI); + verifyNoMoreInteractions(listener); + } + + @Test + public void register_afterStaticInstanceIsInitialized_callsOnNetworkTypeChanged() { + setActiveNetworkInfo(getWifiNetworkInfo()); + NetworkTypeObserver.Listener listener = mock(NetworkTypeObserver.Listener.class); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + + waitForPendingOperations(); + networkTypeObserver.register(listener, directExecutor()); + waitForPendingOperations(); + + verify(listener).onNetworkTypeChanged(C.NETWORK_TYPE_WIFI); + verifyNoMoreInteractions(listener); + } + + @Test + public void register_withCustomExecutor_callsOnNetworkTypeChangedOnExecutor() { + setActiveNetworkInfo(getWifiNetworkInfo()); + AtomicReference actualListenerLooper = new AtomicReference<>(); + NetworkTypeObserver.Listener listener = + networkType -> actualListenerLooper.set(Looper.myLooper()); + HandlerThread listenerThread = new HandlerThread("CustomListenerThread"); + listenerThread.start(); + Looper listenerThreadLooper = listenerThread.getLooper(); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + + networkTypeObserver.register(listener, new Handler(listenerThreadLooper)::post); + waitForPendingOperations(); + shadowOf(listenerThreadLooper).idle(); + listenerThread.quit(); + + assertThat(actualListenerLooper.get()).isEqualTo(listenerThreadLooper); + } + + @Test + public void register_withChangeInNetworkType_callsOnNetworkTypeChangedAgain() { + setActiveNetworkInfo(getWifiNetworkInfo()); + NetworkTypeObserver.Listener listener = mock(NetworkTypeObserver.Listener.class); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + + networkTypeObserver.register(listener, directExecutor()); + waitForPendingOperations(); + setActiveNetworkInfo(get4gNetworkInfo()); + waitForPendingOperations(); + setActiveNetworkInfo(get4gNetworkInfo()); // Check same network type isn't reported twice. + waitForPendingOperations(); + setActiveNetworkInfo(get3gNetworkInfo()); + waitForPendingOperations(); + + verify(listener).onNetworkTypeChanged(C.NETWORK_TYPE_WIFI); + verify(listener).onNetworkTypeChanged(C.NETWORK_TYPE_4G); + verify(listener).onNetworkTypeChanged(C.NETWORK_TYPE_3G); + verifyNoMoreInteractions(listener); + } + + @Test + public void getNetworkType_withWifiNetwork_returnsNetworkTypeWifi() { + setActiveNetworkInfo(getWifiNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_WIFI); + } + + @Test + public void getNetworkType_with2gNetwork_returnsNetworkType2g() { + setActiveNetworkInfo(get2gNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_2G); + } + + @Test + public void getNetworkType_with3gNetwork_returnsNetworkType3g() { + setActiveNetworkInfo(get3gNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_3G); + } + + @Test + public void getNetworkType_with4gNetwork_returnsNetworkType4g() { + setActiveNetworkInfo(get4gNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_4G); + } + + @Test + @Config(minSdk = 31) // 5G-NSA detection is supported from API 31. + public void getNetworkType_with5gNsaNetwork_returnsNetworkType5gNsa() { + setActiveNetworkInfo(get4gNetworkInfo(), TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_5G_NSA); + } + + @Test + @Config(minSdk = 29) // 5G-SA detection is supported from API 29. + public void getNetworkType_with5gSaNetwork_returnsNetworkType5gSa() { + setActiveNetworkInfo(get5gSaNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_5G_SA); + } + + @Test + public void getNetworkType_withEthernetNetwork_returnsNetworkTypeEthernet() { + setActiveNetworkInfo(getEthernetNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_ETHERNET); + } + + @Test + public void getNetworkType_withOfflineNetwork_returnsNetworkTypeOffline() { + setActiveNetworkInfo(getOfflineNetworkInfo()); + NetworkTypeObserver networkTypeObserver = + NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); + waitForPendingOperations(); + + assertThat(networkTypeObserver.getNetworkType()).isEqualTo(C.NETWORK_TYPE_OFFLINE); + } + + private void waitForPendingOperations() { + ShadowLooper.idleMainLooper(); + shadowOf(backgroundThread.getLooper()).idle(); + } + + private static void setActiveNetworkInfo(NetworkInfo networkInfo) { + setActiveNetworkInfo(networkInfo, TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); + } + + // Adding the permission to the test AndroidManifest.xml doesn't work to appease lint. + @SuppressWarnings({"StickyBroadcast", "MissingPermission"}) + private static void setActiveNetworkInfo(NetworkInfo networkInfo, int networkTypeOverride) { + // Set network info in ConnectivityManager and TelephonyDisplayInfo in TelephonyManager. + Context context = ApplicationProvider.getApplicationContext(); + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(networkInfo); + if (Util.SDK_INT >= 31) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + Object displayInfo = + ShadowTelephonyManager.createTelephonyDisplayInfo( + networkInfo.getType(), networkTypeOverride); + Shadows.shadowOf(telephonyManager).setTelephonyDisplayInfo(displayInfo); + } + // Create a sticky broadcast for the connectivity action because Robolectric isn't replying with + // the current network state if a receiver for this intent is registered. + context.sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + private static NetworkInfo getWifiNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_WIFI, + /* subType= */ 0, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo get2gNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_GPRS, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo get3gNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_HSDPA, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo get4gNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_LTE, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo get5gSaNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_NR, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo getEthernetNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.CONNECTED, + ConnectivityManager.TYPE_ETHERNET, + /* subType= */ 0, + /* isAvailable= */ true, + CONNECTED); + } + + private static NetworkInfo getOfflineNetworkInfo() { + return ShadowNetworkInfo.newInstance( + NetworkInfo.DetailedState.DISCONNECTED, + ConnectivityManager.TYPE_WIFI, + /* subType= */ 0, + /* isAvailable= */ false, + DISCONNECTED); + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java index 073a06bd2d..d47d09ee8f 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java @@ -25,9 +25,12 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyManager; import androidx.media3.common.C; +import androidx.media3.common.util.BackgroundExecutor; import androidx.media3.common.util.NetworkTypeObserver; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -747,9 +750,10 @@ public final class DefaultBandwidthMeterTest { // the current network state if a receiver for this intent is registered. ApplicationProvider.getApplicationContext() .sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); - // Trigger initialization of static network type observer. + // Trigger initialization of static network type observer using the main handler to ensure we + // can wait for the initialization to be done. + BackgroundExecutor.set(new Handler(Looper.getMainLooper())::post); NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); - // Wait until all pending messages are handled and the network initialization is done. ShadowLooper.idleMainLooper(); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java index a4ee300f35..ef7cc6055f 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/experimental/ExperimentalBandwidthMeterTest.java @@ -25,9 +25,12 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyManager; import androidx.media3.common.C; +import androidx.media3.common.util.BackgroundExecutor; import androidx.media3.common.util.NetworkTypeObserver; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -757,9 +760,10 @@ public final class ExperimentalBandwidthMeterTest { // the current network state if a receiver for this intent is registered. ApplicationProvider.getApplicationContext() .sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION)); - // Trigger initialization of static network type observer. + // Trigger initialization of static network type observer using the main handler to ensure we + // can wait for the initialization to be done. + BackgroundExecutor.set(new Handler(Looper.getMainLooper())::post); NetworkTypeObserver.getInstance(ApplicationProvider.getApplicationContext()); - // Wait until all pending messages are handled and the network initialization is done. ShadowLooper.idleMainLooper(); }