diff --git a/network/README.md b/network/README.md
index 48496869f..254a0cc49 100644
--- a/network/README.md
+++ b/network/README.md
@@ -99,6 +99,8 @@ Represents the state and type of the network connection.
| -------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----- |
| **`connected`** | boolean | Whether there is an active connection or not. | 1.0.0 |
| **`connectionType`** | ConnectionType | The type of network connection currently in use. If there is no active network connection, `connectionType` will be `'none'`. | 1.0.0 |
+| **`constrained`** | boolean | Whether the active connection is constrained by platform data-saving or bandwidth-reduction signals. | 8.1.0 |
+| **`expensive`** | boolean | Whether the active connection is considered expensive or metered by the platform. | 8.1.0 |
#### PluginListenerHandle
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 8f8842fee..0a0592783 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
@@ -2,9 +2,12 @@
import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
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;
@@ -38,7 +41,7 @@ public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull
private ConnectivityCallback connectivityCallback;
private Context context;
private ConnectivityManager connectivityManager;
- private BroadcastReceiver receiver;
+ private BroadcastReceiver restrictBackgroundReceiver;
/**
* Create network monitoring object.
@@ -48,6 +51,14 @@ public Network(@NonNull Context context) {
this.context = context;
this.connectivityManager = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE);
this.connectivityCallback = new ConnectivityCallback();
+ this.restrictBackgroundReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED.equals(intent.getAction()) && statusChangeListener != null) {
+ statusChangeListener.onNetworkStatusChanged(false);
+ }
+ }
+ };
}
/**
@@ -75,11 +86,17 @@ public NetworkStatus getNetworkStatus() {
NetworkStatus networkStatus = new NetworkStatus();
if (this.connectivityManager != null) {
android.net.Network activeNetwork = this.connectivityManager.getActiveNetwork();
- NetworkCapabilities capabilities = this.connectivityManager.getNetworkCapabilities(this.connectivityManager.getActiveNetwork());
+ NetworkCapabilities capabilities = this.connectivityManager.getNetworkCapabilities(activeNetwork);
if (activeNetwork != null && capabilities != null) {
networkStatus.connected =
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ networkStatus.expensive = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ networkStatus.constrained = isConstrained(
+ connectivityManager.getRestrictBackgroundStatus(),
+ networkStatus.expensive,
+ isBandwidthConstrained(capabilities)
+ );
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
networkStatus.connectionType = NetworkStatus.ConnectionType.WIFI;
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
@@ -92,6 +109,17 @@ public NetworkStatus getNetworkStatus() {
return networkStatus;
}
+ static boolean isConstrained(int restrictBackgroundStatus, boolean expensive, boolean bandwidthConstrained) {
+ return (expensive && restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) || bandwidthConstrained;
+ }
+
+ private static boolean isBandwidthConstrained(NetworkCapabilities capabilities) {
+ return (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM &&
+ !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ );
+ }
+
@SuppressWarnings("deprecation")
private NetworkStatus getAndParseNetworkInfo() {
NetworkStatus networkStatus = new NetworkStatus();
@@ -113,6 +141,12 @@ private NetworkStatus getAndParseNetworkInfo() {
*/
public void startMonitoring() {
connectivityManager.registerDefaultNetworkCallback(connectivityCallback);
+ IntentFilter filter = new IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ context.registerReceiver(restrictBackgroundReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
+ } else {
+ context.registerReceiver(restrictBackgroundReceiver, filter);
+ }
}
/**
@@ -120,5 +154,6 @@ public void startMonitoring() {
*/
public void stopMonitoring() {
connectivityManager.unregisterNetworkCallback(connectivityCallback);
+ context.unregisterReceiver(restrictBackgroundReceiver);
}
}
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 d1fe63ef8..0f1b1a1ad 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,6 +1,5 @@
package com.capacitorjs.plugins.network;
-import android.os.Build;
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
@@ -26,6 +25,8 @@ public void load() {
JSObject jsObject = new JSObject();
jsObject.put("connected", false);
jsObject.put("connectionType", "none");
+ jsObject.put("constrained", false);
+ jsObject.put("expensive", false);
notifyListeners(NETWORK_CHANGE_EVENT, jsObject);
} else {
updateNetworkStatus();
@@ -89,6 +90,8 @@ private JSObject parseNetworkStatus(NetworkStatus networkStatus) {
JSObject jsObject = new JSObject();
jsObject.put("connected", networkStatus.connected);
jsObject.put("connectionType", networkStatus.connectionType.getConnectionType());
+ jsObject.put("constrained", networkStatus.constrained);
+ jsObject.put("expensive", networkStatus.expensive);
return jsObject;
}
}
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 ac0a7a121..c150bc648 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
@@ -21,4 +21,6 @@ public String getConnectionType() {
public boolean connected = false;
public ConnectionType connectionType = ConnectionType.NONE;
+ public boolean constrained = false;
+ public boolean expensive = false;
}
diff --git a/network/android/src/test/java/com/capacitorjs/plugins/network/NetworkTest.java b/network/android/src/test/java/com/capacitorjs/plugins/network/NetworkTest.java
new file mode 100644
index 000000000..be0a89d38
--- /dev/null
+++ b/network/android/src/test/java/com/capacitorjs/plugins/network/NetworkTest.java
@@ -0,0 +1,31 @@
+package com.capacitorjs.plugins.network;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.ConnectivityManager;
+import org.junit.Test;
+
+public class NetworkTest {
+
+ @Test
+ public void isConstrainedReturnsTrueWhenMeteredBackgroundDataIsRestricted() {
+ assertTrue(Network.isConstrained(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED, true, false));
+ }
+
+ @Test
+ public void isConstrainedReturnsFalseWhenUnmeteredBackgroundDataIsRestricted() {
+ assertFalse(Network.isConstrained(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED, false, false));
+ }
+
+ @Test
+ public void isConstrainedReturnsTrueWhenBandwidthIsConstrained() {
+ assertTrue(Network.isConstrained(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED, false, true));
+ }
+
+ @Test
+ public void isConstrainedReturnsFalseWhenBackgroundDataIsUnrestricted() {
+ assertFalse(Network.isConstrained(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED, true, false));
+ assertFalse(Network.isConstrained(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED, true, false));
+ }
+}
diff --git a/network/ios/Sources/NetworkPlugin/Network.swift b/network/ios/Sources/NetworkPlugin/Network.swift
index 255b214c8..0895b5853 100644
--- a/network/ios/Sources/NetworkPlugin/Network.swift
+++ b/network/ios/Sources/NetworkPlugin/Network.swift
@@ -1,4 +1,5 @@
import Foundation
+import Network
public typealias NetworkConnectionChangedObserver = (Network.Connection) -> Void
@@ -9,9 +10,22 @@ public class Network {
public enum Connection {
case unavailable, wifi, cellular
}
+ struct ConnectionDetails {
+ var constrained = false
+ var expensive = false
+ }
internal private(set) var reachability: Reachability?
+ private let pathMonitor = NWPathMonitor()
+ private let pathMonitorQueue = DispatchQueue(label: "capacitor.network.pathMonitor")
+ private let connectionDetailsQueue = DispatchQueue(label: "capacitor.network.connectionDetails")
+ private let observerQueue = DispatchQueue(label: "capacitor.network.observer")
+ private var details = ConnectionDetails()
+ private var observerNotificationPending = false
var statusObserver: NetworkConnectionChangedObserver?
+ var connectionDetails: ConnectionDetails {
+ return connectionDetailsQueue.sync { details }
+ }
init() throws {
reachability = try Reachability()
@@ -19,18 +33,64 @@ public class Network {
throw NetworkError.initializationFailed
}
// setup our callback(s) and start notifications
- reachability?.whenReachable = { [weak self] reachable in
- self?.statusObserver?(reachable.connection.equivalentEnum)
+ reachability?.whenReachable = { [weak self] _ in
+ self?.notifyStatusObserver()
}
reachability?.whenUnreachable = { [weak self] _ in
- self?.statusObserver?(Connection.unavailable)
+ self?.notifyStatusObserver()
}
+ pathMonitor.pathUpdateHandler = { [weak self] path in
+ if self?.updateConnectionDetails(path) == true {
+ self?.notifyStatusObserver()
+ }
+ }
+ pathMonitor.start(queue: pathMonitorQueue)
+ _ = updateConnectionDetails(pathMonitor.currentPath)
try reachability?.startNotifier()
}
+ deinit {
+ pathMonitor.cancel()
+ }
+
func currentStatus() -> Network.Connection {
return reachability?.connection.equivalentEnum ?? Connection.unavailable
}
+
+ private func updateConnectionDetails(_ path: NWPath) -> Bool {
+ let nextDetails = ConnectionDetails(constrained: path.isConstrained, expensive: path.isExpensive)
+ return connectionDetailsQueue.sync {
+ guard details.constrained != nextDetails.constrained || details.expensive != nextDetails.expensive else {
+ return false
+ }
+ details = nextDetails
+ return true
+ }
+ }
+
+ private func notifyStatusObserver() {
+ var shouldNotify = false
+ observerQueue.sync {
+ if !observerNotificationPending {
+ observerNotificationPending = true
+ shouldNotify = true
+ }
+ }
+
+ guard shouldNotify else {
+ return
+ }
+
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else {
+ return
+ }
+ self.observerQueue.sync {
+ self.observerNotificationPending = false
+ }
+ self.statusObserver?(self.currentStatus())
+ }
+ }
}
fileprivate extension Reachability.Connection {
diff --git a/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift b/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift
index 061dce4e4..be532cec7 100644
--- a/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift
+++ b/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift
@@ -16,10 +16,7 @@ public class NetworkPlugin: CAPPlugin, CAPBridgedPlugin {
implementation = try Network()
implementation?.statusObserver = { [weak self] status in
CAPLog.print(status.logMessage)
- self?.notifyListeners("networkStatusChange", data: [
- "connected": status.isConnected,
- "connectionType": status.jsStringValue
- ])
+ self?.notifyListeners("networkStatusChange", data: self?.statusData(status) ?? [:])
}
} catch let error {
CAPLog.print("Unable to start network monitor: \(error)")
@@ -28,7 +25,17 @@ public class NetworkPlugin: CAPPlugin, CAPBridgedPlugin {
@objc func getStatus(_ call: CAPPluginCall) {
let status = implementation?.currentStatus() ?? Network.Connection.unavailable
- call.resolve(["connected": status.isConnected, "connectionType": status.jsStringValue])
+ call.resolve(statusData(status))
+ }
+
+ private func statusData(_ status: Network.Connection) -> [String: Any] {
+ let details = status.isConnected ? (implementation?.connectionDetails ?? Network.ConnectionDetails()) : Network.ConnectionDetails()
+ return [
+ "connected": status.isConnected,
+ "connectionType": status.jsStringValue,
+ "constrained": details.constrained,
+ "expensive": details.expensive
+ ]
}
}
diff --git a/network/ios/Tests/NetworkPluginTests/PluginTests.swift b/network/ios/Tests/NetworkPluginTests/PluginTests.swift
index 06f507c1a..0ee493119 100644
--- a/network/ios/Tests/NetworkPluginTests/PluginTests.swift
+++ b/network/ios/Tests/NetworkPluginTests/PluginTests.swift
@@ -34,4 +34,14 @@ class NetworkTests: XCTestCase {
XCTFail("Network initialization failed! \(error)")
}
}
+
+ func testConnectionConditionDefaults() {
+ do {
+ let implementation = try Network()
+ XCTAssertFalse(implementation.connectionDetails.constrained)
+ XCTAssertFalse(implementation.connectionDetails.expensive)
+ } catch let error {
+ XCTFail("Network initialization failed! \(error)")
+ }
+ }
}
diff --git a/network/src/definitions.ts b/network/src/definitions.ts
index ea2a3a1a5..56c921009 100644
--- a/network/src/definitions.ts
+++ b/network/src/definitions.ts
@@ -47,6 +47,20 @@ export interface ConnectionStatus {
* @since 1.0.0
*/
connectionType: ConnectionType;
+
+ /**
+ * Whether the active connection is constrained by platform data-saving or bandwidth-reduction signals.
+ *
+ * @since 8.1.0
+ */
+ constrained?: boolean;
+
+ /**
+ * Whether the active connection is considered expensive or metered by the platform.
+ *
+ * @since 8.1.0
+ */
+ expensive?: boolean;
}
/**
diff --git a/network/src/web.ts b/network/src/web.ts
index 7057184f7..781da0f2b 100644
--- a/network/src/web.ts
+++ b/network/src/web.ts
@@ -49,6 +49,14 @@ function translatedConnection(): ConnectionType {
return result;
}
+function connectionDetails(): Pick {
+ const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
+ return {
+ constrained: !!connection?.saveData,
+ expensive: false,
+ };
+}
+
export class NetworkWeb extends WebPlugin implements NetworkPlugin {
constructor() {
super();
@@ -69,6 +77,7 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin {
const status: ConnectionStatus = {
connected,
connectionType: connected ? connectionType : 'none',
+ ...connectionDetails(),
};
return status;
@@ -80,6 +89,7 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin {
const status: ConnectionStatus = {
connected: true,
connectionType: connectionType,
+ ...connectionDetails(),
};
this.notifyListeners('networkStatusChange', status);
@@ -89,6 +99,8 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin {
const status: ConnectionStatus = {
connected: false,
connectionType: 'none',
+ constrained: false,
+ expensive: false,
};
this.notifyListeners('networkStatusChange', status);