summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj8
-rw-r--r--ios/MullvadVPN/AppDelegate.swift14
-rw-r--r--ios/MullvadVPN/Info.plist22
-rw-r--r--ios/MullvadVPN/IntentHandlers.swift68
-rw-r--r--ios/MullvadVPN/Intents.intentdefinition163
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift48
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)
}
}