diff --git a/Passepartout-iOS/Scenes/ProviderPoolViewController.swift b/Passepartout-iOS/Scenes/ProviderPoolViewController.swift index 3151ea6a..d88ffe9c 100644 --- a/Passepartout-iOS/Scenes/ProviderPoolViewController.swift +++ b/Passepartout-iOS/Scenes/ProviderPoolViewController.swift @@ -26,36 +26,6 @@ import UIKit import Passepartout_Core -private class PoolModel { - let title: String - - var poolsByGroup: [PoolGroup: [Pool]] = [:] - - private(set) var sortedGroups: [PoolGroup] = [] - - var isEmpty: Bool { - return sortedGroups.isEmpty - } - - init(title: String) { - self.title = title - } - - func addPool(_ p: Pool) { - let group = p.group() - if var existingPools = poolsByGroup[group] { - existingPools.append(p) - poolsByGroup[group] = existingPools - } else { - poolsByGroup[group] = [p] - } - } - - func sort() { - sortedGroups = poolsByGroup.keys.sorted() - } -} - protocol ProviderPoolViewControllerDelegate: class { func providerPoolController(_: ProviderPoolViewController, didSelectPool pool: Pool) } @@ -69,28 +39,19 @@ class ProviderPoolViewController: UIViewController { weak var delegate: ProviderPoolViewControllerDelegate? - func setPools(_ pools: [Pool], currentPoolId: String?) { - let freeModel = PoolModel(title: L10n.Provider.Pool.Sections.Free.header) - let paidModel = PoolModel(title: L10n.Provider.Pool.Sections.Paid.header) - for p in pools { - if p.isFree ?? false { - freeModel.addPool(p) - } else { - paidModel.addPool(p) + func setModels(_ models: [PoolModel], currentPoolId: String?) { + self.models = models + + // XXX: uglyyy + for m in models { + for pools in m.poolsByGroup.values { + for p in pools { + if p.id == currentPoolId { + currentPool = p + return + } + } } - if p.id == currentPoolId { - currentPool = p - } - } - freeModel.sort() - paidModel.sort() - - models = [] - if !freeModel.isEmpty { - models.append(freeModel) - } - if !paidModel.isEmpty { - models.append(paidModel) } } @@ -140,7 +101,7 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate return nil } let model = models[section] - return model.title + return model.isFree ? L10n.Provider.Pool.Sections.Free.header : L10n.Provider.Pool.Sections.Paid.header } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -173,7 +134,7 @@ extension ProviderPoolViewController: UITableViewDataSource, UITableViewDelegate let model = models[indexPath.section] let group = model.sortedGroups[indexPath.row] let groupPools = model.poolsByGroup[group]! - guard let pool = groupPools.first else { + guard let pool = groupPools.randomElement() else { fatalError("Empty pools in group \(group)") } currentPool = pool diff --git a/Passepartout-iOS/Scenes/ServiceViewController.swift b/Passepartout-iOS/Scenes/ServiceViewController.swift index 3d399db3..100a2e6c 100644 --- a/Passepartout-iOS/Scenes/ServiceViewController.swift +++ b/Passepartout-iOS/Scenes/ServiceViewController.swift @@ -152,7 +152,7 @@ class ServiceViewController: UIViewController, TableModelHost { case .providerPoolSegueIdentifier: let vc = destination as? ProviderPoolViewController - vc?.setPools(uncheckedProviderProfile.pools(), currentPoolId: uncheckedProviderProfile.poolId) + vc?.setModels(InfrastructureCache.shared.poolModels(for: uncheckedProviderProfile), currentPoolId: uncheckedProviderProfile.poolId) vc?.delegate = self case .endpointSegueIdentifier: @@ -289,14 +289,19 @@ class ServiceViewController: UIViewController, TableModelHost { } private func refreshProviderInfrastructure() { + let name = uncheckedProviderProfile.name + let hud = HUD() - let isUpdating = InfrastructureFactory.shared.update(uncheckedProviderProfile.name, notBeforeInterval: AppConstants.Web.minimumUpdateInterval) { (response, error) in + let isUpdating = InfrastructureFactory.shared.update(name, notBeforeInterval: AppConstants.Web.minimumUpdateInterval) { (response, error) in hud.hide() guard let response = response else { return } self.lastInfrastructureUpdate = response.1 self.tableView.reloadData() + + // invalidate current pool cache + InfrastructureCache.shared.removePoolModels(for: name) } if !isUpdating { hud.hide() diff --git a/Passepartout-iOS/Scenes/Shortcuts/ShortcutsConnectToViewController.swift b/Passepartout-iOS/Scenes/Shortcuts/ShortcutsConnectToViewController.swift index 7d42b204..2acf5d9d 100644 --- a/Passepartout-iOS/Scenes/Shortcuts/ShortcutsConnectToViewController.swift +++ b/Passepartout-iOS/Scenes/Shortcuts/ShortcutsConnectToViewController.swift @@ -89,7 +89,7 @@ class ShortcutsConnectToViewController: UITableViewController, ProviderPoolViewC guard let provider = selectedProfile as? ProviderConnectionProfile else { return } - vc.setPools(provider.pools(), currentPoolId: nil) + vc.setModels(InfrastructureCache.shared.poolModels(for: provider), currentPoolId: nil) vc.delegate = self } diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index ce67e0a9..f4203a0a 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; }; 0E58BF65224152F9006FB157 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E58BD9122404EF1006FB157 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 0E58BF68224305A8006FB157 /* Countries.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E58BF6A224305A8006FB157 /* Countries.strings */; }; + 0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolModel.swift */; }; + 0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */; }; 0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; }; 0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; }; 0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; }; @@ -217,6 +219,8 @@ 0E5E5DDE215119AF00E318A3 /* VPNStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = ""; }; 0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = ""; }; 0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = ""; }; + 0E66A26F225FE25800F9C779 /* PoolModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolModel.swift; sourceTree = ""; }; + 0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfrastructureCache.swift; sourceTree = ""; }; 0E6BE13920CFB76800A6DD36 /* ApplicationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationError.swift; sourceTree = ""; }; 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogViewController.swift; sourceTree = ""; }; 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = ""; }; @@ -476,7 +480,9 @@ isa = PBXGroup; children = ( 0EBE3AA3213DC1B000BFA2F5 /* HostConnectionProfile.swift */, + 0E66A271225FE5FB00F9C779 /* InfrastructureCache.swift */, 0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */, + 0E66A26F225FE25800F9C779 /* PoolModel.swift */, 0E79D14021919F5600BB5FB2 /* ProfileKey.swift */, 0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */, ); @@ -1005,6 +1011,7 @@ files = ( 0E3152BD223FA03D00F61841 /* GroupConstants.swift in Sources */, 0ECEB10A224FECEA00E9E551 /* DataUnit.swift in Sources */, + 0E66A270225FE25800F9C779 /* PoolModel.swift in Sources */, 0E3152C2223FA04800F61841 /* MockVPNProvider.swift in Sources */, 0E533B162258E03B00EF94FC /* PoolGroup.swift in Sources */, 0E3152D2223FA05400F61841 /* DebugLog.swift in Sources */, @@ -1035,6 +1042,7 @@ 0E3152CD223FA05400F61841 /* ConnectionProfile.swift in Sources */, 0E3152BC223FA03D00F61841 /* ApplicationError.swift in Sources */, 0E3152C9223FA04D00F61841 /* InfrastructureFactory.swift in Sources */, + 0E66A272225FE5FB00F9C779 /* InfrastructureCache.swift in Sources */, 0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */, 0E3152D3223FA05400F61841 /* EndpointDataSource.swift in Sources */, 0E3152D4223FA05400F61841 /* Preferences.swift in Sources */, diff --git a/Passepartout/Sources/Model/Profiles/InfrastructureCache.swift b/Passepartout/Sources/Model/Profiles/InfrastructureCache.swift new file mode 100644 index 00000000..add72036 --- /dev/null +++ b/Passepartout/Sources/Model/Profiles/InfrastructureCache.swift @@ -0,0 +1,76 @@ +// +// InfrastructureCache.swift +// Passepartout +// +// Created by Davide De Rosa on 4/11/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation + +// TODO: retain max N pool models at a time (LRU) + +public class InfrastructureCache { + public static let shared = InfrastructureCache() + + private var poolModelsByName: [Infrastructure.Name: [PoolModel]] + + private init() { + poolModelsByName = [:] + } + + public func poolModels(for provider: ProviderConnectionProfile) -> [PoolModel] { + if let models = poolModelsByName[provider.name] { + return models + } + let freeModel = PoolModel(isFree: true) + let paidModel = PoolModel(isFree: false) + for p in provider.infrastructure.pools { + if p.isFree ?? false { + freeModel.addPool(p) + } else { + paidModel.addPool(p) + } +// if p.id == currentPoolId { +// currentPool = p +// } + } + freeModel.sort() + paidModel.sort() + + var models: [PoolModel] = [] + if !freeModel.isEmpty { + models.append(freeModel) + } + if !paidModel.isEmpty { + models.append(paidModel) + } + poolModelsByName[provider.name] = models + return models + } + + public func removePoolModels(for name: Infrastructure.Name? = nil) { + if let name = name { + poolModelsByName.removeValue(forKey: name) + return + } + poolModelsByName.removeAll() + } +} diff --git a/Passepartout/Sources/Model/Profiles/PoolModel.swift b/Passepartout/Sources/Model/Profiles/PoolModel.swift new file mode 100644 index 00000000..077fa7e9 --- /dev/null +++ b/Passepartout/Sources/Model/Profiles/PoolModel.swift @@ -0,0 +1,58 @@ +// +// PoolModel.swift +// Passepartout +// +// Created by Davide De Rosa on 4/11/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation + +public class PoolModel { + public let isFree: Bool + + public var poolsByGroup: [PoolGroup: [Pool]] + + public private(set) var sortedGroups: [PoolGroup] + + public init(isFree: Bool) { + self.isFree = isFree + poolsByGroup = [:] + sortedGroups = [] + } + + public var isEmpty: Bool { + return sortedGroups.isEmpty + } + + public func addPool(_ p: Pool) { + let group = p.group() + if var existingPools = poolsByGroup[group] { + existingPools.append(p) + poolsByGroup[group] = existingPools + } else { + poolsByGroup[group] = [p] + } + } + + public func sort() { + sortedGroups = poolsByGroup.keys.sorted() + } +} diff --git a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift index 1d08b99c..b2b8667f 100644 --- a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift @@ -72,10 +72,6 @@ public class ProviderConnectionProfile: ConnectionProfile, Codable, Equatable { presetId = infrastructure.defaults.preset } - public func pools() -> [Pool] { - return infrastructure.pools - } - private func validateEndpoint() { guard let pool = pool, let preset = preset else { manualAddress = nil diff --git a/Passepartout/Sources/Services/InfrastructureFactory.swift b/Passepartout/Sources/Services/InfrastructureFactory.swift index b4dc83b4..57c1b61c 100644 --- a/Passepartout/Sources/Services/InfrastructureFactory.swift +++ b/Passepartout/Sources/Services/InfrastructureFactory.swift @@ -28,6 +28,8 @@ import SwiftyBeaver private let log = SwiftyBeaver.self +// TODO: retain max N infrastructures at a time (LRU) + public class InfrastructureFactory { private static func embedded(withName name: Infrastructure.Name) -> Infrastructure { guard let url = name.bundleURL else {