summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2023-04-24 21:20:24 +0200
committerJon Petersson <jon.petersson@kvadrat.se>2023-05-04 13:10:32 +0200
commit79ab47b947992016cea8ead12504ae11ff01fde7 (patch)
tree1a9e082e649b92a7e54119c7822d189e704b7430
parent6ed13e3a8ad510f2d2f751ffe57301252294160e (diff)
downloadmullvadvpn-79ab47b947992016cea8ead12504ae11ff01fde7.tar.xz
mullvadvpn-79ab47b947992016cea8ead12504ae11ff01fde7.zip
Improve behavior when there's no network
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift4
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift41
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationController.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift19
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift4
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift36
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift13
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift50
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: