diff --git a/network/android/src/main/java/com/capacitorjs/plugins/network/Network.java b/network/android/src/main/java/com/capacitorjs/plugins/network/Network.java index 8f8842feec..ea2fb1e9a1 100644 --- a/network/android/src/main/java/com/capacitorjs/plugins/network/Network.java +++ b/network/android/src/main/java/com/capacitorjs/plugins/network/Network.java @@ -5,6 +5,7 @@ import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.NetworkCapabilities; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,28 +15,57 @@ public class Network { * Interface for callbacks when network status changes. */ interface NetworkStatusChangeListener { - void onNetworkStatusChanged(boolean wasLostEvent); + void onNetworkStatusChanged(NetworkStatus status); } + // Status is derived from the callback arguments. Querying ConnectivityManager from inside a + // NetworkCallback can return state that is already out of sync with the callback. class ConnectivityCallback extends NetworkCallback { + @Nullable + private android.net.Network currentNetwork; + + @Override + public void onAvailable(@NonNull android.net.Network network) { + super.onAvailable(network); + currentNetwork = network; + // From API 26 onCapabilitiesChanged always follows onAvailable for the default network. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + notifyStatus(statusFromCapabilities(connectivityManager.getNetworkCapabilities(network))); + } + } + @Override public void onLost(@NonNull android.net.Network network) { super.onLost(network); - statusChangeListener.onNetworkStatusChanged(true); + // A late onLost for the previous default network must not override the new one. + if (currentNetwork != null && !currentNetwork.equals(network)) { + return; + } + currentNetwork = null; + notifyStatus(new NetworkStatus()); } @Override public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull NetworkCapabilities networkCapabilities) { super.onCapabilitiesChanged(network, networkCapabilities); - statusChangeListener.onNetworkStatusChanged(false); + currentNetwork = network; + notifyStatus(statusFromCapabilities(networkCapabilities)); + } + + private void notifyStatus(NetworkStatus status) { + NetworkStatusChangeListener listener = statusChangeListener; + if (listener != null) { + listener.onNetworkStatusChanged(status); + } } } @Nullable - private NetworkStatusChangeListener statusChangeListener; + private volatile NetworkStatusChangeListener statusChangeListener; private ConnectivityCallback connectivityCallback; + private boolean monitoring = false; private Context context; private ConnectivityManager connectivityManager; private BroadcastReceiver receiver; @@ -77,16 +107,24 @@ public NetworkStatus getNetworkStatus() { android.net.Network activeNetwork = this.connectivityManager.getActiveNetwork(); NetworkCapabilities capabilities = this.connectivityManager.getNetworkCapabilities(this.connectivityManager.getActiveNetwork()); if (activeNetwork != null && capabilities != null) { - networkStatus.connected = - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - networkStatus.connectionType = NetworkStatus.ConnectionType.WIFI; - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - networkStatus.connectionType = NetworkStatus.ConnectionType.CELLULAR; - } else { - networkStatus.connectionType = NetworkStatus.ConnectionType.UNKNOWN; - } + networkStatus = statusFromCapabilities(capabilities); + } + } + return networkStatus; + } + + static NetworkStatus statusFromCapabilities(@Nullable NetworkCapabilities capabilities) { + NetworkStatus networkStatus = new NetworkStatus(); + if (capabilities != null) { + networkStatus.connected = + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + networkStatus.connectionType = NetworkStatus.ConnectionType.WIFI; + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + networkStatus.connectionType = NetworkStatus.ConnectionType.CELLULAR; + } else { + networkStatus.connectionType = NetworkStatus.ConnectionType.UNKNOWN; } } return networkStatus; @@ -109,16 +147,24 @@ private NetworkStatus getAndParseNetworkInfo() { } /** - * Register a network callback. + * Register a network callback. Does nothing if it is already registered. */ - public void startMonitoring() { + public synchronized void startMonitoring() { + if (monitoring) { + return; + } connectivityManager.registerDefaultNetworkCallback(connectivityCallback); + monitoring = true; } /** - * Unregister the network callback. + * Unregister the network callback. Does nothing if it is not registered. */ - public void stopMonitoring() { + public synchronized void stopMonitoring() { + if (!monitoring) { + return; + } + monitoring = false; connectivityManager.unregisterNetworkCallback(connectivityCallback); } } diff --git a/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkPlugin.java b/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkPlugin.java index d1fe63ef8d..52a8306fa3 100644 --- a/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkPlugin.java +++ b/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkPlugin.java @@ -1,7 +1,7 @@ package com.capacitorjs.plugins.network; -import android.os.Build; import android.util.Log; +import androidx.annotation.Nullable; import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; @@ -13,7 +13,9 @@ public class NetworkPlugin extends Plugin { private Network implementation; public static final String NETWORK_CHANGE_EVENT = "networkStatusChange"; - private NetworkStatus prePauseNetworkStatus = null; + + @Nullable + private NetworkStatus lastNotifiedStatus = null; /** * Monitor for network status changes and fire our event. @@ -21,17 +23,8 @@ public class NetworkPlugin extends Plugin { @Override public void load() { implementation = new Network(getContext()); - Network.NetworkStatusChangeListener listener = (wasLostEvent) -> { - if (wasLostEvent) { - JSObject jsObject = new JSObject(); - jsObject.put("connected", false); - jsObject.put("connectionType", "none"); - notifyListeners(NETWORK_CHANGE_EVENT, jsObject); - } else { - updateNetworkStatus(); - } - }; - implementation.setStatusChangeListener(listener); + implementation.setStatusChangeListener(this::notifyIfChanged); + implementation.startMonitoring(); } /** @@ -40,6 +33,7 @@ public void load() { @Override protected void handleOnDestroy() { implementation.setStatusChangeListener(null); + implementation.stopMonitoring(); } /** @@ -52,37 +46,38 @@ public void getStatus(PluginCall call) { } /** - * Register the IntentReceiver on resume + * Register the network callback on resume */ @Override protected void handleOnResume() { implementation.startMonitoring(); + // Registering delivers the current default network, but nothing is delivered when there is + // none, so a connection lost while paused has to be reported from here. NetworkStatus afterPauseNetworkStatus = implementation.getNetworkStatus(); - if ( - prePauseNetworkStatus != null && - !afterPauseNetworkStatus.connected && - (prePauseNetworkStatus.connected || afterPauseNetworkStatus.connectionType != prePauseNetworkStatus.connectionType) - ) { - Log.d( - "Capacitor/NetworkPlugin", - "Detected pre-pause and after-pause network status mismatch. Updating network status and notifying listeners." - ); - this.updateNetworkStatus(); + if (!afterPauseNetworkStatus.connected && hasNotified()) { + Log.d("Capacitor/NetworkPlugin", "Network status is not connected after resume. Notifying listeners if it changed."); + notifyIfChanged(afterPauseNetworkStatus); } - this.prePauseNetworkStatus = null; } /** - * Unregister the IntentReceiver on pause to avoid leaking it + * Unregister the network callback on pause to avoid leaking it */ @Override protected void handleOnPause() { - this.prePauseNetworkStatus = implementation.getNetworkStatus(); implementation.stopMonitoring(); } - private void updateNetworkStatus() { - notifyListeners(NETWORK_CHANGE_EVENT, parseNetworkStatus(implementation.getNetworkStatus())); + private synchronized boolean hasNotified() { + return lastNotifiedStatus != null; + } + + private synchronized void notifyIfChanged(NetworkStatus status) { + if (status.equals(lastNotifiedStatus)) { + return; + } + lastNotifiedStatus = status; + notifyListeners(NETWORK_CHANGE_EVENT, parseNetworkStatus(status)); } private JSObject parseNetworkStatus(NetworkStatus networkStatus) { diff --git a/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkStatus.java b/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkStatus.java index ac0a7a1211..583b06673e 100644 --- a/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkStatus.java +++ b/network/android/src/main/java/com/capacitorjs/plugins/network/NetworkStatus.java @@ -1,5 +1,7 @@ package com.capacitorjs.plugins.network; +import java.util.Objects; + public class NetworkStatus { public enum ConnectionType { @@ -21,4 +23,21 @@ public String getConnectionType() { public boolean connected = false; public ConnectionType connectionType = ConnectionType.NONE; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NetworkStatus)) { + return false; + } + NetworkStatus other = (NetworkStatus) o; + return connected == other.connected && connectionType == other.connectionType; + } + + @Override + public int hashCode() { + return Objects.hash(connected, connectionType); + } } diff --git a/network/ios/Sources/NetworkPlugin/Network.swift b/network/ios/Sources/NetworkPlugin/Network.swift index 255b214c81..3c2f7d6d5d 100644 --- a/network/ios/Sources/NetworkPlugin/Network.swift +++ b/network/ios/Sources/NetworkPlugin/Network.swift @@ -29,7 +29,7 @@ public class Network { } func currentStatus() -> Network.Connection { - return reachability?.connection.equivalentEnum ?? Connection.unavailable + return reachability?.currentConnection.equivalentEnum ?? Connection.unavailable } } diff --git a/network/ios/Sources/NetworkPlugin/Reachability.swift b/network/ios/Sources/NetworkPlugin/Reachability.swift index d384a61ef8..5929c880b3 100644 --- a/network/ios/Sources/NetworkPlugin/Reachability.swift +++ b/network/ios/Sources/NetworkPlugin/Reachability.swift @@ -92,11 +92,16 @@ public class Reachability { return connection } - public var connection: Connection { - if flags == nil { - try? setReachabilityFlags() + public var currentConnection: Connection { + var freshFlags = SCNetworkReachabilityFlags() + if SCNetworkReachabilityGetFlags(reachabilityRef, &freshFlags) { + reachabilitySerialQueue.sync { flags = freshFlags } } + return connection + } + public var connection: Connection { + if flags == nil { try? setReachabilityFlags() } switch flags?.connection { case .unavailable?, nil: return .unavailable case .cellular?: return allowsCellularConnection ? .cellular : .unavailable