diff options
| author | Emīls <emils@mullvad.net> | 2023-05-04 13:15:24 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2023-05-04 13:15:24 +0200 |
| commit | 40b25b95544f94e321b0216200b4513e90faaf35 (patch) | |
| tree | 1a9e082e649b92a7e54119c7822d189e704b7430 | |
| parent | 6ed13e3a8ad510f2d2f751ffe57301252294160e (diff) | |
| parent | 79ab47b947992016cea8ead12504ae11ff01fde7 (diff) | |
| download | mullvadvpn-40b25b95544f94e321b0216200b4513e90faaf35.tar.xz mullvadvpn-40b25b95544f94e321b0216200b4513e90faaf35.zip | |
Merge branch 'bad-behavior-when-theres-no-connectivity-ios-62'
8 files changed, 145 insertions, 40 deletions
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index b4695072dd..4b3816b630 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -778,13 +778,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo guard tunnelManager.deviceState.isLoggedIn else { return false } switch tunnelManager.tunnelStatus.state { - case .connected, .connecting, .reconnecting, .waitingForConnectivity: + case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection): tunnelManager.reconnectTunnel(selectNewRelay: true) case .disconnecting, .disconnected: tunnelManager.startTunnel() - case .pendingReconnect: + case .pendingReconnect, .waitingForConnectivity(.noNetwork): break } return true diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index daa5b6bf9b..b9302211c2 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -10,6 +10,7 @@ import Foundation final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider { private var isWaitingForConnectivity = false + private var noNetwork = false private var packetTunnelError: String? private var tunnelManagerError: Error? private var tunnelObserver: TunnelBlockObserver? @@ -25,6 +26,8 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific return notificationDescription(for: tunnelManagerError) } else if isWaitingForConnectivity { return connectivityNotificationDescription() + } else if noNetwork { + return noNetworkNotificationDescription() } else { return nil } @@ -57,8 +60,9 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific ) let invalidateForManagerError = updateTunnelManagerError(tunnelStatus.state) let invalidateForConnectivity = updateConnectivity(tunnelStatus.state) + let invalidateForNetwork = updateNetwork(tunnelStatus.state) - if invalidateForTunnelError || invalidateForManagerError || invalidateForConnectivity { + if invalidateForTunnelError || invalidateForManagerError || invalidateForConnectivity || invalidateForNetwork { invalidate() } } @@ -74,7 +78,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific } private func updateConnectivity(_ tunnelState: TunnelState) -> Bool { - let isWaitingState = tunnelState == .waitingForConnectivity + let isWaitingState = tunnelState == .waitingForConnectivity(.noConnection) if isWaitingForConnectivity != isWaitingState { isWaitingForConnectivity = isWaitingState @@ -84,6 +88,17 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific return false } + private func updateNetwork(_ tunnelState: TunnelState) -> Bool { + let isWaitingState = tunnelState == .waitingForConnectivity(.noNetwork) + + if noNetwork != isWaitingState { + noNetwork = isWaitingState + return true + } + + return false + } + private func updateTunnelManagerError(_ tunnelState: TunnelState) -> Bool { switch tunnelState { case .connecting, .connected, .reconnecting: @@ -181,4 +196,26 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific ) ) } + + private func noNetworkNotificationDescription() -> InAppNotificationDescriptor { + return InAppNotificationDescriptor( + identifier: identifier, + style: .warning, + title: NSLocalizedString( + "TUNNEL_NO_NETWORK_INAPP_NOTIFICATION_TITLE", + value: "NETWORK ISSUES", + comment: "" + ), + body: .init( + string: NSLocalizedString( + "TUNNEL_NO_NETWORK_INAPP_NOTIFICATION_BODY", + value: """ + Your device is offline. Try connecting again when the device \ + has access to Internet. + """, + comment: "" + ) + ) + ) + } } diff --git a/ios/MullvadVPN/Notifications/UI/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift index 4825fa689b..be87f74a07 100644 --- a/ios/MullvadVPN/Notifications/UI/NotificationController.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift @@ -70,9 +70,9 @@ final class NotificationController: UIViewController { hideBannerConstraint?.isActive = true } - let finish = { - if !show { - self.bannerView.isHidden = true + let finish = { [weak self] in + if self?.lastNotification == nil { + self?.bannerView.isHidden = true } completion?() } @@ -108,17 +108,6 @@ final class NotificationController: UIViewController { bannerView.action = notification.action bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body.string)" - if animated { - let animator = UIViewPropertyAnimator( - duration: 0.25, - timingParameters: UICubicTimingParameters(animationCurve: .easeOut) - ) - animator.addAnimations { - self.view.layoutIfNeeded() - } - animator.startAnimation() - } - // Do not emit the .layoutChanged unless the banner is focused to avoid capturing // the voice over focus. if bannerView.accessibilityElementIsFocused() { @@ -133,6 +122,7 @@ final class NotificationController: UIViewController { setNotification(notification, animated: showsBanner) toggleBanner(show: true, animated: true) } else { + lastNotification = nil toggleBanner(show: false, animated: animated) } } diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 6b0248cf4c..1083ab5e89 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -18,16 +18,19 @@ class MapConnectionStatusOperation: AsyncOperation { private let interactor: TunnelInteractor private let connectionStatus: NEVPNStatus private var request: Cancellable? + private var pathStatus: Network.NWPath.Status? private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation") init( queue: DispatchQueue, interactor: TunnelInteractor, - connectionStatus: NEVPNStatus + connectionStatus: NEVPNStatus, + networkStatus: Network.NWPath.Status? ) { self.interactor = interactor self.connectionStatus = connectionStatus + pathStatus = networkStatus super.init(dispatchQueue: queue) } @@ -56,7 +59,7 @@ class MapConnectionStatusOperation: AsyncOperation { if packetTunnelStatus.isNetworkReachable { return packetTunnelStatus.tunnelRelay.map { .connecting($0) } } else { - return .waitingForConnectivity + return .waitingForConnectivity(.noConnection) } } return @@ -66,7 +69,7 @@ class MapConnectionStatusOperation: AsyncOperation { if packetTunnelStatus.isNetworkReachable { return packetTunnelStatus.tunnelRelay.map { .reconnecting($0) } } else { - return .waitingForConnectivity + return .waitingForConnectivity(.noConnection) } } return @@ -76,7 +79,7 @@ class MapConnectionStatusOperation: AsyncOperation { if packetTunnelStatus.isNetworkReachable { return packetTunnelStatus.tunnelRelay.map { .connected($0) } } else { - return .waitingForConnectivity + return .waitingForConnectivity(.noConnection) } } return @@ -97,7 +100,9 @@ class MapConnectionStatusOperation: AsyncOperation { default: interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() - tunnelStatus.state = .disconnected + tunnelStatus.state = pathStatus == .unsatisfied + ? .waitingForConnectivity(.noNetwork) + : .disconnected } } @@ -115,7 +120,9 @@ class MapConnectionStatusOperation: AsyncOperation { case .invalid: interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() - tunnelStatus.state = .disconnected + tunnelStatus.state = pathStatus == .unsatisfied + ? .waitingForConnectivity(.noNetwork) + : .disconnected } @unknown default: diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 38e60c7cf4..f7c2dd4245 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -35,7 +35,7 @@ class StopTunnelOperation: ResultOperation<Void> { finish(result: .success(())) - case .connected, .connecting, .reconnecting, .waitingForConnectivity: + case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection): guard let tunnel = interactor.tunnel else { finish(result: .failure(UnsetTunnelError())) return @@ -55,7 +55,7 @@ class StopTunnelOperation: ResultOperation<Void> { } } - case .disconnected, .disconnecting, .pendingReconnect: + case .disconnected, .disconnecting, .pendingReconnect, .waitingForConnectivity(.noNetwork): finish(result: .success(())) } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index b2644dcec0..3dcb412734 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -57,6 +57,7 @@ final class TunnelManager: StorePaymentObserver { private var statusObserver: TunnelStatusBlockObserver? private var lastMapConnectionStatusOperation: Operation? private let observerList = ObserverList<TunnelObserver>() + private var networkMonitor: NWPathMonitor? private var privateKeyRotationTimer: DispatchSourceTimer? private var isRunningPeriodicPrivateKeyRotation = false @@ -177,6 +178,7 @@ final class TunnelManager: StorePaymentObserver { } self.updatePrivateKeyRotationTimer() + self.startNetworkMonitor() completionHandler(completion.error) } @@ -654,11 +656,11 @@ final class TunnelManager: StorePaymentObserver { // while the tunnel process is trying to connect. startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) - case .connected, .waitingForConnectivity: + case .connected, .waitingForConnectivity(.noConnection): // Start polling tunnel status to keep connectivity status up to date. startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval) - case .pendingReconnect, .disconnecting, .disconnected: + case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork): // Stop polling tunnel status once connection moved to final state. cancelPollingTunnelStatus() } @@ -742,6 +744,10 @@ final class TunnelManager: StorePaymentObserver { refreshTunnelStatus() } + private func didUpdateNetworkPath(_ path: Network.NWPath) { + updateTunnelStatus(tunnel?.status ?? .disconnected) + } + fileprivate func selectRelay() throws -> RelaySelectorResult { let cachedRelays = try relayCacheTracker.getCachedRelays() @@ -795,10 +801,33 @@ final class TunnelManager: StorePaymentObserver { guard let self = self else { return } self.logger.debug("VPN connection status changed to \(status).") + + if [.disconnected, .invalid].contains(tunnel.status) { + self.startNetworkMonitor() + } else { + self.cancelNetworkMonitor() + } + self.updateTunnelStatus(status) } } + private func startNetworkMonitor() { + cancelNetworkMonitor() + + networkMonitor = NWPathMonitor() + networkMonitor?.pathUpdateHandler = { [weak self] path in + self?.didUpdateNetworkPath(path) + } + + networkMonitor?.start(queue: internalQueue) + } + + private func cancelNetworkMonitor() { + networkMonitor?.cancel() + networkMonitor = nil + } + private func unsubscribeVPNStatusObserver() { nslock.lock() defer { nslock.unlock() } @@ -826,7 +855,8 @@ final class TunnelManager: StorePaymentObserver { let operation = MapConnectionStatusOperation( queue: internalQueue, interactor: TunnelInteractorProxy(self), - connectionStatus: connectionStatus + connectionStatus: connectionStatus, + networkStatus: networkMonitor?.currentPath.status ) operation.addCondition( diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index d7ba65f673..d641decc80 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -33,6 +33,13 @@ struct TunnelStatus: Equatable, CustomStringConvertible { /// An enum that describes the tunnel state. enum TunnelState: Equatable, CustomStringConvertible { + enum WaitingForConnectionReason { + /// Tunnel connection is down. + case noConnection + /// Network is down. + case noNetwork + } + /// Pending reconnect after disconnect. case pendingReconnect @@ -56,7 +63,7 @@ enum TunnelState: Equatable, CustomStringConvertible { case reconnecting(PacketTunnelRelay) /// Waiting for connectivity to come back up. - case waitingForConnectivity + case waitingForConnectivity(WaitingForConnectionReason) var description: String { switch self { @@ -83,9 +90,9 @@ enum TunnelState: Equatable, CustomStringConvertible { var isSecured: Bool { switch self { - case .reconnecting, .connecting, .connected, .waitingForConnectivity: + case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection): return true - case .pendingReconnect, .disconnecting, .disconnected: + case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork): return false } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index e962133f09..15d41c95df 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -159,6 +159,7 @@ final class TunnelControlView: UIView { let views = actionButtons.map { self.view(forActionButton: $0) } updateButtonTitles() + updateButtonEnabledStates() setArrangedButtons(views) } @@ -204,6 +205,23 @@ final class TunnelControlView: UIView { ) } + private func updateButtonEnabledStates() { + let allButtons = [ + connectButton, + selectLocationButton, + cancelButton, + splitDisconnectButton.primaryButton, + splitDisconnectButton.secondaryButton, + ] + + switch tunnelState { + case .waitingForConnectivity(.noNetwork): + allButtons.forEach { $0.isEnabled = false } + default: + allButtons.forEach { $0.isEnabled = true } + } + } + private func updateTunnelRelay() { if let tunnelRelay = tunnelState.relay { cityLabel.attributedText = attributedStringForLocation( @@ -423,13 +441,13 @@ final class TunnelControlView: UIView { private extension TunnelState { var textColorForSecureLabel: UIColor { switch self { - case .connecting, .reconnecting, .waitingForConnectivity: + case .connecting, .reconnecting, .waitingForConnectivity(.noConnection): return .white case .connected: return .successColor - case .disconnecting, .disconnected, .pendingReconnect: + case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork): return .dangerColor } } @@ -475,13 +493,21 @@ private extension TunnelState { comment: "" ) - case .waitingForConnectivity: + case .waitingForConnectivity(.noConnection): return NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", tableName: "Main", value: "Blocked connection", comment: "" ) + + case .waitingForConnectivity(.noNetwork): + return NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK", + tableName: "Main", + value: "No network", + comment: "" + ) } } @@ -554,7 +580,7 @@ private extension TunnelState { tunnelInfo.location.country ) - case .waitingForConnectivity: + case .waitingForConnectivity(.noConnection): return NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", tableName: "Main", @@ -562,6 +588,14 @@ private extension TunnelState { comment: "" ) + case .waitingForConnectivity(.noNetwork): + return NSLocalizedString( + "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "No network", + comment: "" + ) + case .disconnecting(.nothing): return NSLocalizedString( "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", @@ -584,11 +618,11 @@ private extension TunnelState { switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { case (.phone, _), (.pad, .compact): switch self { - case .disconnected, .disconnecting(.nothing): + case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): return [.selectLocation, .connect] case .connecting, .pendingReconnect, .disconnecting(.reconnect), - .waitingForConnectivity: + .waitingForConnectivity(.noConnection): return [.selectLocation, .cancel] case .connected, .reconnecting: @@ -597,11 +631,11 @@ private extension TunnelState { case (.pad, .regular): switch self { - case .disconnected, .disconnecting(.nothing): + case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): return [.connect] case .connecting, .pendingReconnect, .disconnecting(.reconnect), - .waitingForConnectivity: + .waitingForConnectivity(.noConnection): return [.cancel] case .connected, .reconnecting: |
