Skip to content

Commit

Permalink
Rollforward using a non-deprecated API for connectivity monitoring
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 406191957
  • Loading branch information
sjudd authored and glide-copybara-robot committed Oct 28, 2021
1 parent b43ea98 commit 833ef21
Show file tree
Hide file tree
Showing 3 changed files with 354 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.manager.ConnectivityMonitor.ConnectivityListener;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.GlideSuppliers;
import com.bumptech.glide.util.GlideSuppliers.GlideSupplier;
import com.bumptech.glide.util.Synthetic;
import com.bumptech.glide.util.Util;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand All @@ -22,41 +29,9 @@
/** Uses {@link android.net.ConnectivityManager} to identify connectivity changes. */
final class SingletonConnectivityReceiver {
private static volatile SingletonConnectivityReceiver instance;
@Synthetic static final String TAG = "ConnectivityMonitor";
// Only accessed on the main thread.
@Synthetic boolean isConnected;

private final BroadcastReceiver connectivityReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(@NonNull Context context, Intent intent) {
List<ConnectivityListener> listenersToNotify = null;
boolean wasConnected = isConnected;
isConnected = isConnected(context);
if (wasConnected != isConnected) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "connectivity changed, isConnected: " + isConnected);
}

synchronized (SingletonConnectivityReceiver.this) {
listenersToNotify = new ArrayList<>(listeners);
}
}
// Make sure that we do not hold our lock while calling our listener. Otherwise we could
// deadlock where our listener acquires its lock, then tries to acquire ours elsewhere and
// then here we acquire our lock and try to acquire theirs.
// The consequence of this is that we may notify a listener after it has been
// unregistered in a few specific (unlikely) scenarios. That appears to be safe and is
// documented in the unregister method.
if (listenersToNotify != null) {
for (ConnectivityListener listener : listenersToNotify) {
listener.onConnectivityChanged(isConnected);
}
}
}
};
private static final String TAG = "ConnectivityMonitor";

private final Context context;
private final FrameworkConnectivityMonitor frameworkConnectivityMonitor;

