diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 1be98b11a..124a3a77a 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -356,6 +356,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private let tunnelHealth: NetworkProtectionTunnelHealthStore private let controllerErrorStore: NetworkProtectionTunnelErrorStore private let knownFailureStore: NetworkProtectionKnownFailureStore + private let snoozeTimingStore: NetworkProtectionSnoozeTimingStore // MARK: - Cancellables @@ -374,6 +375,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { tunnelHealthStore: NetworkProtectionTunnelHealthStore, controllerErrorStore: NetworkProtectionTunnelErrorStore, knownFailureStore: NetworkProtectionKnownFailureStore = NetworkProtectionKnownFailureStore(), + snoozeTimingStore: NetworkProtectionSnoozeTimingStore, keychainType: KeychainType, tokenStore: NetworkProtectionTokenStore, debugEvents: EventMapping?, @@ -392,6 +394,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { self.tunnelHealth = tunnelHealthStore self.controllerErrorStore = controllerErrorStore self.knownFailureStore = knownFailureStore + self.snoozeTimingStore = snoozeTimingStore self.settings = settings self.defaults = defaults self.isSubscriptionEnabled = isSubscriptionEnabled @@ -559,6 +562,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { open func handleConnectionStatusChange(old: ConnectionStatus, new: ConnectionStatus) { os_log("⚫️ Connection Status Change: %{public}s -> %{public}s", log: .networkProtectionPixel, type: .debug, old.description, new.description) + // TODO: Handle snooze + switch (old, new) { case (_, .connecting), (_, .reasserting): providerEvents.fire(.reportConnectionAttempt(attempt: .connecting)) @@ -1601,10 +1606,9 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { await stopMonitors() self.connectionStatus = .snoozing + self.snoozeTimingStore.activeTiming = .init(startDate: Date(), duration: duration) self.adapter.snooze { error in if error == nil { - self.notificationsPresenter.showSnoozeBeganNotification() - let timer = DispatchSource.makeTimerSource(queue: self.timerQueue) timer.schedule(deadline: .now() + duration, repeating: .never) timer.setEventHandler { @@ -1616,13 +1620,22 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } timer.resume() + } else { + self.snoozeTimingStore.reset() } } } private func cancelSnooze() async { os_log("Ending snooze mode", log: .networkProtection) - self.notificationsPresenter.showSnoozeEndedNotification() + + guard snoozeTimingStore.activeTiming != nil else { + assertionFailure("Tried to cancel snooze when it was not active") + return + } + + notificationsPresenter.showSnoozeEndedNotification() + snoozeTimingStore.reset() try? await startTunnel(onDemand: false) } diff --git a/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughSession.swift b/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughSession.swift index 10a3f7663..70642ff39 100644 --- a/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughSession.swift +++ b/Sources/NetworkProtection/Status/ConnectionStatusObserver/ConnectionStatusObserverThroughSession.swift @@ -37,6 +37,7 @@ public class ConnectionStatusObserverThroughSession: ConnectionStatusObserver { // MARK: - Notifications private let notificationCenter: NotificationCenter + private let platformSnoozeTimingStore: NetworkProtectionSnoozeTimingStore private let platformNotificationCenter: NotificationCenter private let platformDidWakeNotification: Notification.Name private var cancellables = Set() @@ -49,11 +50,13 @@ public class ConnectionStatusObserverThroughSession: ConnectionStatusObserver { public init(tunnelSessionProvider: TunnelSessionProvider, notificationCenter: NotificationCenter = .default, + platformSnoozeTimingStore: NetworkProtectionSnoozeTimingStore, platformNotificationCenter: NotificationCenter, platformDidWakeNotification: Notification.Name, log: OSLog = .networkProtection) { self.notificationCenter = notificationCenter + self.platformSnoozeTimingStore = platformSnoozeTimingStore self.platformNotificationCenter = platformNotificationCenter self.platformDidWakeNotification = platformDidWakeNotification self.tunnelSessionProvider = tunnelSessionProvider @@ -74,8 +77,12 @@ public class ConnectionStatusObserverThroughSession: ConnectionStatusObserver { self?.handleStatusChangeNotification(notification) }.store(in: &cancellables) + notificationCenter.publisher(for: .snoozeDidChange).sink { [weak self] notification in + self?.handleStatusRefreshNotification(notification) + }.store(in: &cancellables) + platformNotificationCenter.publisher(for: platformDidWakeNotification).sink { [weak self] notification in - self?.handleDidWake(notification) + self?.handleStatusRefreshNotification(notification) }.store(in: &cancellables) } @@ -89,7 +96,7 @@ public class ConnectionStatusObserverThroughSession: ConnectionStatusObserver { // MARK: - Handling Notifications - private func handleDidWake(_ notification: Notification) { + private func handleStatusRefreshNotification(_ notification: Notification) { Task { guard let session = await tunnelSessionProvider.activeSession() else { return @@ -128,8 +135,12 @@ public class ConnectionStatusObserverThroughSession: ConnectionStatusObserver { switch internalStatus { case .connected: - let connectedDate = connectedDate(from: session) - status = .connected(connectedDate: connectedDate) + if platformSnoozeTimingStore.activeTiming != nil { + status = .snoozing + } else { + let connectedDate = connectedDate(from: session) + status = .connected(connectedDate: connectedDate) + } case .connecting: status = .connecting case .reasserting: diff --git a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift index ad56aef16..cc3ed551d 100644 --- a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift +++ b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenter.swift @@ -41,8 +41,6 @@ public protocol NetworkProtectionNotificationsPresenter { /// Present a "expired subscription" notification to the user. func showEntitlementNotification() - func showSnoozeBeganNotification() - func showSnoozeEndedNotification() } diff --git a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift index f28ca1baa..637d46168 100644 --- a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift +++ b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift @@ -66,10 +66,6 @@ final public class NetworkProtectionNotificationsPresenterTogglableDecorator: Ne } } - public func showSnoozeBeganNotification() { - wrappeePresenter.showSnoozeBeganNotification() - } - public func showSnoozeEndedNotification() { wrappeePresenter.showSnoozeEndedNotification() } diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionSnoozeTimingStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionSnoozeTimingStore.swift new file mode 100644 index 000000000..8c3a07fa4 --- /dev/null +++ b/Sources/NetworkProtection/Storage/NetworkProtectionSnoozeTimingStore.swift @@ -0,0 +1,70 @@ +// +// NetworkProtectionSnoozeTimingStore.swift +// +// Copyright © 2023 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 Common + +final public class NetworkProtectionSnoozeTimingStore { + + public struct SnoozeTiming: Codable, Equatable { + let startDate: Date + let duration: TimeInterval + + var endDate: Date { + return startDate.addingTimeInterval(duration) + } + } + + private static let activeSnoozeTimingKey = "com.duckduckgo.NetworkProtectionSnoozeTimingStore.activeTiming" + + private let userDefaults: UserDefaults + private let notificationCenter: NotificationCenter + + public init(userDefaults: UserDefaults = .standard, notificationCenter: NotificationCenter = .default) { + self.userDefaults = userDefaults + self.notificationCenter = notificationCenter + } + + public var activeTiming: SnoozeTiming? { + get { + guard let data = userDefaults.data(forKey: Self.activeSnoozeTimingKey) else { return nil } + return try? JSONDecoder().decode(SnoozeTiming.self, from: data) + } + + set { + if let newValue, let data = try? JSONEncoder().encode(newValue) { + userDefaults.set(data, forKey: Self.activeSnoozeTimingKey) + } else { + userDefaults.removeObject(forKey: Self.activeSnoozeTimingKey) + } + + notificationCenter.post(name: .snoozeDidChange, object: nil) + } + } + + public func reset() { + activeTiming = nil + } + +} + +extension NSNotification.Name { + + static let snoozeDidChange: NSNotification.Name = Notification.Name(rawValue: "com.duckduckgo.browserServicesKit.vpn.snoozeDidChange") + +}