Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Represents the state and type of the network connection.
| -------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----- |
| **`connected`** | <code>boolean</code> | Whether there is an active connection or not. | 1.0.0 |
| **`connectionType`** | <code><a href="#connectiontype">ConnectionType</a></code> | The type of network connection currently in use. If there is no active network connection, `connectionType` will be `'none'`. | 1.0.0 |
| **`constrained`** | <code>boolean</code> | Whether the active connection is constrained by platform data-saving or bandwidth-reduction signals. | 8.1.0 |
| **`expensive`** | <code>boolean</code> | Whether the active connection is considered expensive or metered by the platform. | 8.1.0 |


#### PluginListenerHandle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -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);
}
}
};
}

/**
Expand Down Expand Up @@ -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)) {
Expand All @@ -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();
Expand All @@ -113,12 +141,19 @@ 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);
}
}

/**
* Unregister the network callback.
*/
public void stopMonitoring() {
connectivityManager.unregisterNetworkCallback(connectivityCallback);
context.unregisterReceiver(restrictBackgroundReceiver);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public String getConnectionType() {

public boolean connected = false;
public ConnectionType connectionType = ConnectionType.NONE;
public boolean constrained = false;
public boolean expensive = false;
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
66 changes: 63 additions & 3 deletions network/ios/Sources/NetworkPlugin/Network.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Network

public typealias NetworkConnectionChangedObserver = (Network.Connection) -> Void

Expand All @@ -9,28 +10,87 @@ 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()
if reachability == nil {
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 {
Expand Down
17 changes: 12 additions & 5 deletions network/ios/Sources/NetworkPlugin/NetworkPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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
]
}
}

Expand Down
10 changes: 10 additions & 0 deletions network/ios/Tests/NetworkPluginTests/PluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}
14 changes: 14 additions & 0 deletions network/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions network/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ function translatedConnection(): ConnectionType {
return result;
}

function connectionDetails(): Pick<ConnectionStatus, 'constrained' | 'expensive'> {
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();
Expand All @@ -69,6 +77,7 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin {
const status: ConnectionStatus = {
connected,
connectionType: connected ? connectionType : 'none',
...connectionDetails(),
};

return status;
Expand All @@ -80,6 +89,7 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin {
const status: ConnectionStatus = {
connected: true,
connectionType: connectionType,
...connectionDetails(),
};

this.notifyListeners('networkStatusChange', status);
Expand All @@ -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);
Expand Down