diff options
| -rw-r--r-- | ios/CHANGELOG.md | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/Info.plist | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/IntentHandlers.swift | 68 | ||||
| -rw-r--r-- | ios/MullvadVPN/Intents.intentdefinition | 163 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManager.swift | 48 |
7 files changed, 300 insertions, 25 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 92ab5b5170..d3100508f0 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -30,6 +30,8 @@ Line wrap the file at 100 chars. Th - Add revoked device view displayed when the app detects that device is no longer registered on backend. - Add ability to manage registered devices if too many devices detected during log-in. +- Add intents: start VPN, stop VPN, reconnect VPN (acts as start VPN when the tunnel is down, + otherwise picks new relay). ### Fixed - Improve random port distribution. Should be less biased towards port 53. diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8e807d6229..d8b9c9093a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -152,6 +152,8 @@ 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB8225498CA20051A0A4 /* Swizzle.swift */; }; 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; }; 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; }; + 5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 5872631A283F6EAB00E14ADF /* Intents.intentdefinition */; }; + 5872631D283F755900E14ADF /* IntentHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5872631C283F755900E14ADF /* IntentHandlers.swift */; }; 58727283265D173C00F315B2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 58727282265D173C00F315B2 /* LaunchScreen.storyboard */; }; 5872D6E8286304DE00DB5F4E /* TermsOfService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */; }; 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587425C02299833500CA2045 /* RootContainerViewController.swift */; }; @@ -456,6 +458,8 @@ 5871FB8225498CA20051A0A4 /* Swizzle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swizzle.swift; sourceTree = "<group>"; }; 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; }; 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; }; + 5872631A283F6EAB00E14ADF /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = "<group>"; }; + 5872631C283F755900E14ADF /* IntentHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandlers.swift; sourceTree = "<group>"; }; 58727282265D173C00F315B2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfService.swift; sourceTree = "<group>"; }; 587425C02299833500CA2045 /* RootContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootContainerViewController.swift; sourceTree = "<group>"; }; @@ -890,6 +894,8 @@ 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */, 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */, 58CE5E6F224146210008646E /* Info.plist */, + 5872631C283F755900E14ADF /* IntentHandlers.swift */, + 5872631A283F6EAB00E14ADF /* Intents.intentdefinition */, 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */, 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */, 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */, @@ -1331,6 +1337,7 @@ 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 58289082286B590900478596 /* UIFont+Monospaced.swift in Sources */, 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, + 5872631B283F6EAB00E14ADF /* Intents.intentdefinition in Sources */, 585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 58DF5B742851FF3F00E92647 /* InputOperation.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, @@ -1447,6 +1454,7 @@ 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */, 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, + 5872631D283F755900E14ADF /* IntentHandlers.swift in Sources */, 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 3db5eebe09..6908f86a43 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import BackgroundTasks import StoreKit import UserNotifications +import Intents import Logging @UIApplicationMain @@ -80,6 +81,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? { + switch intent { + case is StartVPNIntent: + return StartVPNIntentHandler() + case is StopVPNIntent: + return StopVPNIntentHandler() + case is ReconnectVPNIntent: + return ReconnectVPNIntentHandler() + default: + return nil + } + } + func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void diff --git a/ios/MullvadVPN/Info.plist b/ios/MullvadVPN/Info.plist index d4a1405adb..8a22ff38f0 100644 --- a/ios/MullvadVPN/Info.plist +++ b/ios/MullvadVPN/Info.plist @@ -4,11 +4,6 @@ <dict> <key>ApplicationSecurityGroupIdentifier</key> <string>$(SECURITY_GROUP_IDENTIFIER)</string> - <key>UIApplicationSceneManifest</key> - <dict> - <key>UIApplicationSupportsMultipleScenes</key> - <true/> - </dict> <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>net.mullvad.MullvadVPN.AppRefresh</string> @@ -33,10 +28,27 @@ <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>$(CURRENT_PROJECT_VERSION)</string> + <key>INIntentsSupported</key> + <array> + <string>StartVPNIntent</string> + <string>StopVPNIntent</string> + <string>ReconnectVPNIntent</string> + </array> <key>ITSAppUsesNonExemptEncryption</key> <false/> <key>LSRequiresIPhoneOS</key> <true/> + <key>NSUserActivityTypes</key> + <array> + <string>StartVPNIntent</string> + <string>StopVPNIntent</string> + <string>ReconnectVPNIntent</string> + </array> + <key>UIApplicationSceneManifest</key> + <dict> + <key>UIApplicationSupportsMultipleScenes</key> + <true/> + </dict> <key>UIBackgroundModes</key> <array> <string>fetch</string> diff --git a/ios/MullvadVPN/IntentHandlers.swift b/ios/MullvadVPN/IntentHandlers.swift new file mode 100644 index 0000000000..8764df7ef9 --- /dev/null +++ b/ios/MullvadVPN/IntentHandlers.swift @@ -0,0 +1,68 @@ +// +// IntentHandlers.swift +// MullvadVPN +// +// Created by pronebird on 26/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +final class StartVPNIntentHandler: NSObject, StartVPNIntentHandling { + func handle(intent: StartVPNIntent, completion: @escaping (StartVPNIntentResponse) -> Void) { + TunnelManager.shared.startTunnel { operationCompletion in + let code: StartVPNIntentResponseCode = operationCompletion.isSuccess + ? .success : .failure + let response = StartVPNIntentResponse(code: code, userActivity: nil) + + completion(response) + } + } +} + +final class StopVPNIntentHandler: NSObject, StopVPNIntentHandling { + func handle(intent: StopVPNIntent, completion: @escaping (StopVPNIntentResponse) -> Void) { + TunnelManager.shared.stopTunnel { operationCompletion in + let code: StopVPNIntentResponseCode = operationCompletion.isSuccess + ? .success : .failure + let response = StopVPNIntentResponse(code: code, userActivity: nil) + + completion(response) + } + } +} + +final class ReconnectVPNIntentHandler: NSObject, ReconnectVPNIntentHandling { + func handle(intent: ReconnectVPNIntent, completion: @escaping (ReconnectVPNIntentResponse) -> Void) { + let tunnelManager = TunnelManager.shared + + tunnelManager.reconnectTunnel(selectNewRelay: true) { operationCompletion in + let error = operationCompletion.error + + let shouldStartTunnel: Bool + if case .tunnelDown = error as? SendTunnelProviderMessageError { + shouldStartTunnel = true + } else { + shouldStartTunnel = error is UnsetTunnelError + } + + if shouldStartTunnel { + tunnelManager.startTunnel { operationCompletion in + completion( + ReconnectVPNIntentResponse( + code: operationCompletion.isSuccess ? .success : .failure, + userActivity: nil + ) + ) + } + } else { + completion( + ReconnectVPNIntentResponse( + code: operationCompletion.isSuccess ? .success : .failure, + userActivity: nil + ) + ) + } + } + } +} diff --git a/ios/MullvadVPN/Intents.intentdefinition b/ios/MullvadVPN/Intents.intentdefinition new file mode 100644 index 0000000000..c05dda03cc --- /dev/null +++ b/ios/MullvadVPN/Intents.intentdefinition @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>INEnums</key> + <array/> + <key>INIntentDefinitionModelVersion</key> + <string>1.2</string> + <key>INIntentDefinitionNamespace</key> + <string>5pIysl</string> + <key>INIntentDefinitionSystemVersion</key> + <string>21F79</string> + <key>INIntentDefinitionToolsBuildVersion</key> + <string>13F100</string> + <key>INIntentDefinitionToolsVersion</key> + <string>13.4.1</string> + <key>INIntents</key> + <array> + <dict> + <key>INIntentCategory</key> + <string>generic</string> + <key>INIntentConfigurable</key> + <true/> + <key>INIntentDescriptionID</key> + <string>jBv2Ko</string> + <key>INIntentIneligibleForSuggestions</key> + <true/> + <key>INIntentManagedParameterCombinations</key> + <dict> + <key></key> + <dict> + <key>INIntentParameterCombinationSupportsBackgroundExecution</key> + <true/> + <key>INIntentParameterCombinationUpdatesLinked</key> + <true/> + </dict> + </dict> + <key>INIntentName</key> + <string>StartVPN</string> + <key>INIntentResponse</key> + <dict> + <key>INIntentResponseCodes</key> + <array> + <dict> + <key>INIntentResponseCodeName</key> + <string>success</string> + <key>INIntentResponseCodeSuccess</key> + <true/> + </dict> + <dict> + <key>INIntentResponseCodeName</key> + <string>failure</string> + </dict> + </array> + <key>INIntentResponseLastParameterTag</key> + <integer>1</integer> + </dict> + <key>INIntentTitle</key> + <string>Start VPN</string> + <key>INIntentTitleID</key> + <string>EpurV0</string> + <key>INIntentType</key> + <string>Custom</string> + <key>INIntentVerb</key> + <string>Do</string> + </dict> + <dict> + <key>INIntentCategory</key> + <string>generic</string> + <key>INIntentConfigurable</key> + <true/> + <key>INIntentDescriptionID</key> + <string>uwGwEw</string> + <key>INIntentIneligibleForSuggestions</key> + <true/> + <key>INIntentManagedParameterCombinations</key> + <dict> + <key></key> + <dict> + <key>INIntentParameterCombinationSupportsBackgroundExecution</key> + <true/> + <key>INIntentParameterCombinationUpdatesLinked</key> + <true/> + </dict> + </dict> + <key>INIntentName</key> + <string>StopVPN</string> + <key>INIntentResponse</key> + <dict> + <key>INIntentResponseCodes</key> + <array> + <dict> + <key>INIntentResponseCodeName</key> + <string>success</string> + <key>INIntentResponseCodeSuccess</key> + <true/> + </dict> + <dict> + <key>INIntentResponseCodeName</key> + <string>failure</string> + </dict> + </array> + </dict> + <key>INIntentTitle</key> + <string>Stop VPN</string> + <key>INIntentTitleID</key> + <string>4GnhAo</string> + <key>INIntentType</key> + <string>Custom</string> + <key>INIntentVerb</key> + <string>Do</string> + </dict> + <dict> + <key>INIntentCategory</key> + <string>generic</string> + <key>INIntentConfigurable</key> + <true/> + <key>INIntentDescriptionID</key> + <string>SPC7AD</string> + <key>INIntentIneligibleForSuggestions</key> + <true/> + <key>INIntentManagedParameterCombinations</key> + <dict> + <key></key> + <dict> + <key>INIntentParameterCombinationSupportsBackgroundExecution</key> + <true/> + <key>INIntentParameterCombinationUpdatesLinked</key> + <true/> + </dict> + </dict> + <key>INIntentName</key> + <string>ReconnectVPN</string> + <key>INIntentResponse</key> + <dict> + <key>INIntentResponseCodes</key> + <array> + <dict> + <key>INIntentResponseCodeName</key> + <string>success</string> + <key>INIntentResponseCodeSuccess</key> + <true/> + </dict> + <dict> + <key>INIntentResponseCodeName</key> + <string>failure</string> + </dict> + </array> + </dict> + <key>INIntentTitle</key> + <string>Reconnect VPN</string> + <key>INIntentTitleID</key> + <string>puJvsb</string> + <key>INIntentType</key> + <string>Custom</string> + <key>INIntentVerb</key> + <string>Do</string> + </dict> + </array> + <key>INTypes</key> + <array/> +</dict> +</plist> diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 03f095c9af..f42a60b84f 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -262,24 +262,28 @@ final class TunnelManager { _refreshTunnelStatus() } - func startTunnel() { + func startTunnel(completionHandler: ((OperationCompletion<(), Error>) -> Void)? = nil) { let operation = StartTunnelOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self), completionHandler: { [weak self] completion in - guard let self = self, let error = completion.error else { return } - - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to start the tunnel." - ) + guard let self = self else { return } DispatchQueue.main.async { - let tunnelError = StartTunnelError(underlyingError: error) + if let error = completion.error { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to start the tunnel." + ) - self.observerList.forEach { observer in - observer.tunnelManager(self, didFailWithError: tunnelError) + let tunnelError = StartTunnelError(underlyingError: error) + + self.observerList.forEach { observer in + observer.tunnelManager(self, didFailWithError: tunnelError) + } } + + completionHandler?(completion) } }) @@ -289,24 +293,28 @@ final class TunnelManager { operationQueue.addOperation(operation) } - func stopTunnel() { + func stopTunnel(completionHandler: ((OperationCompletion<(), Error>) -> Void)? = nil) { let operation = StopTunnelOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self) ) { [weak self] completion in - guard let self = self, let error = completion.error else { return } - - self.logger.error( - chainedError: AnyChainedError(error), - message: "Failed to stop the tunnel." - ) + guard let self = self else { return } DispatchQueue.main.async { - let tunnelError = StopTunnelError(underlyingError: error) + if let error = completion.error { + self.logger.error( + chainedError: AnyChainedError(error), + message: "Failed to stop the tunnel." + ) - self.observerList.forEach { observer in - observer.tunnelManager(self, didFailWithError: tunnelError) + let tunnelError = StopTunnelError(underlyingError: error) + + self.observerList.forEach { observer in + observer.tunnelManager(self, didFailWithError: tunnelError) + } } + + completionHandler?(completion) } } |