@GuardedBy("this")
@Synthetic
Expand All @@ -69,7 +44,7 @@ static SingletonConnectivityReceiver get(@NonNull Context context) {
if (instance == null) {
synchronized (SingletonConnectivityReceiver.class) {
if (instance == null) {
instance = new SingletonConnectivityReceiver(context);
instance = new SingletonConnectivityReceiver(context.getApplicationContext());
}
}
}
Expand All @@ -81,8 +56,34 @@ static void reset() {
instance = null;
}

private SingletonConnectivityReceiver(@NonNull Context context) {
this.context = context.getApplicationContext();
private SingletonConnectivityReceiver(final @NonNull Context context) {
GlideSupplier<ConnectivityManager> connectivityManager =
GlideSuppliers.memorize(
new GlideSupplier<ConnectivityManager>() {
@Override
public ConnectivityManager get() {
return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
});
ConnectivityListener connectivityListener =
new ConnectivityListener() {
@Override
public void onConnectivityChanged(boolean isConnected) {
List<ConnectivityListener> toNotify;
synchronized (SingletonConnectivityReceiver.this) {
toNotify = new ArrayList<>(listeners);
}
for (ConnectivityListener listener : toNotify) {
listener.onConnectivityChanged(isConnected);
}
}
};

frameworkConnectivityMonitor =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? new FrameworkConnectivityMonitorPostApi24(connectivityManager, connectivityListener)
: new FrameworkConnectivityMonitorPreApi24(
context, connectivityManager, connectivityListener);
}

synchronized void register(ConnectivityListener listener) {
Expand All @@ -106,18 +107,7 @@ private void maybeRegisterReceiver() {
if (isRegistered || listeners.isEmpty()) {
return;
}
isConnected = isConnected(context);
try {
// See #1405
context.registerReceiver(
connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
isRegistered = true;
} catch (SecurityException e) {
// See #1417, registering the receiver can throw SecurityException.
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to register", e);
}
}
isRegistered = frameworkConnectivityMonitor.register();
}

@GuardedBy("this")
Expand All @@ -126,31 +116,167 @@ private void maybeUnregisterReceiver() {
return;
}

context.unregisterReceiver(connectivityReceiver);
frameworkConnectivityMonitor.unregister();
isRegistered = false;
}

@SuppressWarnings("WeakerAccess")
@Synthetic
// Permissions are checked in the factory instead.
@SuppressLint("MissingPermission")
boolean isConnected(@NonNull Context context) {
ConnectivityManager connectivityManager =
Preconditions.checkNotNull(
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
NetworkInfo networkInfo;
try {
networkInfo = connectivityManager.getActiveNetworkInfo();
} catch (RuntimeException e) {
// #1405 shows that this throws a SecurityException.
// b/70869360 shows that this throws NullPointerException on APIs 22, 23, and 24.
// b/70869360 also shows that this throws RuntimeException on API 24 and 25.
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e);
private interface FrameworkConnectivityMonitor {
boolean register();

void unregister();
}

@RequiresApi(VERSION_CODES.N)
private static final class FrameworkConnectivityMonitorPostApi24
implements FrameworkConnectivityMonitor {

@Synthetic boolean isConnected;
@Synthetic final ConnectivityListener listener;
private final GlideSupplier<ConnectivityManager> connectivityManager;
private final NetworkCallback networkCallback =
new NetworkCallback() {
@Override
public void onAvailable(@NonNull Network network) {
postOnConnectivityChange(true);
}

@Override
public void onLost(@NonNull Network network) {
postOnConnectivityChange(false);
}

private void postOnConnectivityChange(final boolean newState) {
// We could use registerDefaultNetworkCallback with a Handler, but that's only available
// on API 26, instead of API 24. We can mimic the same behavior here manually by
// posting to the UI thread. All calls have to be posted to make sure that we retain the
// original order. Otherwise a call on a background thread, followed by a call on the UI
// thread could result in the first call running second.
Util.postOnUiThread(
new Runnable() {
@Override
public void run() {
onConnectivityChange(newState);
}
});
}

@Synthetic
void onConnectivityChange(boolean newState) {
// See b/201425456.
Util.assertMainThread();

boolean wasConnected = isConnected;
isConnected = newState;
if (wasConnected != newState) {
listener.onConnectivityChanged(newState);
}
}
};

FrameworkConnectivityMonitorPostApi24(
GlideSupplier<ConnectivityManager> connectivityManager, ConnectivityListener listener) {
this.connectivityManager = connectivityManager;
this.listener = listener;
}

// Permissions are checked in the factory instead.
@SuppressLint("MissingPermission")
@Override
public boolean register() {
isConnected = connectivityManager.get().getActiveNetwork() != null;
try {
connectivityManager.get().registerDefaultNetworkCallback(networkCallback);
return true;
// See b/201664814, b/204226444: At least TooManyRequestsException is not public and
// doesn't extend from any subclass :/.
} catch (RuntimeException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to register callback", e);
}
return false;
}
}

@Override
public void unregister() {
connectivityManager.get().unregisterNetworkCallback(networkCallback);
}
}

private static final class FrameworkConnectivityMonitorPreApi24
implements FrameworkConnectivityMonitor {
private final Context context;
@Synthetic final ConnectivityListener listener;
private final GlideSupplier<ConnectivityManager> connectivityManager;
@Synthetic boolean isConnected;

private final BroadcastReceiver connectivityReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(@NonNull Context context, Intent intent) {
boolean wasConnected = isConnected;
isConnected = isConnected();
if (wasConnected != isConnected) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "connectivity changed, isConnected: " + isConnected);
}

listener.onConnectivityChanged(isConnected);
}
}
};

FrameworkConnectivityMonitorPreApi24(
Context context,
GlideSupplier<ConnectivityManager> connectivityManager,
ConnectivityListener listener) {
this.context = context.getApplicationContext();
this.connectivityManager = connectivityManager;
this.listener = listener;
}

@Override
public boolean register() {
// Initialize isConnected so that we notice the first time around when there's a broadcast.
isConnected = isConnected();
try {
// See #1405
context.registerReceiver(
connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
return true;
} catch (SecurityException e) {
// See #1417, registering the receiver can throw SecurityException.
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to register", e);
}
return false;
}
}

@Override
public void unregister() {
context.unregisterReceiver(connectivityReceiver);
}

@SuppressWarnings("WeakerAccess")
@Synthetic
// Permissions are checked in the factory instead.
@SuppressLint("MissingPermission")
boolean isConnected() {
NetworkInfo networkInfo;
try {
networkInfo = connectivityManager.get().getActiveNetworkInfo();
} catch (RuntimeException e) {
// #1405 shows that this throws a SecurityException.
// b/70869360 shows that this throws NullPointerException on APIs 22, 23, and 24.
// b/70869360 also shows that this throws RuntimeException on API 24 and 25.
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e);
}
// Default to true;
return true;
}
// Default to true;
return true;
return networkInfo != null && networkInfo.isConnected();
}
return networkInfo != null && networkInfo.isConnected();
}
}
33 changes: 33 additions & 0 deletions library/src/main/java/com/bumptech/glide/util/GlideSuppliers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.bumptech.glide.util;

/** Similar to {@link com.google.common.base.Suppliers}, but named to reduce import confusion. */
public final class GlideSuppliers {
/**
* Produces a non-null instance of {@code T}.
*
* @param <T> The data type
*/
public interface GlideSupplier<T> {
T get();
}

private GlideSuppliers() {}

public static <T> GlideSupplier<T> memorize(final GlideSupplier<T> supplier) {
return new GlideSupplier<T>() {
private volatile T instance;

@Override
public T get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = Preconditions.checkNotNull(supplier.get());
}
}
}
return instance;
}
};
}
}
Loading

0 comments on commit 833ef21

Please sign in to comment.