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..9510cadfa 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 @@ -77,9 +77,10 @@ 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); + boolean validated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + networkStatus.connected = validated && hasInternet; + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { networkStatus.connectionType = NetworkStatus.ConnectionType.WIFI; } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { @@ -87,6 +88,8 @@ public NetworkStatus getNetworkStatus() { } else { networkStatus.connectionType = NetworkStatus.ConnectionType.UNKNOWN; } + networkStatus.internetReachable = networkStatus.connected; + networkStatus.state = networkStatus.connected ? "online" : "limited"; } } return networkStatus; @@ -104,6 +107,8 @@ private NetworkStatus getAndParseNetworkInfo() { } else if (typeName.equals("MOBILE")) { networkStatus.connectionType = NetworkStatus.ConnectionType.CELLULAR; } + networkStatus.internetReachable = networkStatus.connected; + networkStatus.state = networkStatus.connected ? "online" : "offline"; } return networkStatus; } 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..bd4206a63 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 @@ -26,6 +26,8 @@ public void load() { JSObject jsObject = new JSObject(); jsObject.put("connected", false); jsObject.put("connectionType", "none"); + jsObject.put("internetReachable", false); + jsObject.put("state", "offline"); notifyListeners(NETWORK_CHANGE_EVENT, jsObject); } else { updateNetworkStatus(); @@ -89,6 +91,8 @@ private JSObject parseNetworkStatus(NetworkStatus networkStatus) { JSObject jsObject = new JSObject(); jsObject.put("connected", networkStatus.connected); jsObject.put("connectionType", networkStatus.connectionType.getConnectionType()); + jsObject.put("internetReachable", networkStatus.internetReachable); + jsObject.put("state", networkStatus.state); 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..5588b0137 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 internetReachable = false; + public String state = "offline"; } diff --git a/network/ios/Sources/NetworkPlugin/Network.swift b/network/ios/Sources/NetworkPlugin/Network.swift index 255b214c8..ed0ef67e6 100644 --- a/network/ios/Sources/NetworkPlugin/Network.swift +++ b/network/ios/Sources/NetworkPlugin/Network.swift @@ -1,18 +1,43 @@ -import Foundation +public typealias NetworkStatusChangeObserver = (NetworkStatus) -> Void -public typealias NetworkConnectionChangedObserver = (Network.Connection) -> Void +public struct NetworkStatus { + let connection: Network.Connection + let internetReachable: Bool + let state: String + var connected: Bool { connection.isConnected } + var connectionType: String { connection.jsStringValue } +} public class Network { public enum NetworkError: Error { case initializationFailed } + public enum Connection { case unavailable, wifi, cellular } - + internal private(set) var reachability: Reachability? - var statusObserver: NetworkConnectionChangedObserver? - + var statusObserver: NetworkStatusChangeObserver? + + private let probeURLs: [URL] = [ + URL(string: "https://www.google.com/generate_204")!, + URL(string: "https://captive.apple.com")!, + URL(string: "https://one.one.one.one")!, + URL(string: "https://www.msftconnecttest.com/connecttest.txt")! + ] + + private lazy var probeSession: URLSession = { + let config = URLSessionConfiguration.ephemeral + config.timeoutIntervalForRequest = 5 + config.timeoutIntervalForResource = 5 + config.waitsForConnectivity = false + config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + return URLSession(configuration: config) + }() + + private var lastEmitted: NetworkStatus? + init() throws { reachability = try Reachability() if reachability == nil { @@ -20,16 +45,88 @@ public class Network { } // setup our callback(s) and start notifications reachability?.whenReachable = { [weak self] reachable in - self?.statusObserver?(reachable.connection.equivalentEnum) + self?.emitStatus(for: reachable.connection.equivalentEnum) } reachability?.whenUnreachable = { [weak self] _ in - self?.statusObserver?(Connection.unavailable) + self?.emitImmediate(.unavailable, internetReachable: false) } try reachability?.startNotifier() } + + func getStatus(completion: @escaping (NetworkStatus) -> Void) { + let connection = currentConnection() + if !connection.isConnected { + completion(buildStatus(connection: connection, internetReachable: false)) + return + } + + probeInternet { [weak self] reachable in + guard let self = self else { return } + completion(self.buildStatus(connection: connection, internetReachable: reachable)) + } + } + + func currentConnection() -> Connection { + reachability?.connection.equivalentEnum ?? .unavailable + } + + private func emitStatus(for connection: Connection) { + if !connection.isConnected { + emitImmediate(connection, internetReachable: false) + return + } + + probeInternet { [weak self] reachable in + guard let self = self else { return } + self.emitIfChanged(self.buildStatus(connection: connection, internetReachable: reachable)) + } + } + + private func emitImmediate(_ connection: Connection, internetReachable: Bool) { + emitIfChanged(buildStatus(connection: connection, internetReachable: internetReachable)) + } + + private func emitIfChanged(_ status: NetworkStatus) { + guard status.connection != lastEmitted?.connection + || status.internetReachable != lastEmitted?.internetReachable + || status.state != lastEmitted?.state else { return } + lastEmitted = status + statusObserver?(status) + } + + private func probeInternet(completion: @escaping (Bool) -> Void) { + let group = DispatchGroup() + var reachable = false + let lock = NSLock() + for url in probeURLs { + group.enter() + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + probeSession.dataTask(with: request) { _, response, _ in + if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { + lock.lock() + reachable = true + lock.unlock() + } + group.leave() + }.resume() + } - func currentStatus() -> Network.Connection { - return reachability?.connection.equivalentEnum ?? Connection.unavailable + group.notify(queue: .global(qos: .utility)) { + completion(reachable) + } + } + + private func buildStatus(connection: Connection, internetReachable: Bool) -> NetworkStatus { + let state: String + if !connection.isConnected { + state = "offline" + } else if internetReachable { + state = "online" + } else { + state = "limited" + } + return NetworkStatus(connection: connection, internetReachable: internetReachable, state: state) } } diff --git a/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift b/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift index 061dce4e4..36737d460 100644 --- a/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift +++ b/network/ios/Sources/NetworkPlugin/NetworkPlugin.swift @@ -15,10 +15,12 @@ public class NetworkPlugin: CAPPlugin, CAPBridgedPlugin { do { implementation = try Network() implementation?.statusObserver = { [weak self] status in - CAPLog.print(status.logMessage) + CAPLog.print("\(status.connection.logMessage) | state=\(status.state)") self?.notifyListeners("networkStatusChange", data: [ - "connected": status.isConnected, - "connectionType": status.jsStringValue + "connected": status.connected, + "connectionType": status.connectionType, + "internetReachable": status.internetReachable, + "state": status.state ]) } } catch let error { @@ -27,8 +29,24 @@ 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]) + guard let implementation = implementation else { + call.resolve([ + "connected": false, + "connectionType": "none", + "internetReachable": false, + "state": "offline" + ]) + return + } + + implementation.getStatus { status in + call.resolve([ + "connected": status.connected, + "connectionType": status.connectionType, + "internetReachable": status.internetReachable, + "state": status.state + ]) + } } } diff --git a/network/src/definitions.ts b/network/src/definitions.ts index ea2a3a1a5..f951d1500 100644 --- a/network/src/definitions.ts +++ b/network/src/definitions.ts @@ -47,6 +47,16 @@ export interface ConnectionStatus { * @since 1.0.0 */ connectionType: ConnectionType; + + /** + * Whether the device can reach the internet or not. + */ + internetReachable: boolean; + + /** + * The current state of the internet connection. + */ + state: 'online' | 'limited' | 'offline'; } /** diff --git a/network/src/web.ts b/network/src/web.ts index 7057184f7..f776031bd 100644 --- a/network/src/web.ts +++ b/network/src/web.ts @@ -69,6 +69,8 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin { const status: ConnectionStatus = { connected, connectionType: connected ? connectionType : 'none', + internetReachable: connected, + state: connected ? 'online' : 'offline', }; return status; @@ -80,6 +82,8 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin { const status: ConnectionStatus = { connected: true, connectionType: connectionType, + internetReachable: true, + state: 'online', }; this.notifyListeners('networkStatusChange', status); @@ -89,6 +93,8 @@ export class NetworkWeb extends WebPlugin implements NetworkPlugin { const status: ConnectionStatus = { connected: false, connectionType: 'none', + internetReachable: false, + state: 'offline' }; this.notifyListeners('networkStatusChange', status);