Move NetworkTypeObserver operations off main thread

PiperOrigin-RevId: 721291681
This commit is contained in:
tonihei 2025-01-30 01:14:11 -08:00 committed by Copybara-Service
parent 9af43c7381
commit 6b31b4620c
5 changed files with 476 additions and 40 deletions

View File

@ -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.
*
* <p>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() {}
}

View File

@ -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<WeakReference<Listener>> listeners;
private final Object networkTypeLock;
private final Executor backgroundExecutor;
private final CopyOnWriteArrayList<ListenerHolder> 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 {
* <p>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<Listener> 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<ListenerHolder> currentListeners;
synchronized (lock) {
if (isInitialized && this.networkType == networkType) {
return;
}
isInitialized = true;
this.networkType = networkType;
currentListeners = listeners.iterator();
}
for (WeakReference<Listener> 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> 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());
}
});
}
}
}

View File

@ -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<Looper> 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);
}
}

View File

@ -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();
}

View File

@ -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();
}