diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2024-01-30 16:29:55 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-01-30 16:29:55 +0100 |
| commit | 21d1883687ed32f5abe60de062eb83ae31f97163 (patch) | |
| tree | bc4a4b4a94e86e4c4a9cc4453881fe0f19c41ab2 | |
| parent | 2141af9ad1f8f71cc4abaad1c0942b2a6d1415d1 (diff) | |
| parent | 851803b3f7f868faa11600663cf8c16c5e0b34b0 (diff) | |
| download | mullvadvpn-android/fix-devmole-google-play.tar.xz mullvadvpn-android/fix-devmole-google-play.zip | |
Merge branch 'currently-in-use-api-access-method-should-be-visible-in-ui-ios-470'android/fix-devmole-google-play
22 files changed, 315 insertions, 153 deletions
diff --git a/ios/MullvadREST/Transport/AccessMethodIterator.swift b/ios/MullvadREST/Transport/AccessMethodIterator.swift index 34ec668c64..531372efce 100644 --- a/ios/MullvadREST/Transport/AccessMethodIterator.swift +++ b/ios/MullvadREST/Transport/AccessMethodIterator.swift @@ -11,7 +11,6 @@ import Foundation import MullvadSettings class AccessMethodIterator { - private var lastReachableApiAccessCache: LastReachableApiAccessCache private let dataSource: AccessMethodRepositoryDataSource private var index = 0 @@ -21,19 +20,15 @@ class AccessMethodIterator { dataSource.fetchAll().filter { $0.isEnabled } } - private var lastReachableApiAccessId: UUID { - lastReachableApiAccessCache.id + private var lastReachableApiAccessId: UUID? { + dataSource.fetchLastReachable().id } - init(_ userDefaults: UserDefaults, dataSource: AccessMethodRepositoryDataSource) { + init(dataSource: AccessMethodRepositoryDataSource) { self.dataSource = dataSource - self.lastReachableApiAccessCache = LastReachableApiAccessCache( - defaultValue: dataSource.directAccess.id, - container: userDefaults - ) self.dataSource - .publisher + .accessMethodsPublisher .sink { [weak self] _ in guard let self else { return } self.refreshCacheIfNeeded() @@ -48,14 +43,14 @@ class AccessMethodIterator { } else { /// When `firstIndex` is `nil`, that means the current configuration is not valid anymore /// Invalidating cache by replacing the `current` to the next enabled access method - lastReachableApiAccessCache.id = pick().id + dataSource.saveLastReachable(pick()) } } func rotate() { let (partial, isOverflow) = index.addingReportingOverflow(1) index = isOverflow ? 0 : partial - lastReachableApiAccessCache.id = pick().id + dataSource.saveLastReachable(pick()) } func pick() -> PersistentAccessMethod { diff --git a/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift b/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift deleted file mode 100644 index 100a591730..0000000000 --- a/ios/MullvadREST/Transport/LastReachableApiAccessCache.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// LastReachableApiAccessStorage.swift -// MullvadREST -// -// Created by Mojgan on 2024-01-08. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadSettings -struct LastReachableApiAccessCache: Identifiable { - /// `UserDefaults` key shared by both processes. Used to cache and synchronize last reachable api access method between them. - private let key = "LastReachableConfigurationCacheKey" - private var container: UserDefaults - private let defaultValue: UUID - - init(defaultValue: UUID, container: UserDefaults) { - self.container = container - self.defaultValue = defaultValue - } - - var id: UUID { - get { - guard let value = container.string(forKey: key) else { - return defaultValue - } - return UUID(uuidString: value)! - } - set { - container.set(newValue.uuidString, forKey: key) - } - } -} diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift index a41139a41d..72920ddcd5 100644 --- a/ios/MullvadREST/Transport/TransportStrategy.swift +++ b/ios/MullvadREST/Transport/TransportStrategy.swift @@ -11,7 +11,7 @@ import Logging import MullvadSettings import MullvadTypes -public class TransportStrategy: Equatable { +public struct TransportStrategy: Equatable { /// The different transports suggested by the strategy public enum Transport: Equatable { /// Connecting a direct connection @@ -45,15 +45,11 @@ public class TransportStrategy: Equatable { private let accessMethodIterator: AccessMethodIterator public init( - _ userDefaults: UserDefaults, datasource: AccessMethodRepositoryDataSource, shadowsocksLoader: ShadowsocksLoaderProtocol ) { self.shadowsocksLoader = shadowsocksLoader - self.accessMethodIterator = AccessMethodIterator( - userDefaults, - dataSource: datasource - ) + self.accessMethodIterator = AccessMethodIterator(dataSource: datasource) } /// Rotating between enabled configurations by what order they were added in diff --git a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift index bd8b341654..a2640df952 100644 --- a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift +++ b/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift @@ -9,10 +9,10 @@ import Combine import MullvadSettings -typealias PersistentAccessMethod = MullvadSettings.PersistentAccessMethod struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { - var directAccess: MullvadSettings.PersistentAccessMethod - var publisher: AnyPublisher<[MullvadSettings.PersistentAccessMethod], Never> { + var directAccess: PersistentAccessMethod + + var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { passthroughSubject.eraseToAnyPublisher() } @@ -23,7 +23,13 @@ struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { passthroughSubject.send(accessMethods) } - func fetchAll() -> [MullvadSettings.PersistentAccessMethod] { + func fetchAll() -> [PersistentAccessMethod] { passthroughSubject.value } + + func saveLastReachable(_ method: PersistentAccessMethod) {} + + func fetchLastReachable() -> PersistentAccessMethod { + directAccess + } } diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift index 721cd5b904..3174686ff6 100644 --- a/ios/MullvadRESTTests/TransportStrategyTests.swift +++ b/ios/MullvadRESTTests/TransportStrategyTests.swift @@ -12,22 +12,13 @@ import XCTest class TransportStrategyTests: XCTestCase { - var userDefaults: UserDefaults! - static var suiteName: String! - private var directAccess: PersistentAccessMethod! private var bridgeAccess: PersistentAccessMethod! private var shadowsocksLoader: ShadowsocksLoaderStub! - override class func setUp() { - super.setUp() - suiteName = UUID().uuidString - } - override func setUpWithError() throws { try super.setUpWithError() - userDefaults = UserDefaults(suiteName: Self.suiteName) shadowsocksLoader = ShadowsocksLoaderStub(configuration: ShadowsocksConfiguration( address: .ipv4(.loopback), @@ -51,16 +42,10 @@ class TransportStrategyTests: XCTestCase { ) } - override func tearDownWithError() throws { - userDefaults.removePersistentDomain(forName: Self.suiteName) - try super.tearDownWithError() - } - func testDefaultStrategyIsDirectWhenAllMethodsAreDisabled() throws { directAccess.isEnabled = false bridgeAccess.isEnabled = false let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -76,7 +61,6 @@ class TransportStrategyTests: XCTestCase { func testReuseSameStrategyWhenEverythingElseIsDisabled() throws { directAccess.isEnabled = false let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -96,7 +80,6 @@ class TransportStrategyTests: XCTestCase { func testLoopsFromTheStartAfterTryingAllEnabledStrategies() { let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -130,7 +113,6 @@ class TransportStrategyTests: XCTestCase { func testUsesNextWhenItIsNotReachable() { bridgeAccess.isEnabled = false let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -164,7 +146,6 @@ class TransportStrategyTests: XCTestCase { func testGoToNextStrategyWhenItFailsToLoadBridgeConfiguration() { shadowsocksLoader.error = IOError.fileNotFound let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -180,7 +161,6 @@ class TransportStrategyTests: XCTestCase { shadowsocksLoader.error = IOError.fileNotFound directAccess.isEnabled = false let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, @@ -207,7 +187,6 @@ class TransportStrategyTests: XCTestCase { authentication: authentication ) let transportStrategy = TransportStrategy( - userDefaults, datasource: AccessMethodRepositoryStub(accessMethods: [ directAccess, bridgeAccess, diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift index 0b03f03817..2df3ce7c89 100644 --- a/ios/MullvadSettings/AccessMethodRepository.swift +++ b/ios/MullvadSettings/AccessMethodRepository.swift @@ -27,10 +27,14 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol { proxyConfiguration: .bridges ) - let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([]) + private let accessMethodsSubject: CurrentValueSubject<[PersistentAccessMethod], Never> + public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { + accessMethodsSubject.eraseToAnyPublisher() + } - public var publisher: AnyPublisher<[PersistentAccessMethod], Never> { - passthroughSubject.eraseToAnyPublisher() + private let lastReachableAccessMethodSubject: CurrentValueSubject<PersistentAccessMethod, Never> + public var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> { + lastReachableAccessMethodSubject.eraseToAnyPublisher() } public var directAccess: PersistentAccessMethod { @@ -38,37 +42,57 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol { } public init() { + accessMethodsSubject = CurrentValueSubject([]) + lastReachableAccessMethodSubject = CurrentValueSubject(direct) + add([direct, bridge]) + + accessMethodsSubject.send(fetchAll()) + lastReachableAccessMethodSubject.send(fetchLastReachable()) } public func save(_ method: PersistentAccessMethod) { - var storedMethods = fetchAll() + var methodStore = readApiAccessMethodStore() - if let index = storedMethods.firstIndex(where: { $0.id == method.id }) { - storedMethods[index] = method + if let index = methodStore.accessMethods.firstIndex(where: { $0.id == method.id }) { + methodStore.accessMethods[index] = method } else { - storedMethods.append(method) + methodStore.accessMethods.append(method) } do { - try writeApiAccessMethods(storedMethods) + try writeApiAccessMethodStore(methodStore) + accessMethodsSubject.send(methodStore.accessMethods) } catch { - logger.error("Could not update access methods: \(storedMethods) \nError: \(error)") + logger.error("Could not save access method: \(method) \nError: \(error)") + } + } + + public func saveLastReachable(_ method: PersistentAccessMethod) { + var methodStore = readApiAccessMethodStore() + methodStore.lastReachableAccessMethod = method + + do { + try writeApiAccessMethodStore(methodStore) + lastReachableAccessMethodSubject.send(method) + } catch { + logger.error("Could not save last reachable access method: \(method) \nError: \(error)") } } public func delete(id: UUID) { - var methods = fetchAll() - guard let index = methods.firstIndex(where: { $0.id == id }) else { return } + var methodStore = readApiAccessMethodStore() + guard let index = methodStore.accessMethods.firstIndex(where: { $0.id == id }) else { return } // Prevent removing methods that have static UUIDs and are always present. - let method = methods[index] + let method = methodStore.accessMethods[index] if !method.kind.isPermanent { - methods.remove(at: index) + methodStore.accessMethods.remove(at: index) } do { - try writeApiAccessMethods(methods) + try writeApiAccessMethodStore(methodStore) + accessMethodsSubject.send(methodStore.accessMethods) } catch { logger.error("Could not delete access method with id: \(id) \nError: \(error)") } @@ -79,7 +103,11 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol { } public func fetchAll() -> [PersistentAccessMethod] { - (try? readApiAccessMethods()) ?? [] + readApiAccessMethodStore().accessMethods + } + + public func fetchLastReachable() -> PersistentAccessMethod { + readApiAccessMethodStore().lastReachableAccessMethod } public func reloadWithDefaultsAfterDataRemoval() { @@ -87,35 +115,39 @@ public class AccessMethodRepository: AccessMethodRepositoryProtocol { } private func add(_ methods: [PersistentAccessMethod]) { - var storedMethods = fetchAll() + var methodStore = readApiAccessMethodStore() methods.forEach { method in - if !storedMethods.contains(where: { $0.id == method.id }) { - storedMethods.append(method) + if !methodStore.accessMethods.contains(where: { $0.id == method.id }) { + methodStore.accessMethods.append(method) } } do { - try writeApiAccessMethods(storedMethods) + try writeApiAccessMethodStore(methodStore) + accessMethodsSubject.send(methods) } catch { - logger.error("Could not update access methods: \(storedMethods) \nError: \(error)") + logger.error("Could not update access methods: \(methods) \nError: \(error)") } } - private func readApiAccessMethods() throws -> [PersistentAccessMethod] { + private func readApiAccessMethodStore() -> PersistentAccessMethodStore { let parser = makeParser() - let data = try SettingsManager.store.read(key: .apiAccessMethods) - return try parser.parseUnversionedPayload(as: [PersistentAccessMethod].self, from: data) + do { + let data = try SettingsManager.store.read(key: .apiAccessMethods) + return try parser.parseUnversionedPayload(as: PersistentAccessMethodStore.self, from: data) + } catch { + logger.error("Could not load access method store: \(error)") + return PersistentAccessMethodStore(lastReachableAccessMethod: direct, accessMethods: []) + } } - private func writeApiAccessMethods(_ accessMethods: [PersistentAccessMethod]) throws { + private func writeApiAccessMethodStore(_ store: PersistentAccessMethodStore) throws { let parser = makeParser() - let data = try parser.produceUnversionedPayload(accessMethods) + let data = try parser.produceUnversionedPayload(store) try SettingsManager.store.write(data, for: .apiAccessMethods) - - passthroughSubject.send(accessMethods) } private func makeParser() -> SettingsParser { diff --git a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift index 037ea24cda..02e0fa71f9 100644 --- a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift +++ b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift @@ -9,8 +9,8 @@ import Combine public protocol AccessMethodRepositoryDataSource { - /// Publisher that propagates a snapshot of persistent store upon modifications. - var publisher: AnyPublisher<[PersistentAccessMethod], Never> { get } + /// Publisher that propagates a snapshot of all access methods upon modifications. + var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { get } /// - Returns: the default strategy. var directAccess: PersistentAccessMethod { get } @@ -18,9 +18,18 @@ public protocol AccessMethodRepositoryDataSource { /// Fetch all access method from the persistent store. /// - Returns: an array of all persistent access method. func fetchAll() -> [PersistentAccessMethod] + + /// Save last reachable access method to the persistent store. + func saveLastReachable(_ method: PersistentAccessMethod) + + /// Fetch last reachable access method from the persistent store. + func fetchLastReachable() -> PersistentAccessMethod } public protocol AccessMethodRepositoryProtocol: AccessMethodRepositoryDataSource { + /// Publisher that propagates a snapshot of last reachable access method upon modifications. + var lastReachableAccessMethodPublisher: AnyPublisher<PersistentAccessMethod, Never> { get } + /// Add new access method. /// - Parameter method: persistent access method model. func save(_ method: PersistentAccessMethod) diff --git a/ios/MullvadSettings/PersistentAccessMethod.swift b/ios/MullvadSettings/PersistentAccessMethod.swift index 6728552375..9d67ce849f 100644 --- a/ios/MullvadSettings/PersistentAccessMethod.swift +++ b/ios/MullvadSettings/PersistentAccessMethod.swift @@ -10,6 +10,15 @@ import Foundation import MullvadTypes import Network +/// Persistent access method container model. +public struct PersistentAccessMethodStore: Codable { + /// The last successfully reached access method. + public var lastReachableAccessMethod: PersistentAccessMethod + + /// Persistent access method models. + public var accessMethods: [PersistentAccessMethod] +} + /// Persistent access method model. public struct PersistentAccessMethod: Identifiable, Codable, Equatable { /// The unique identifier used for referencing the access method entry in a persistent store. diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index f65b281bb3..b84f9dba52 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -499,6 +499,8 @@ 7A5869C72B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869C62B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift */; }; 7A6000F62B60092F001CF0D9 /* AccessMethodViewModelEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */; }; 7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D7B2B03BDD100E7CDD7 /* AccessMethodViewModel.swift */; }; + 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */; }; + 7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */; }; 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */; }; 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */; }; 7A6F2FA72AFBB9AE006D0856 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; }; @@ -737,7 +739,6 @@ F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */; }; F0164EBC2B482E430020268D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBB2B482E430020268D /* AppStorage.swift */; }; F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; }; - F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */; }; F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; }; F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; }; F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; }; @@ -1660,6 +1661,8 @@ 7A5869C42B5A899C00640D27 /* MethodSettingsCellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsCellConfiguration.swift; sourceTree = "<group>"; }; 7A5869C62B5A8E4C00640D27 /* MethodSettingsDataSourceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsDataSourceConfiguration.swift; sourceTree = "<group>"; }; 7A6000F52B60092F001CF0D9 /* AccessMethodViewModelEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodViewModelEditing.swift; sourceTree = "<group>"; }; + 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentConfiguration.swift; sourceTree = "<group>"; }; + 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellContentView.swift; sourceTree = "<group>"; }; 7A6B4F582AB8412E00123853 /* TunnelMonitorTimings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTimings.swift; sourceTree = "<group>"; }; 7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryTests.swift; sourceTree = "<group>"; }; 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; }; @@ -1799,7 +1802,6 @@ F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = "<group>"; }; F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = "<group>"; }; F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = "<group>"; }; - F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastReachableApiAccessCache.swift; sourceTree = "<group>"; }; F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = "<group>"; }; F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodIterator.swift; sourceTree = "<group>"; }; F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewController.swift; sourceTree = "<group>"; }; @@ -2620,6 +2622,8 @@ 58FF9FE92B07653800E4C97D /* ButtonCellContentView.swift */, 58CEB3092AFD584700E6E088 /* CustomCellDisclosureHandling.swift */, 58CEB30B2AFD586600E6E088 /* DynamicBackgroundConfiguration.swift */, + 7A6000FB2B628DF6001CF0D9 /* ListCellContentConfiguration.swift */, + 7A6000FD2B628E9F001CF0D9 /* ListCellContentView.swift */, 58CEB3012AFD365600E6E088 /* SwitchCellContentConfiguration.swift */, 58CEB3032AFD36CE00E6E088 /* SwitchCellContentView.swift */, 58CEB2F42AFD0BB500E6E088 /* TextCellContentConfiguration.swift */, @@ -3422,7 +3426,6 @@ F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */, A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */, F0DC77A32B2315800087F09D /* Direct */, - F0164EC02B4C03980020268D /* LastReachableApiAccessCache.swift */, 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */, 58E7BA182A975DF70068EC3A /* RESTTransportProvider.swift */, F0DC77A22B2314EF0087F09D /* Shadowsocks */, @@ -4357,7 +4360,6 @@ 06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */, A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */, A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */, - F0164EC12B4C03980020268D /* LastReachableApiAccessCache.swift in Sources */, 06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */, 06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */, A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */, @@ -4772,6 +4774,7 @@ 7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */, 588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */, 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */, + 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, 7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */, 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, @@ -4831,6 +4834,7 @@ 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */, 7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */, 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */, + 7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index e7847a7623..7a1ddb1358 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -91,8 +91,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession()) let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) - // This init cannot fail as long as the security group identifier is valid - let sharedUserDefaults = UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)! shadowsocksLoader = ShadowsocksLoader( shadowsocksCache: shadowsocksCache, relayCache: relayCache, @@ -105,7 +103,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) let transportStrategy = TransportStrategy( - sharedUserDefaults, datasource: accessMethodRepository, shadowsocksLoader: shadowsocksLoader ) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift new file mode 100644 index 0000000000..891927c510 --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentConfiguration.swift @@ -0,0 +1,50 @@ +// +// ListCellContentConfiguration.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-01-25. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +/// Content configuration presenting a label and switch control. +struct ListCellContentConfiguration: UIContentConfiguration, Equatable { + struct TextProperties: Equatable { + var font = UIFont.systemFont(ofSize: 17) + var color = UIColor.Cell.titleTextColor + } + + struct SecondaryTextProperties: Equatable { + var font = UIFont.systemFont(ofSize: 17) + var color = UIColor.Cell.detailTextColor.withAlphaComponent(0.8) + } + + struct TertiaryTextProperties: Equatable { + var font = UIFont.systemFont(ofSize: 15) + var color = UIColor.Cell.titleTextColor.withAlphaComponent(0.6) + } + + /// Primary text label. + var text: String? + let textProperties = TextProperties() + + /// Secondary (trailing) text label. + var secondaryText: String? + let secondaryTextProperties = SecondaryTextProperties() + + /// Tertiary (below primary) text label. + var tertiaryText: String? + let tertiaryTextProperties = TertiaryTextProperties() + + /// Content view layout margins. + var directionalLayoutMargins: NSDirectionalEdgeInsets = UIMetrics.SettingsCell.apiAccessInsetLayoutMargins + + func makeContentView() -> UIView & UIContentView { + return ListCellContentView(configuration: self) + } + + func updated(for state: UIConfigurationState) -> Self { + return self + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift new file mode 100644 index 0000000000..2702d38a3e --- /dev/null +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/ListCellContentView.swift @@ -0,0 +1,103 @@ +// +// ListCellContentView.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-01-25. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +/// Content view presenting a primary, secondary (trailing) and tertiary (below primary) label. +class ListCellContentView: UIView, UIContentView, UITextFieldDelegate { + private var textLabel = UILabel() + private var secondaryTextLabel = UILabel() + private var tertiaryTextLabel = UILabel() + + var configuration: UIContentConfiguration { + get { + actualConfiguration + } + set { + guard let newConfiguration = newValue as? ListCellContentConfiguration, + actualConfiguration != newConfiguration else { return } + + let previousConfiguration = actualConfiguration + actualConfiguration = newConfiguration + + configureSubviews(previousConfiguration: previousConfiguration) + } + } + + private var actualConfiguration: ListCellContentConfiguration + + func supports(_ configuration: UIContentConfiguration) -> Bool { + configuration is ListCellContentConfiguration + } + + init(configuration: ListCellContentConfiguration) { + actualConfiguration = configuration + + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 0)) + + configureSubviews() + addSubviews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureSubviews(previousConfiguration: ListCellContentConfiguration? = nil) { + configureTextLabel() + configureSecondaryTextLabel() + configureTertiaryTextLabel() + configureLayoutMargins() + } + + private func configureTextLabel() { + let textProperties = actualConfiguration.textProperties + + textLabel.font = textProperties.font + textLabel.textColor = textProperties.color + + textLabel.text = actualConfiguration.text + } + + private func configureSecondaryTextLabel() { + let textProperties = actualConfiguration.secondaryTextProperties + + secondaryTextLabel.font = textProperties.font + secondaryTextLabel.textColor = textProperties.color + + secondaryTextLabel.text = actualConfiguration.secondaryText + } + + private func configureTertiaryTextLabel() { + let textProperties = actualConfiguration.tertiaryTextProperties + + tertiaryTextLabel.font = textProperties.font + tertiaryTextLabel.textColor = textProperties.color + + tertiaryTextLabel.text = actualConfiguration.tertiaryText + } + + private func configureLayoutMargins() { + directionalLayoutMargins = actualConfiguration.directionalLayoutMargins + } + + private func addSubviews() { + let leadingTextContainer = UIStackView(arrangedSubviews: [textLabel, tertiaryTextLabel]) + leadingTextContainer.axis = .vertical + + addConstrainedSubviews([leadingTextContainer, secondaryTextLabel]) { + leadingTextContainer.pinEdgesToSuperviewMargins(.all().excluding(.trailing)) + leadingTextContainer.centerYAnchor.constraint(equalTo: centerYAnchor) + secondaryTextLabel.pinEdgesToSuperviewMargins(.all().excluding(.leading)) + secondaryTextLabel.leadingAnchor.constraint( + greaterThanOrEqualToSystemSpacingAfter: leadingTextContainer.trailingAnchor, + multiplier: 1 + ) + } + } +} diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift index 59650aa15d..e62c56ed12 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/AccessMethodViewModelEditing.swift @@ -8,6 +8,6 @@ import MullvadSettings -protocol AccessMethodEditing { +protocol AccessMethodEditing: AnyObject { func accessMethodDidSave(_ accessMethod: PersistentAccessMethod) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift index b33e3c227c..420fd71eab 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift @@ -15,14 +15,6 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol { let repository: AccessMethodRepositoryProtocol let proxyConfigurationTester: ProxyConfigurationTesterProtocol - var directAccess: PersistentAccessMethod { - repository.directAccess - } - - var publisher: AnyPublisher<[PersistentAccessMethod], Never> { - repository.publisher.eraseToAnyPublisher() - } - func saveAccessMethod() { guard let persistentMethod = try? subject.value.intoPersistentAccessMethod() else { return } @@ -33,10 +25,6 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol { repository.delete(id: subject.value.id) } - func fetchAll() -> [MullvadSettings.PersistentAccessMethod] { - return repository.fetchAll() - } - func startProxyConfigurationTest(_ completion: ((Bool) -> Void)?) { guard let config = try? subject.value.intoPersistentProxyConfiguration() else { return } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift index 870224bd8e..a5a68db881 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractorProtocol.swift @@ -9,7 +9,7 @@ import MullvadSettings /// The type implementing the interface for persisting changes to the underlying access method view model in the editing context. -protocol EditAccessMethodInteractorProtocol: ProxyConfigurationInteractorProtocol, AccessMethodRepositoryDataSource { +protocol EditAccessMethodInteractorProtocol: ProxyConfigurationInteractorProtocol { /// Save changes to persistent store. /// /// - Calling this method when the underlying view model fails validation does nothing. diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift index aee945cc76..9c0f844786 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodViewControllerDelegate.swift @@ -8,7 +8,7 @@ import Foundation -protocol EditAccessMethodViewControllerDelegate: AnyObject { +protocol EditAccessMethodViewControllerDelegate: AnyObject, AccessMethodEditing { /// The view controller requests the delegate to present the proxy configuration view controller. /// - Parameter controller: the calling controller. func controllerShouldShowMethodSettings(_ controller: EditAccessMethodViewController) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift index 4cdc30afbb..d06a804d3b 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractor.swift @@ -11,24 +11,30 @@ import MullvadSettings /// A concrete implementation of an API access list interactor. struct ListAccessMethodInteractor: ListAccessMethodInteractorProtocol { - let reporepository: AccessMethodRepositoryProtocol + let repository: AccessMethodRepositoryProtocol init(repository: AccessMethodRepositoryProtocol) { - self.reporepository = repository + self.repository = repository } - var publisher: any Publisher<[ListAccessMethodItem], Never> { - reporepository.publisher.map { newElements in - newElements.map { $0.toListItem() } + var itemsPublisher: any Publisher<[ListAccessMethodItem], Never> { + repository.accessMethodsPublisher.map { methods in + methods.map { $0.toListItem() } + } + } + + var itemInUsePublisher: any Publisher<ListAccessMethodItem?, Never> { + repository.lastReachableAccessMethodPublisher.map { method in + method.toListItem() } } func item(by id: UUID) -> ListAccessMethodItem? { - reporepository.fetch(by: id)?.toListItem() + repository.fetch(by: id)?.toListItem() } func fetch() -> [ListAccessMethodItem] { - reporepository.fetchAll().map { $0.toListItem() } + repository.fetchAll().map { $0.toListItem() } } } @@ -47,7 +53,8 @@ extension PersistentAccessMethod { tableName: "APIAccess", value: "Disabled", comment: "" - ) + ), + isEnabled: isEnabled ) } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift index aca4e40968..91e766341b 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodInteractorProtocol.swift @@ -11,12 +11,15 @@ import MullvadSettings /// Types describing API access list interactor. protocol ListAccessMethodInteractorProtocol { + /// Publisher that produces a list of method items upon persistent store modifications. + var itemsPublisher: any Publisher<[ListAccessMethodItem], Never> { get } + + /// Publisher that produces the last reachable method item upon persistent store modifications. + var itemInUsePublisher: any Publisher<ListAccessMethodItem?, Never> { get } + /// Returns an item by id. func item(by id: UUID) -> ListAccessMethodItem? /// Fetch all items. func fetch() -> [ListAccessMethodItem] - - /// Publisher that produces a list of method items upon persisrtent store modifications. - var publisher: any Publisher<[ListAccessMethodItem], Never> { get } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift index 44367754ce..bcd536062e 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodItem.swift @@ -17,4 +17,7 @@ struct ListAccessMethodItem: Hashable, Identifiable, Equatable { /// The detailed information displayed alongside. let detail: String? + + /// Whether method is enabled or not. + let isEnabled: Bool } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift index 9e6e31f497..f4340e7877 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodViewController.swift @@ -7,6 +7,8 @@ // import Combine +import MullvadREST +import MullvadSettings import UIKit enum ListAccessMethodSectionIdentifier: Hashable { @@ -26,6 +28,7 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { private let headerView = ListAccessMethodHeaderView() private let interactor: ListAccessMethodInteractorProtocol + private var lastReachableMethodItem: ListAccessMethodItem? private var cancellables = Set<AnyCancellable>() private var dataSource: ListAccessMethodDataSource? @@ -73,8 +76,14 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { addChild(contentController) contentController.didMove(toParent: self) - interactor.publisher.sink { _ in - self.updateDataSource(animated: true) + interactor.itemsPublisher.sink { [weak self] _ in + self?.updateDataSource(animated: true) + } + .store(in: &cancellables) + + interactor.itemInUsePublisher.sink { [weak self] item in + self?.lastReachableMethodItem = item + self?.updateDataSource(animated: true) } .store(in: &cancellables) @@ -82,6 +91,16 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { configureDataSource() } + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let itemIdentifier = dataSource?.itemIdentifier(for: indexPath) else { return 0 } + + if itemIdentifier.id == lastReachableMethodItem?.id { + return UITableView.automaticDimension + } else { + return UIMetrics.SettingsCell.apiAccessCellHeight + } + } + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { let container = UIView() @@ -136,25 +155,18 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { } private func updateDataSource(animated: Bool = true) { - let oldFetchedItems = fetchedItems - let newFetchedItems = interactor.fetch() - fetchedItems = newFetchedItems + fetchedItems = interactor.fetch() var snapshot = NSDiffableDataSourceSnapshot<ListAccessMethodSectionIdentifier, ListAccessMethodItemIdentifier>() snapshot.appendSections([.primary]) - let itemIdentifiers = newFetchedItems.map { item in + let itemIdentifiers = fetchedItems.map { item in ListAccessMethodItemIdentifier(id: item.id) } snapshot.appendItems(itemIdentifiers, toSection: .primary) - for newFetchedItem in newFetchedItems { - for oldFetchedItem in oldFetchedItems { - if newFetchedItem.id == oldFetchedItem.id, - newFetchedItem.name != oldFetchedItem.name || newFetchedItem.detail != oldFetchedItem.detail { - snapshot.reloadItems([ListAccessMethodItemIdentifier(id: newFetchedItem.id)]) - } - } + for item in fetchedItems { + snapshot.reloadItems([ListAccessMethodItemIdentifier(id: item.id)]) } dataSource?.apply(snapshot, animatingDifferences: animated) @@ -167,9 +179,12 @@ class ListAccessMethodViewController: UIViewController, UITableViewDelegate { let cell = tableView.dequeueReusableView(withIdentifier: CellReuseIdentifier.default, for: indexPath) let item = fetchedItems[indexPath.row] - var contentConfiguration = UIListContentConfiguration.mullvadValueCell(tableStyle: .plain) + var contentConfiguration = ListCellContentConfiguration() contentConfiguration.text = item.name contentConfiguration.secondaryText = item.detail + contentConfiguration.tertiaryText = lastReachableMethodItem?.id == item.id + ? NSLocalizedString("LIST_ACCESS_METHODS_IN_USE_ITEM", tableName: "APIAccess", value: "In use", comment: "") + : "" cell.contentConfiguration = contentConfiguration if let cell = cell as? DynamicBackgroundConfiguration { diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift index 7e25a36f3d..3a1d938e05 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Models/AccessMethodViewModel+Persistent.swift @@ -28,7 +28,7 @@ extension AccessMethodViewModel { do { configuration = try intoPersistentProxyConfiguration() - } catch var error as AccessMethodValidationError { + } catch let error as AccessMethodValidationError { var fieldErrors = error.fieldErrors do { diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index fc0dcce43f..3cbf52c675 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -42,7 +42,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // This init cannot fail as long as the security group identifier is valid let transportStrategy = TransportStrategy( - UserDefaults(suiteName: ApplicationConfiguration.securityGroupIdentifier)!, datasource: AccessMethodRepository(), shadowsocksLoader: ShadowsocksLoader( shadowsocksCache: shadowsocksCache, |
