diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountDataThrottling.swift | 80 | ||||
| -rw-r--r-- | ios/MullvadVPN/CustomNavigationController.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/SceneDelegate.swift | 135 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 33 |
5 files changed, 202 insertions, 56 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 84e1b45cf0..2ad363563b 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -163,6 +163,7 @@ 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; + 587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */; }; 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; }; 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; }; @@ -465,6 +466,7 @@ 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tunnel+Messaging.swift"; sourceTree = "<group>"; }; 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; }; 58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; }; + 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataThrottling.swift; sourceTree = "<group>"; }; 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; }; 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV1.swift; sourceTree = "<group>"; }; 587B7535266528A200DEF7E9 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; }; @@ -847,6 +849,7 @@ isa = PBXGroup; children = ( 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */, + 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, @@ -1305,6 +1308,7 @@ 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */, 585DA89926B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, + 587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */, 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, 5842102E282D3FC200F24E46 /* ResultBlockOperation.swift in Sources */, diff --git a/ios/MullvadVPN/AccountDataThrottling.swift b/ios/MullvadVPN/AccountDataThrottling.swift new file mode 100644 index 0000000000..31f0b1f849 --- /dev/null +++ b/ios/MullvadVPN/AccountDataThrottling.swift @@ -0,0 +1,80 @@ +// +// AccountDataThrottling.swift +// MullvadVPN +// +// Created by pronebird on 09/08/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct used for throttling UI calls to update account data via tunnel manager. +struct AccountDataThrottling { + /// Default cooldown interval between requests. + private static let defaultWaitInterval: TimeInterval = 60 + + /// Cooldown interval used when account has already expired. + private static let waitIntervalForExpiredAccount: TimeInterval = 10 + + /// Interval in days when account is considered to be close to expiry. + private static let closeToExpiryDays = 4 + + enum Condition { + /// Always update account data. + case always + + /// Only update account data when account is close to expiry or already expired. + case whenCloseToExpiryAndBeyond + } + + let tunnelManager: TunnelManager + private(set) var lastUpdate: Date? + + init(tunnelManager: TunnelManager = .shared) { + self.tunnelManager = tunnelManager + } + + mutating func requestUpdate(condition: Condition) { + guard let accountData = tunnelManager.deviceState.accountData else { + return + } + + let now = Date() + + switch condition { + case .always: + break + + case .whenCloseToExpiryAndBeyond: + guard let closeToExpiry = Calendar.current.date( + byAdding: .day, + value: Self.closeToExpiryDays * -1, + to: accountData.expiry + ) else { return } + + if closeToExpiry > now { + return + } + } + + let waitInterval = accountData.expiry > now + ? Self.defaultWaitInterval + : Self.waitIntervalForExpiredAccount + + let nextUpdateAfter = lastUpdate?.addingTimeInterval(waitInterval) + let comparisonResult = nextUpdateAfter?.compare(now) ?? .orderedAscending + + switch comparisonResult { + case .orderedAscending, .orderedSame: + lastUpdate = Date() + tunnelManager.updateAccountData() + + case .orderedDescending: + break + } + } + + mutating func reset() { + lastUpdate = nil + } +} diff --git a/ios/MullvadVPN/CustomNavigationController.swift b/ios/MullvadVPN/CustomNavigationController.swift index a19136b0c8..098ba7042f 100644 --- a/ios/MullvadVPN/CustomNavigationController.swift +++ b/ios/MullvadVPN/CustomNavigationController.swift @@ -56,12 +56,18 @@ class CustomNavigationController: UINavigationController, UINavigationBarDelegat // Only call super implementation when we want to pop the controller if shouldPop { + willPop(navigationItem: item) + // Call super implementation return customNavigationController_navigationBar(navigationBar, shouldPop: item) } else { return shouldPop } } + + func willPop(navigationItem: UINavigationItem) { + // Override in subclasses + } } private class CustomPopGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 003672284d..d07b019e34 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -27,6 +27,7 @@ class SceneDelegate: UIResponder { private var connectController: ConnectViewController? private weak var settingsNavController: SettingsNavigationController? private var lastLoginAction: LoginAction? + private var accountDataThrottling = AccountDataThrottling() override init() { super.init() @@ -73,6 +74,8 @@ class SceneDelegate: UIResponder { RelayCache.Tracker.shared.addObserver(self) NotificationManager.shared.delegate = self + + accountDataThrottling.requestUpdate(condition: .always) } private func setShowsPrivacyOverlay(_ showOverlay: Bool) { @@ -115,6 +118,14 @@ class SceneDelegate: UIResponder { @objc private func sceneDidBecomeActive() { TunnelManager.shared.refreshTunnelStatus() + if isSceneConfigured { + accountDataThrottling.requestUpdate( + condition: settingsNavController == nil + ? .whenCloseToExpiryAndBeyond + : .always + ) + } + RelayCache.Tracker.shared.startPeriodicUpdates() TunnelManager.shared.startPeriodicPrivateKeyRotation() AddressCache.Tracker.shared.startPeriodicUpdates() @@ -184,14 +195,13 @@ extension SceneDelegate: RootContainerViewControllerDelegate { ) { // Check if settings controller is already presented. if let settingsNavController = settingsNavController { - if let route = route { - settingsNavController.navigate(to: route, animated: animated) - } else { - settingsNavController.popToRootViewController(animated: animated) - } + settingsNavController.navigate(to: route ?? .root, animated: animated) } else { let navController = makeSettingsNavigationController(route: route) + // Refresh account data each time user opens settings + accountDataThrottling.requestUpdate(condition: .always) + // On iPad the login controller can be presented modally above the root container. // in that case we have to use the presented controller to present the next modal. if let presentedController = controller.presentedViewController { @@ -488,6 +498,55 @@ extension SceneDelegate { fatalError() } } + + private func showRevokedDeviceView() { + switch UIDevice.current.userInterfaceIdiom { + case .phone: + guard let loginController = rootContainer.viewControllers.first as? LoginViewController + else { + return + } + + loginController.reset() + + let viewControllers = [ + loginController, + makeRevokedDeviceController(), + ] + + rootContainer.setViewControllers(viewControllers, animated: true) + + case .pad: + guard let loginController = modalRootContainer.viewControllers + .first as? LoginViewController + else { + return + } + + loginController.reset() + + let viewControllers = [ + loginController, + makeRevokedDeviceController(), + ] + + let didDismissSettings = { + self.showSplitViewMaster(false, animated: true) + self.presentModalRootContainerIfNeeded(animated: true) + } + + modalRootContainer.setViewControllers(viewControllers, animated: isModalRootPresented) + + if let settingsNavController = settingsNavController { + settingsNavController.dismiss(animated: true, completion: didDismissSettings) + } else { + didDismissSettings() + } + + default: + fatalError() + } + } } // MARK: - LoginViewControllerDelegate @@ -608,6 +667,19 @@ extension SceneDelegate: DeviceManagementViewControllerDelegate { extension SceneDelegate: SettingsNavigationControllerDelegate { func settingsNavigationController( _ controller: SettingsNavigationController, + willNavigateTo route: SettingsNavigationRoute + ) { + switch route { + case .root, .account: + accountDataThrottling.requestUpdate(condition: .always) + + default: + break + } + } + + func settingsNavigationController( + _ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason ) { if case .userLoggedOut = reason { @@ -771,53 +843,16 @@ extension SceneDelegate: TunnelObserver { } func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - guard deviceState == .revoked else { return } - - switch UIDevice.current.userInterfaceIdiom { - case .phone: - guard let loginController = rootContainer.viewControllers.first as? LoginViewController - else { - return - } - - loginController.reset() - - let viewControllers = [ - loginController, - makeRevokedDeviceController(), - ] - - rootContainer.setViewControllers(viewControllers, animated: true) - - case .pad: - guard let loginController = modalRootContainer.viewControllers - .first as? LoginViewController - else { - return - } - - loginController.reset() - - let viewControllers = [ - loginController, - makeRevokedDeviceController(), - ] - - let didDismissSettings = { - self.showSplitViewMaster(false, animated: true) - self.presentModalRootContainerIfNeeded(animated: true) - } + switch deviceState { + case .loggedIn: + break - modalRootContainer.setViewControllers(viewControllers, animated: isModalRootPresented) + case .loggedOut: + accountDataThrottling.reset() - if let settingsNavController = settingsNavController { - settingsNavController.dismiss(animated: true, completion: didDismissSettings) - } else { - didDismissSettings() - } - - default: - fatalError() + case .revoked: + accountDataThrottling.reset() + showRevokedDeviceView() } } diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift index cc59a05c4e..88bb80b519 100644 --- a/ios/MullvadVPN/SettingsNavigationController.swift +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -10,6 +10,7 @@ import Foundation import UIKit enum SettingsNavigationRoute { + case root case account case preferences case problemReport @@ -23,6 +24,11 @@ enum SettingsDismissReason { protocol SettingsNavigationControllerDelegate: AnyObject { func settingsNavigationController( _ controller: SettingsNavigationController, + willNavigateTo route: SettingsNavigationRoute + ) + + func settingsNavigationController( + _ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason ) } @@ -43,10 +49,7 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont init() { super.init(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil) - let settingsController = SettingsViewController() - settingsController.delegate = self - - pushViewController(settingsController, animated: false) + setViewControllers([makeViewController(for: .root)], animated: false) } override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { @@ -63,9 +66,14 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont super.viewDidLoad() navigationBar.prefersLargeTitles = true + } + + override func willPop(navigationItem: UINavigationItem) { + let index = viewControllers.firstIndex { $0.navigationItem == navigationItem } - // Update account expiry - TunnelManager.shared.updateAccountData() + if viewControllers.count > 1, index == 1 { + settingsDelegate?.settingsNavigationController(self, willNavigateTo: .root) + } } // MARK: - SettingsViewControllerDelegate @@ -83,7 +91,15 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont // MARK: - Navigation func navigate(to route: SettingsNavigationRoute, animated: Bool) { + guard route != .root else { + popToRootViewController(animated: animated) + return + } + + settingsDelegate?.settingsNavigationController(self, willNavigateTo: route) + let nextViewController = makeViewController(for: route) + if let rootController = viewControllers.first, viewControllers.count > 1 { setViewControllers([rootController, nextViewController], animated: animated) } else { @@ -93,6 +109,11 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont private func makeViewController(for route: SettingsNavigationRoute) -> UIViewController { switch route { + case .root: + let settingsController = SettingsViewController() + settingsController.delegate = self + return settingsController + case .account: let controller = AccountViewController() controller.delegate = self |
