diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index 87e5da033..dd00d0aa3 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -43,7 +43,7 @@ public enum ExtensionMessage: RawRepresentable { case simulateTunnelFatalError case simulateTunnelMemoryOveruse case simulateConnectionInterruption - case getConnectionThroughput + case getDataVolume } // This is actually an improved way to send messages. @@ -68,7 +68,7 @@ public enum ExtensionMessage: RawRepresentable { case simulateTunnelFatalError case simulateTunnelMemoryOveruse case simulateConnectionInterruption - case getConnectionThroughput + case getDataVolume // swiftlint:disable:next cyclomatic_complexity function_body_length public init?(rawValue data: Data) { @@ -136,8 +136,8 @@ public enum ExtensionMessage: RawRepresentable { case .simulateConnectionInterruption: self = .simulateConnectionInterruption - case .getConnectionThroughput: - self = .getConnectionThroughput + case .getDataVolume: + self = .getDataVolume case .none: assertionFailure("Invalid data") @@ -165,7 +165,7 @@ public enum ExtensionMessage: RawRepresentable { case .simulateTunnelFatalError: return .simulateTunnelFatalError case .simulateTunnelMemoryOveruse: return .simulateTunnelMemoryOveruse case .simulateConnectionInterruption: return .simulateConnectionInterruption - case .getConnectionThroughput: return .getConnectionThroughput + case .getDataVolume: return .getDataVolume } } @@ -215,7 +215,7 @@ public enum ExtensionMessage: RawRepresentable { .simulateTunnelFatalError, .simulateTunnelMemoryOveruse, .simulateConnectionInterruption, - .getConnectionThroughput: break + .getDataVolume: break } diff --git a/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift b/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift index 7641e25a2..5a299ae5b 100644 --- a/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift +++ b/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift @@ -87,12 +87,7 @@ extension NetworkProtectionServerInfo { extension NetworkProtectionServerInfo.ServerAttributes { public var serverLocation: String { - let stateOrCountry = isUSServerLocation ? state : country - return "\(city), \(stateOrCountry.localizedUppercase)" + let fullCountryName = Locale.current.localizedString(forRegionCode: country) + return "\(city), \(fullCountryName ?? country.capitalized)" } - - private var isUSServerLocation: Bool { - return country.localizedUppercase == "US" - } - } diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 6c5dad29a..6de46fc05 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -888,8 +888,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { simulateTunnelMemoryOveruse(completionHandler: completionHandler) case .simulateConnectionInterruption: simulateConnectionInterruption(completionHandler: completionHandler) - case .getConnectionThroughput: - getConnectionThroughput(completionHandler: completionHandler) + case .getDataVolume: + getDataVolume(completionHandler: completionHandler) } } @@ -1150,7 +1150,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - private func getConnectionThroughput(completionHandler: ((Data?) -> Void)? = nil) { + private func getDataVolume(completionHandler: ((Data?) -> Void)? = nil) { Task { @MainActor in guard let (received, sent) = try? await adapter.getBytesTransmitted() else { completionHandler?(nil) diff --git a/Sources/NetworkProtection/Settings/VPNLocationFormatting.swift b/Sources/NetworkProtection/Settings/VPNLocationFormatting.swift new file mode 100644 index 000000000..426a81e53 --- /dev/null +++ b/Sources/NetworkProtection/Settings/VPNLocationFormatting.swift @@ -0,0 +1,34 @@ +// +// VPNLocationFormatting.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +public protocol VPNLocationFormatting { + func emoji(for country: String?, + preferredLocation: VPNSettings.SelectedLocation) -> String? + + func string(from location: String?, + preferredLocation: VPNSettings.SelectedLocation) -> String + + @available(macOS 12, iOS 15, *) + func string(from location: String?, + preferredLocation: VPNSettings.SelectedLocation, + locationTextColor: Color, + preferredLocationTextColor: Color) -> AttributedString +} diff --git a/Sources/NetworkProtection/Status/DataVolume.swift b/Sources/NetworkProtection/Status/DataVolume.swift new file mode 100644 index 000000000..04ba14bf0 --- /dev/null +++ b/Sources/NetworkProtection/Status/DataVolume.swift @@ -0,0 +1,29 @@ +// +// DataVolume.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public struct DataVolume: Codable, Equatable { + public let bytesSent: Int64 + public let bytesReceived: Int64 + + public init(bytesSent: Int64 = 0, bytesReceived: Int64 = 0) { + self.bytesSent = bytesSent + self.bytesReceived = bytesReceived + } +} diff --git a/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserver.swift b/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserver.swift new file mode 100644 index 000000000..85e144557 --- /dev/null +++ b/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserver.swift @@ -0,0 +1,26 @@ +// +// DataVolumeObserver.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkExtension + +public protocol DataVolumeObserver { + var publisher: AnyPublisher { get } + var recentValue: DataVolume { get } +} diff --git a/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserverThroughSession.swift b/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserverThroughSession.swift new file mode 100644 index 000000000..717c45a09 --- /dev/null +++ b/Sources/NetworkProtection/Status/DataVolumeObserver/DataVolumeObserverThroughSession.swift @@ -0,0 +1,109 @@ +// +// DataVolumeObserverThroughSession.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkExtension +import NotificationCenter +import Common + +public class DataVolumeObserverThroughSession: DataVolumeObserver { + public lazy var publisher = subject.eraseToAnyPublisher() + public var recentValue: DataVolume { + subject.value + } + + private let subject = CurrentValueSubject(.init()) + + private let tunnelSessionProvider: TunnelSessionProvider + + // MARK: - Notifications + + private let platformNotificationCenter: NotificationCenter + private let platformDidWakeNotification: Notification.Name + private var cancellables = Set() + + // MARK: - Timer + + private static let interval: TimeInterval = .seconds(1) + + // MARK: - Logging + + private let log: OSLog + + // MARK: - Initialization + + public init(tunnelSessionProvider: TunnelSessionProvider, + platformNotificationCenter: NotificationCenter, + platformDidWakeNotification: Notification.Name, + log: OSLog = .networkProtection) { + + self.platformNotificationCenter = platformNotificationCenter + self.platformDidWakeNotification = platformDidWakeNotification + self.tunnelSessionProvider = tunnelSessionProvider + self.log = log + + start() + } + + public func start() { + updateDataVolume() + + Timer.publish(every: Self.interval, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.updateDataVolume() + }.store(in: &cancellables) + + platformNotificationCenter.publisher(for: platformDidWakeNotification).sink { [weak self] notification in + self?.handleDidWake(notification) + }.store(in: &cancellables) + } + + // MARK: - Handling Notifications + + private func handleDidWake(_ notification: Notification) { + updateDataVolume() + } + + // MARK: - Obtaining the data volume + + private func updateDataVolume(session: NETunnelProviderSession) async { + guard let data: ExtensionMessageString = try? await session.sendProviderMessage(.getDataVolume) else { + return + } + + let bytes = data.value.components(separatedBy: ",") + guard let receivedString = bytes.first, let sentString = bytes.last, + let received = Int64(receivedString), let sent = Int64(sentString) else { + return + } + + subject.send(DataVolume(bytesSent: sent, bytesReceived: received)) + } + + private func updateDataVolume() { + Task { + guard let session = await tunnelSessionProvider.activeSession() else { + return + } + + await updateDataVolume(session: session) + } + } +} diff --git a/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift b/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift index d440eb1e6..a83e09891 100644 --- a/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift +++ b/Sources/NetworkProtection/Status/NetworkProtectionStatusReporter.swift @@ -28,6 +28,7 @@ public protocol NetworkProtectionStatusReporter { var connectionErrorObserver: ConnectionErrorObserver { get } var connectivityIssuesObserver: ConnectivityIssueObserver { get } var controllerErrorMessageObserver: ControllerErrorMesssageObserver { get } + var dataVolumeObserver: DataVolumeObserver { get } func forceRefresh() } @@ -68,6 +69,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat public let connectionErrorObserver: ConnectionErrorObserver public let connectivityIssuesObserver: ConnectivityIssueObserver public let controllerErrorMessageObserver: ControllerErrorMesssageObserver + public let dataVolumeObserver: DataVolumeObserver // MARK: - Init & deinit @@ -76,6 +78,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat connectionErrorObserver: ConnectionErrorObserver, connectivityIssuesObserver: ConnectivityIssueObserver, controllerErrorMessageObserver: ControllerErrorMesssageObserver, + dataVolumeObserver: DataVolumeObserver, distributedNotificationCenter: DistributedNotificationCenter = .default()) { self.statusObserver = statusObserver @@ -83,6 +86,7 @@ public final class DefaultNetworkProtectionStatusReporter: NetworkProtectionStat self.connectionErrorObserver = connectionErrorObserver self.connectivityIssuesObserver = connectivityIssuesObserver self.controllerErrorMessageObserver = controllerErrorMessageObserver + self.dataVolumeObserver = dataVolumeObserver self.distributedNotificationCenter = distributedNotificationCenter start() diff --git a/Sources/NetworkProtectionTestUtils/Status/MockDataVolumeObserver.swift b/Sources/NetworkProtectionTestUtils/Status/MockDataVolumeObserver.swift new file mode 100644 index 000000000..efdc079ca --- /dev/null +++ b/Sources/NetworkProtectionTestUtils/Status/MockDataVolumeObserver.swift @@ -0,0 +1,30 @@ +// +// MockDataVolumeObserver.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import NetworkProtection + +public final class MockDataVolumeObserver: DataVolumeObserver { + public init() {} + public let subject = CurrentValueSubject(.init()) + lazy public var publisher = subject.eraseToAnyPublisher() + public var recentValue: DataVolume { + subject.value + } +} diff --git a/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift b/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift index b8487ccad..9df4f1460 100644 --- a/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift +++ b/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift @@ -34,6 +34,7 @@ public final class MockNetworkProtectionStatusReporter: NetworkProtectionStatusR public let connectionErrorObserver: ConnectionErrorObserver public let connectivityIssuesObserver: ConnectivityIssueObserver public let controllerErrorMessageObserver: ControllerErrorMesssageObserver + public let dataVolumeObserver: DataVolumeObserver // MARK: - Init & deinit @@ -42,12 +43,14 @@ public final class MockNetworkProtectionStatusReporter: NetworkProtectionStatusR connectionErrorObserver: ConnectionErrorObserver = MockConnectionErrorObserver(), connectivityIssuesObserver: ConnectivityIssueObserver = MockConnectivityIssueObserver(), controllerErrorMessageObserver: ControllerErrorMesssageObserver = MockControllerErrorMesssageObserver(), + dataVolumeObserver: DataVolumeObserver = MockDataVolumeObserver(), distributedNotificationCenter: DistributedNotificationCenter = .default()) { self.statusObserver = statusObserver self.serverInfoObserver = serverInfoObserver self.connectionErrorObserver = connectionErrorObserver self.connectivityIssuesObserver = connectivityIssuesObserver + self.dataVolumeObserver = dataVolumeObserver self.controllerErrorMessageObserver = controllerErrorMessageObserver } diff --git a/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift index b5057d608..99cb9a4cd 100644 --- a/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift +++ b/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift @@ -31,7 +31,7 @@ final class NetworkProtectionServerInfoTests: XCTestCase { port: 42, attributes: .init(city: "Amsterdam", country: "nl", state: "na")) - XCTAssertEqual(serverInfo.serverLocation, "Amsterdam, NL") + XCTAssertEqual(serverInfo.serverLocation, "Amsterdam, Netherlands") } func testWhenGettingServerLocation_AndAttributesExist_isUS_ThenServerLocationIsCityState() { @@ -43,7 +43,7 @@ final class NetworkProtectionServerInfoTests: XCTestCase { port: 42, attributes: .init(city: "New York", country: "us", state: "ny")) - XCTAssertEqual(serverInfo.serverLocation, "New York, NY") + XCTAssertEqual(serverInfo.serverLocation, "New York, United States") } }