From 85d82b5c964130da0b20747b5c9fb3acc13cbfc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 3 Dec 2024 14:18:37 +0100 Subject: [PATCH 01/27] Move willResignActive code into Inactive state --- DuckDuckGo/AppDelegate.swift | 4 --- .../AppLifecycle/AppStates/Inactive.swift | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 1f6cb0cf60..e5c0be1e10 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -706,10 +706,6 @@ import os.log func applicationWillResignActive(_ application: UIApplication) { appStateMachine.handle(.suspending(application)) - Task { @MainActor in - await refreshShortcuts() - await vpnWorkaround.removeRedditSessionWorkaround() - } } private func fireAppLaunchPixel() { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index 888ef34e09..f0124d41bd 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -18,11 +18,42 @@ // import UIKit +import Subscription struct Inactive: AppState { - init(application: UIApplication) { + let application: UIApplication + init(application: UIApplication, + accountManager: AccountManager, + vpnFeatureVisibility: DefaultNetworkProtectionVisibility, + vpnWorkaround: VPNRedditSessionWorkaround) { + self.application = application + Task { @MainActor in + await refreshVPNShortcuts() + await vpnWorkaround.removeRedditSessionWorkaround() + } + } + + // TODO: move elsewhere - it is used in launching too + @MainActor + func refreshVPNShortcuts() async { + guard vpnFeatureVisibility.shouldShowVPNShortcut() else { + application.shortcutItems = nil + return + } + + if case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { + application.shortcutItems = [ + UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings, + localizedTitle: UserText.netPOpenVPNQuickAction, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), + userInfo: nil) + ] + } else { + application.shortcutItems = nil + } } } From ed3a56cc4440aaa2c1529a3706eda53f0f366cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 3 Dec 2024 14:48:44 +0100 Subject: [PATCH 02/27] Move app shortcuts code into extension --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++ DuckDuckGo/AppDelegate.swift | 2 +- .../AppLifecycle/AppStates/Inactive.swift | 23 +--------- DuckDuckGo/AppShortcuts.swift | 42 +++++++++++++++++++ 4 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 DuckDuckGo/AppShortcuts.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 74091cfaf2..37d4c76772 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -994,6 +994,7 @@ CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; + CBAD0F0A2CFF418F006267B8 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; @@ -2838,6 +2839,7 @@ CBAD0F002CFE1D54006267B8 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateMachine.swift; sourceTree = ""; }; CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; + CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; @@ -6372,6 +6374,7 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */, CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, @@ -8073,6 +8076,7 @@ 314C92B827C3DD660042EC96 /* QuickLookPreviewView.swift in Sources */, 6F5345AF2C53F2DE00424A43 /* NewTabPageSettingsPersistentStorage.swift in Sources */, F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */, + CBAD0F0A2CFF418F006267B8 /* AppShortcuts.swift in Sources */, 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */, 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index e5c0be1e10..35495b0ce3 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -42,7 +42,7 @@ import os.log @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) - private struct ShortcutKey { + struct ShortcutKey { static let clipboard = "com.duckduckgo.mobile.ios.clipboard" static let passwords = "com.duckduckgo.mobile.ios.passwords" static let openVPNSettings = "com.duckduckgo.mobile.ios.vpn.open-settings" diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index f0124d41bd..98b47dfafa 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -30,30 +30,9 @@ struct Inactive: AppState { vpnWorkaround: VPNRedditSessionWorkaround) { self.application = application Task { @MainActor in - await refreshVPNShortcuts() + await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, accountManager: accountManager) await vpnWorkaround.removeRedditSessionWorkaround() } } - // TODO: move elsewhere - it is used in launching too - @MainActor - func refreshVPNShortcuts() async { - guard vpnFeatureVisibility.shouldShowVPNShortcut() else { - application.shortcutItems = nil - return - } - - if case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { - application.shortcutItems = [ - UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings, - localizedTitle: UserText.netPOpenVPNQuickAction, - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), - userInfo: nil) - ] - } else { - application.shortcutItems = nil - } - } - } diff --git a/DuckDuckGo/AppShortcuts.swift b/DuckDuckGo/AppShortcuts.swift new file mode 100644 index 0000000000..b4f55d9166 --- /dev/null +++ b/DuckDuckGo/AppShortcuts.swift @@ -0,0 +1,42 @@ +// +// AppShortcuts.swift +// DuckDuckGo +// +// 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 Subscription + +extension UIApplication { + + func refreshVPNShortcuts(vpnFeatureVisibility: DefaultNetworkProtectionVisibility, accountManager: AccountManager) async { + guard vpnFeatureVisibility.shouldShowVPNShortcut(), + case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, + cachePolicy: .returnCacheDataDontLoad) + else { + shortcutItems = nil + return + } + + shortcutItems = [ + UIApplicationShortcutItem(type: AppDelegate.ShortcutKey.openVPNSettings, + localizedTitle: UserText.netPOpenVPNQuickAction, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), + userInfo: nil) + ] + } + +} From cdff905e2c827dd32906715cbed6997c7d3dbec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 3 Dec 2024 14:48:44 +0100 Subject: [PATCH 03/27] Move app shortcuts code into extension --- DuckDuckGo/AppLifecycle/AppStates/Inactive.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index 98b47dfafa..a93e6c4cd4 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -22,13 +22,10 @@ import Subscription struct Inactive: AppState { - let application: UIApplication - init(application: UIApplication, accountManager: AccountManager, vpnFeatureVisibility: DefaultNetworkProtectionVisibility, vpnWorkaround: VPNRedditSessionWorkaround) { - self.application = application Task { @MainActor in await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, accountManager: accountManager) await vpnWorkaround.removeRedditSessionWorkaround() From 75e80048238db836a83ef2d48935cf60b11b4032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 4 Dec 2024 10:47:37 +0100 Subject: [PATCH 04/27] Make states api cleaner --- DuckDuckGo.xcodeproj/project.pbxproj | 4 +++ DuckDuckGo/AppDependencies.swift | 31 ++++++++++++++++ DuckDuckGo/AppLifecycle/AppStateMachine.swift | 23 ++++++++++-- .../AppLifecycle/AppStateTransitions.swift | 36 ++++++++++++------- .../AppLifecycle/AppStates/Active.swift | 26 +++++++++++++- .../AppLifecycle/AppStates/Background.swift | 8 ++++- .../AppLifecycle/AppStates/Inactive.swift | 12 +++---- DuckDuckGo/AppLifecycle/AppStates/Init.swift | 4 +-- .../AppLifecycle/AppStates/Launched.swift | 14 +++++++- 9 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 DuckDuckGo/AppDependencies.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 37d4c76772..d84a167972 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -995,6 +995,7 @@ CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBAD0F0A2CFF418F006267B8 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */; }; + CBAD0F0C2CFF4EE1006267B8 /* AppDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; @@ -2840,6 +2841,7 @@ CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateMachine.swift; sourceTree = ""; }; CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; + CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependencies.swift; sourceTree = ""; }; CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; @@ -6374,6 +6376,7 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */, CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */, CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, @@ -7901,6 +7904,7 @@ 85047C772A0D5D3D00D2FF3F /* SyncSettingsViewController+SyncDelegate.swift in Sources */, 85DDE0402AC6FF65006ABCA2 /* MainView.swift in Sources */, 980891A72237D5D800313A70 /* FeedbackPresenter.swift in Sources */, + CBAD0F0C2CFF4EE1006267B8 /* AppDependencies.swift in Sources */, 989B337522D7EF2100437824 /* EmptyCollectionReusableView.swift in Sources */, 0283A1FE2C6E3E1B00508FBD /* BrokenSitePromptViewModel.swift in Sources */, 8524CC94246C5C8900E59D45 /* DaxDialogViewController.swift in Sources */, diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift new file mode 100644 index 0000000000..2b4a1f2e3e --- /dev/null +++ b/DuckDuckGo/AppDependencies.swift @@ -0,0 +1,31 @@ +// +// AppDependencies.swift +// DuckDuckGo +// +// 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 Subscription + +struct AppDependencies { + + // embed in Subscription service + let accountManager: AccountManager + // embed in VPN service + let vpnWorkaround: VPNRedditSessionWorkaround + let vpnFeatureVisibility: DefaultNetworkProtectionVisibility + // .. + +} diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 53fb3b1bf0..6f063e033f 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -22,9 +22,9 @@ import UIKit enum AppEvent { case launching(UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) - case activating(UIApplication) - case backgrounding(UIApplication) - case suspending(UIApplication) + case activating + case backgrounding + case suspending case openURL(URL) @@ -51,3 +51,20 @@ final class AppStateMachine: AppEventHandler { } } + +struct AppContext { + + let application: UIApplication + let launchOptions: [UIApplication.LaunchOptionsKey: Any]? + var urlToOpen: URL? + +} + +struct TransitionContext { + + let event: AppEvent + let sourceState: AppState + +} + + diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 8e30596fee..3e119c59cc 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -25,7 +25,7 @@ extension Init { func apply(event: AppEvent) -> any AppState { switch event { case .launching(let application, let launchOptions): - return Launched(application: application, launchOptions: launchOptions) + return Launched(appContext: AppContext(application: application, launchOptions: launchOptions)) default: return handleUnexpectedEvent(event) } @@ -37,9 +37,12 @@ extension Launched { func apply(event: AppEvent) -> any AppState { switch event { - case .activating(let application): - return Active(application: application) - case .openURL: + case .activating: + return Active(appContext: appContext, + transitionContext: TransitionContext(event: event, sourceState: self) + appDependencies: appDependencies) + case .openURL(let url): + appContext.urlToOpen = url return self case .launching, .suspending, .backgrounding: return handleUnexpectedEvent(event) @@ -52,8 +55,9 @@ extension Active { func apply(event: AppEvent) -> any AppState { switch event { - case .suspending(let application): - return Inactive(application: application) + case .suspending: + return Inactive(appContext: appContext, + appDependencies: appDependencies) case .launching, .activating, .backgrounding, .openURL: return handleUnexpectedEvent(event) } @@ -65,10 +69,13 @@ extension Inactive { func apply(event: AppEvent) -> any AppState { switch event { - case .backgrounding(let application): - return Background(application: application) - case .activating(let application): - return Active(application: application) + case .backgrounding: + return Background(appContext: appContext, + appDependencies: appDependencies) + case .activating: + return Active(appContext: appContext, + transitionContext: TransitionContext(event: event, sourceState: self), + appDependencies: appDependencies) case .launching, .suspending, .openURL: return handleUnexpectedEvent(event) } @@ -80,9 +87,12 @@ extension Background { func apply(event: AppEvent) -> any AppState { switch event { - case .activating(let application): - return Active(application: application) - case .openURL: + case .activating: + return Active(appContext: appContext, + transitionContext: TransitionContext(event: event, sourceState: self), + appDependencies: appDependencies) + case .openURL(let url): + appContext.urlToOpen = url return self case .launching, .suspending, .backgrounding: return handleUnexpectedEvent(event) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index df99c36d50..f567864deb 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -21,8 +21,32 @@ import UIKit struct Active: AppState { - init(application: UIApplication) { + let appContext: AppContext + let appDependencies: AppDependencies + init(appContext: AppContext, + transitionContext: TransitionContext, + appDependencies: AppDependencies) { + self.appContext = appContext + self.appDependencies = appDependencies + + if transitionContext.sourceState is Background { + // handle applicationWillEnterForeground(_:) logic here + } + + if let url = appContext.urlToOpen { + openURL(url) + } + + // handle applicationDidBecomeActive(_:) logic here + } + + private func openURL(_ url: URL) { + defer { + appContext.urlToOpen = nil + } + + // handle application(_:open:options:) logic here } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 9332e41b6b..fcc09b1aee 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -21,8 +21,14 @@ import UIKit struct Background: AppState { - init(application: UIApplication) { + let appContext: AppContext + let appDependencies: AppDependencies + init(appContext: AppContext, appDependencies: AppDependencies) { + self.appContext = appContext + self.appDependencies = appDependencies + + // handle applicationDidEnterBackground(_:) logic here } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index a93e6c4cd4..0143b07aa3 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -22,13 +22,13 @@ import Subscription struct Inactive: AppState { - init(application: UIApplication, - accountManager: AccountManager, - vpnFeatureVisibility: DefaultNetworkProtectionVisibility, - vpnWorkaround: VPNRedditSessionWorkaround) { + let appContext: AppContext + let appDependencies: AppDependencies + + init(appContext: AppContext, appDependencies: AppDependencies) { Task { @MainActor in - await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, accountManager: accountManager) - await vpnWorkaround.removeRedditSessionWorkaround() + await appContext.application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, accountManager: accountManager) + await appDependencies.vpnWorkaround.removeRedditSessionWorkaround() } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift index d68d714ea5..5197090006 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Init.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -17,6 +17,4 @@ // limitations under the License. // -struct Init: AppState { - -} +struct Init: AppState { } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 674bcf0b91..d8750de824 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -21,8 +21,20 @@ import UIKit struct Launched: AppState { - init(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + let appContext: AppContext + let appDependencies: AppDependencies + init(appContext: AppContext) { + self.appContext = appContext + let accountManager = AppDependencyProvider.shared.accountManager + let tunnelController = AppDependencyProvider.shared.networkProtectionTunnelController + let vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) + let vpnFeatureVisibility = AppDependencyProvider.shared.vpnFeatureVisibility + self.appDependencies = AppDependencies(accountManager: accountManager, + vpnWorkaround: vpnWorkaround, + vpnFeatureVisibility: vpnFeatureVisibility) + + // handle application(_:didFinishLaunchingWithOptions:) logic here } } From e4f9be0c65c3c0352968d716874f1c1a90783fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 5 Dec 2024 15:32:43 +0100 Subject: [PATCH 05/27] Clean more dependency hell --- DuckDuckGo.xcodeproj/project.pbxproj | 20 + DuckDuckGo/AppDelegate.swift | 13 +- DuckDuckGo/AppDependencies.swift | 30 +- DuckDuckGo/AppLifecycle/AppStateMachine.swift | 30 +- .../AppLifecycle/AppStateTransitions.swift | 10 +- .../AppLifecycle/AppStates/Active.swift | 34 +- .../AppLifecycle/AppStates/Background.swift | 23 +- .../AppLifecycle/AppStates/Inactive.swift | 8 +- DuckDuckGo/AppLifecycle/AppStates/Init.swift | 19 +- .../AppLifecycle/AppStates/Launched.swift | 545 +++++++++++++++++- .../AppServices/SubscriptionService.swift | 32 + DuckDuckGo/AppServices/UIService.swift | 104 ++++ DuckDuckGo/AppServices/UNService.swift | 96 +++ DuckDuckGo/AppShortcuts.swift | 1 + DuckDuckGo/BlankSnapshotViewController.swift | 10 +- 15 files changed, 937 insertions(+), 38 deletions(-) create mode 100644 DuckDuckGo/AppServices/SubscriptionService.swift create mode 100644 DuckDuckGo/AppServices/UIService.swift create mode 100644 DuckDuckGo/AppServices/UNService.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d84a167972..af4a7d0395 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -996,6 +996,9 @@ CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBAD0F0A2CFF418F006267B8 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */; }; CBAD0F0C2CFF4EE1006267B8 /* AppDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */; }; + CBAD0F102D0062A7006267B8 /* UIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F0F2D0062A3006267B8 /* UIService.swift */; }; + CBAD0F122D00F1C8006267B8 /* UNService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F112D00F1C8006267B8 /* UNService.swift */; }; + CBAD0F142D01EE45006267B8 /* SubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F132D01EE40006267B8 /* SubscriptionService.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; @@ -2842,6 +2845,9 @@ CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependencies.swift; sourceTree = ""; }; + CBAD0F0F2D0062A3006267B8 /* UIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIService.swift; sourceTree = ""; }; + CBAD0F112D00F1C8006267B8 /* UNService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNService.swift; sourceTree = ""; }; + CBAD0F132D01EE40006267B8 /* SubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionService.swift; sourceTree = ""; }; CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; @@ -5487,6 +5493,16 @@ path = AppLifecycle; sourceTree = ""; }; + CBAD0F0E2D006291006267B8 /* AppServices */ = { + isa = PBXGroup; + children = ( + CBAD0F0F2D0062A3006267B8 /* UIService.swift */, + CBAD0F112D00F1C8006267B8 /* UNService.swift */, + CBAD0F132D01EE40006267B8 /* SubscriptionService.swift */, + ); + path = AppServices; + sourceTree = ""; + }; D62EC3B72C24695800FC9D04 /* DuckPlayer */ = { isa = PBXGroup; children = ( @@ -6376,6 +6392,7 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F0E2D006291006267B8 /* AppServices */, CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */, CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */, CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, @@ -7728,6 +7745,7 @@ 8505836E219F424500ED4EDB /* RoundedRectangleView.swift in Sources */, EE8594992A44791C008A6D06 /* NetworkProtectionTunnelController.swift in Sources */, 1EEF123F2850A68A003DDE57 /* PrivacyInfoContainerView.swift in Sources */, + CBAD0F122D00F1C8006267B8 /* UNService.swift in Sources */, F4B0B796252CB35700830156 /* OnboardingWidgetsDetailsViewController.swift in Sources */, CB258D1329A4F24E00DEBA24 /* ConfigurationStore.swift in Sources */, 85058370219F424500ED4EDB /* SearchBarExtension.swift in Sources */, @@ -7896,6 +7914,7 @@ 9F8E0F2F2CCA6202001EA7C5 /* VideoPlayerViewModel.swift in Sources */, 98D98A8225ED88E300D8E3DF /* BrowsingMenuSeparatorViewCell.swift in Sources */, D63657192A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift in Sources */, + CBAD0F142D01EE45006267B8 /* SubscriptionService.swift in Sources */, 1E4FAA6427D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift in Sources */, 8C4724502217A14B004C9B2D /* TabViewControllerLongPressBookmarkExtension.swift in Sources */, 564DE4532C3ED1B700D23241 /* NewTabDaxDialogFactory.swift in Sources */, @@ -7916,6 +7935,7 @@ 8598D2E02CEB98B500C45685 /* Favicons.swift in Sources */, 8598D2E12CEB98B500C45685 /* NotFoundCachingDownloader.swift in Sources */, 8598D2E22CEB98B500C45685 /* FaviconRequestModifier.swift in Sources */, + CBAD0F102D0062A7006267B8 /* UIService.swift in Sources */, 8598D2E32CEB98B500C45685 /* FaviconUserScript.swift in Sources */, 8598D2E42CEB98B500C45685 /* FaviconSourcesProvider.swift in Sources */, BD862E052B30DB250073E2EE /* VPNFeedbackCategory.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 35495b0ce3..855736e306 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -361,7 +361,7 @@ import os.log if shouldPresentInsufficientDiskSpaceAlertAndCrash { window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = BlankSnapshotViewController(appSettings: AppDependencyProvider.shared.appSettings, + window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, voiceSearchHelper: voiceSearchHelper) window?.makeKeyAndVisible() @@ -600,7 +600,7 @@ import os.log func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } - appStateMachine.handle(.activating(application)) + appStateMachine.handle(.activating) defer { if let didFinishLaunchingStartTime { @@ -705,7 +705,7 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { - appStateMachine.handle(.suspending(application)) + appStateMachine.handle(.suspending) } private func fireAppLaunchPixel() { @@ -794,7 +794,7 @@ import os.log } func applicationDidEnterBackground(_ application: UIApplication) { - appStateMachine.handle(.backgrounding(application)) + appStateMachine.handle(.backgrounding) displayBlankSnapshotWindow() autoClear?.startClearingTimer() lastBackgroundDate = Date() @@ -955,7 +955,8 @@ import os.log overlayWindow = UIWindow(frame: frame) overlayWindow?.windowLevel = UIWindow.Level.alert - let overlay = BlankSnapshotViewController(appSettings: AppDependencyProvider.shared.appSettings, voiceSearchHelper: voiceSearchHelper) + let overlay = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) overlay.delegate = self overlayWindow?.rootViewController = overlay @@ -1229,7 +1230,7 @@ extension DataStoreWarmup.ApplicationState { } } -private extension Error { +extension Error { var isDiskFull: Bool { let nsError = self as NSError diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift index 2b4a1f2e3e..b1e8ee9144 100644 --- a/DuckDuckGo/AppDependencies.swift +++ b/DuckDuckGo/AppDependencies.swift @@ -18,14 +18,42 @@ // import Subscription +import UIKit +import Core +import DDGSync +import Combine -struct AppDependencies { +struct AppDependencies { // should we initialize some of these in place or all in Launched state? ; also struct/class? // embed in Subscription service let accountManager: AccountManager // embed in VPN service let vpnWorkaround: VPNRedditSessionWorkaround let vpnFeatureVisibility: DefaultNetworkProtectionVisibility + + // embed in DBService + let appSettings: AppSettings + let privacyStore: PrivacyUserDefaults + // .. + let uiService: UIService + + // .. + + let voiceSearchHelper: VoiceSearchHelper + let autoClear: AutoClear + let autofillLoginSession: AutofillLoginSession + let marketplaceAdPostbackManager: MarketplaceAdPostbackManager + let syncService: DDGSync + let isSyncInProgressCancellable: AnyCancellable + let privacyProDataReporter: PrivacyProDataReporting + let remoteMessagingClient: RemoteMessagingClient + + let subscriptionService: SubscriptionService + + let onboardingPixelReporter: OnboardingPixelReporter + // .. + + } diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 6f063e033f..89f9698a97 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -32,16 +32,18 @@ enum AppEvent { protocol AppState { - func apply(event: AppEvent) -> any AppState + mutating func apply(event: AppEvent) -> any AppState } protocol AppEventHandler { + @MainActor func handle(_ event: AppEvent) } +@MainActor final class AppStateMachine: AppEventHandler { private(set) var currentState: any AppState = Init() @@ -52,11 +54,35 @@ final class AppStateMachine: AppEventHandler { } -struct AppContext { +final class AppContext { let application: UIApplication let launchOptions: [UIApplication.LaunchOptionsKey: Any]? + + var didCrashDuringCrashHandlersSetUp: Bool + var window: UIWindow? var urlToOpen: URL? + var lastBackgroundDate: Date? + var didFinishLaunchingStartTime: CFAbsoluteTime? + var isTesting: Bool + + init(application: UIApplication, + launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil, + didCrashDuringCrashHandlersSetUp: Bool = false, + window: UIWindow? = nil, + urlToOpen: URL? = nil, + lastBackgroundDate: Date? = nil, + didFinishLaunchingStartTime: CFAbsoluteTime? = nil, + isTesting: Bool = false) { + self.application = application + self.launchOptions = launchOptions + self.didCrashDuringCrashHandlersSetUp = didCrashDuringCrashHandlersSetUp + self.window = window + self.urlToOpen = urlToOpen + self.lastBackgroundDate = lastBackgroundDate + self.didFinishLaunchingStartTime = didFinishLaunchingStartTime + self.isTesting = isTesting + } } diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 3e119c59cc..cbb10b0627 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -25,7 +25,9 @@ extension Init { func apply(event: AppEvent) -> any AppState { switch event { case .launching(let application, let launchOptions): - return Launched(appContext: AppContext(application: application, launchOptions: launchOptions)) + return Launched(appContext: AppContext(application: application, // nested type + launchOptions: launchOptions, + didCrashDuringCrashHandlersSetUp: didCrashDuringCrashHandlersSetUp)) default: return handleUnexpectedEvent(event) } @@ -35,11 +37,11 @@ extension Init { extension Launched { - func apply(event: AppEvent) -> any AppState { + mutating func apply(event: AppEvent) -> any AppState { switch event { case .activating: return Active(appContext: appContext, - transitionContext: TransitionContext(event: event, sourceState: self) + transitionContext: TransitionContext(event: event, sourceState: self), appDependencies: appDependencies) case .openURL(let url): appContext.urlToOpen = url @@ -85,7 +87,7 @@ extension Inactive { extension Background { - func apply(event: AppEvent) -> any AppState { + mutating func apply(event: AppEvent) -> any AppState { switch event { case .activating: return Active(appContext: appContext, diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index f567864deb..a01f56c912 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -17,11 +17,11 @@ // limitations under the License. // -import UIKit +import Foundation struct Active: AppState { - let appContext: AppContext + var appContext: AppContext let appDependencies: AppDependencies init(appContext: AppContext, @@ -38,10 +38,38 @@ struct Active: AppState { openURL(url) } + + /* + only once! on sourceState is Launched! + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + let subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher // TODO: should we move it to Active State? + .receive(on: DispatchQueue.main) + .sink { /*[weak self, weak privacyConfigurationManager] in */ + // guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } // TODO + // + // let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + // + // Task { @MainActor [weak self] in + // if isEnabled { + // self?.subscriptionCookieManager.enableSettingSubscriptionCookie() + // } else { + // await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() + // } + // } + } + */ + // handle applicationDidBecomeActive(_:) logic here } - private func openURL(_ url: URL) { + private mutating func openURL(_ url: URL) { defer { appContext.urlToOpen = nil } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index fcc09b1aee..78c7fb3a55 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -17,11 +17,11 @@ // limitations under the License. // -import UIKit +import Foundation struct Background: AppState { - let appContext: AppContext + var appContext: AppContext let appDependencies: AppDependencies init(appContext: AppContext, appDependencies: AppDependencies) { @@ -29,6 +29,25 @@ struct Background: AppState { self.appDependencies = appDependencies // handle applicationDidEnterBackground(_:) logic here + if appDependencies.autoClear.isClearingEnabled || appDependencies.privacyStore.authenticationEnabled { + appDependencies.uiService.displayBlankSnapshotWindow(voiceSearchHelper: appDependencies.voiceSearchHelper, + addressBarPosition: appDependencies.appSettings.currentAddressBarPosition) + } + appDependencies.autoClear.startClearingTimer() + self.appContext.lastBackgroundDate = Date() + appDependencies.autofillLoginSession.endSession() + + /* + + suspendSync() + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) + privacyProDataReporter.saveApplicationLastSessionEnded() + resetAppStartTime() + + */ + } + + } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index 0143b07aa3..bdb11f13b9 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -17,17 +17,17 @@ // limitations under the License. // -import UIKit -import Subscription - struct Inactive: AppState { let appContext: AppContext let appDependencies: AppDependencies init(appContext: AppContext, appDependencies: AppDependencies) { + self.appContext = appContext + self.appDependencies = appDependencies Task { @MainActor in - await appContext.application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, accountManager: accountManager) + await appContext.application.refreshVPNShortcuts(vpnFeatureVisibility: appDependencies.vpnFeatureVisibility, + accountManager: appDependencies.accountManager) await appDependencies.vpnWorkaround.removeRedditSessionWorkaround() } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift index 5197090006..2b6354e4d5 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Init.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -17,4 +17,21 @@ // limitations under the License. // -struct Init: AppState { } +import Core +import Crashes + +@MainActor +struct Init: AppState { + + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + var didCrashDuringCrashHandlersSetUp: Bool + + init() { + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp(swapCxaThrow: false) + didCrashDuringCrashHandlersSetUp = false + } + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index d8750de824..cd6fbe524e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -17,24 +17,549 @@ // limitations under the License. // +import Foundation +import Core +import Networking +import Configuration +import Crashes import UIKit +import Persistence +import BrowserServicesKit +import WidgetKit +import DDGSync +import RemoteMessaging +import Subscription +import WebKit +import Common +import Combine +@MainActor struct Launched: AppState { - let appContext: AppContext - let appDependencies: AppDependencies + var appContext: AppContext + + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + private var privacyConfigCustomURL: String? + + private let crashCollection = CrashCollection(platform: .iOS) + private let bookmarksDatabase = BookmarksDatabase.make() + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() + private let accountManager = AppDependencyProvider.shared.accountManager + private let tunnelController = AppDependencyProvider.shared.networkProtectionTunnelController + private let vpnFeatureVisibility = AppDependencyProvider.shared.vpnFeatureVisibility + private let appSettings = AppDependencyProvider.shared.appSettings + private let privacyStore = PrivacyUserDefaults() + private let uiService = UIService() + private let voiceSearchHelper = VoiceSearchHelper() + private let autofillLoginSession = AppDependencyProvider.shared.autofillLoginSession + private let onboardingPixelReporter = OnboardingPixelReporter() + private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() + private let tipKitAppEventsHandler = TipKitAppEventHandler() + private let fireproofing = UserDefaultsFireproofing.xshared + + private let vpnWorkaround: VPNRedditSessionWorkaround + private let privacyProDataReporter: PrivacyProDataReporting + private let unService: UNService + + // TODO + private var syncDataProviders: SyncDataProviders! + private var autoClear: AutoClear! + private var syncService: DDGSync! + private var isSyncInProgressCancellable: AnyCancellable! + private var remoteMessagingClient: RemoteMessagingClient! + private var subscriptionCookieManager: SubscriptionCookieManaging! init(appContext: AppContext) { self.appContext = appContext - let accountManager = AppDependencyProvider.shared.accountManager - let tunnelController = AppDependencyProvider.shared.networkProtectionTunnelController - let vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) - let vpnFeatureVisibility = AppDependencyProvider.shared.vpnFeatureVisibility - self.appDependencies = AppDependencies(accountManager: accountManager, - vpnWorkaround: vpnWorkaround, - vpnFeatureVisibility: vpnFeatureVisibility) + privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) + vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) + unService = UNService(window: appContext.window, accountManager: accountManager) + + self.appContext.didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + defer { + if let didFinishLaunchingStartTime = appContext.didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } + } + +#if targetEnvironment(simulator) + if ProcessInfo.processInfo.environment["UITESTING"] == "true" { + // Disable hardware keyboards. + let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") + UITextInputMode.activeInputModes + // Filter `UIKeyboardInputMode`s. + .filter({ $0.responds(to: setHardwareLayout) }) + .forEach { $0.perform(setHardwareLayout, with: nil) } + } +#endif + +#if DEBUG + Pixel.isDryRun = true +#else + Pixel.isDryRun = false +#endif + + ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert + // Explicitly prepare ContentBlockingUpdating instance before Tabs are created + _ = ContentBlockingUpdating.shared + + // Can be removed after a couple of versions + cleanUpMacPromoExperiment2() + cleanUpIncrementalRolloutPixelTest() + + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) + + if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } + + crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + pixelParameters.forEach { params in + Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) + } + + // Async dispatch because rootViewController may otherwise be nil here + DispatchQueue.main.async { + guard let viewController = appContext.window?.rootViewController else { return } + + let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) // test, does it show? + } + } + + clearTmp() + + _ = DefaultUserAgentManager.shared + self.appContext.isTesting = ProcessInfo().arguments.contains("testing") + if appContext.isTesting { + Pixel.isDryRun = true + _ = DefaultUserAgentManager.shared + Database.shared.loadStore { _, _ in } + _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) + + self.appContext.window = UIWindow(frame: UIScreen.main.bounds) + self.appContext.window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + + let blockingDelegate = BlockingNavigationDelegate() + let webView = blockingDelegate.prepareWebView() + self.appContext.window?.rootViewController?.view.addSubview(webView) + self.appContext.window?.rootViewController?.view.backgroundColor = .red + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + + let request = URLRequest(url: URL(string: "about:blank")!) + webView.load(request) + + return + } + + removeEmailWaitlistState() + + var shouldPresentInsufficientDiskSpaceAlertAndCrash = false + Database.shared.loadStore { context, error in + guard let context = context else { + + let parameters = [PixelParameters.applicationState: "\(appContext.application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(appContext.application.isProtectedDataAvailable)"] + + switch error { + case .none: + fatalError("Could not create database stack: Unknown Error") + case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): + Pixel.fire(pixel: .dbContainerInitializationError, + error: underlyingError, + withAdditionalParameters: parameters) + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(underlyingError.localizedDescription)") + case .some(let error): + Pixel.fire(pixel: .dbInitializationError, + error: error, + withAdditionalParameters: parameters) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + return + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + } + DatabaseMigration.migrate(to: context) + } + + switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { + case .success: + break + case .failure(let error): + Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, + error: error) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + + WidgetCenter.shared.reloadAllTimelines() + + Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { + WidgetCenter.shared.reloadAllTimelines() + } + + PrivacyFeatures.httpsUpgrade.loadDataAsync() + + let variantManager = DefaultVariantManager() + let daxDialogs = DaxDialogs.shared + + // assign it here, because "did become active" is already too late and "viewWillAppear" + // has already been called on the HomeViewController so won't show the home row CTA + cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + + // MARK: Sync initialisation +#if DEBUG + let defaultEnvironment = ServerEnvironment.development +#else + let defaultEnvironment = ServerEnvironment.production +#endif + + let environment = ServerEnvironment( + UserDefaultsWrapper( + key: .syncEnvironment, + defaultValue: defaultEnvironment.description + ).wrappedValue + ) ?? defaultEnvironment + + let syncErrorHandler = SyncErrorHandler() + + syncDataProviders = SyncDataProviders( + bookmarksDatabase: bookmarksDatabase, + secureVaultErrorReporter: SecureVaultReporter(), + settingHandlers: [FavoritesDisplayModeSyncHandler()], + favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), + syncErrorHandler: syncErrorHandler, + faviconStoring: Favicons.shared + ) + + syncService = DDGSync( + dataProvidersSource: syncDataProviders, + errorEvents: SyncErrorHandler(), + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + environment: environment + ) + syncService.initializeIfNeeded() + isSyncInProgressCancellable = syncService.isSyncInProgressPublisher + .filter { $0 } + .sink { [weak syncService] _ in + DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) + syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in + Pixel.fire(pixel: .syncSuccessRateDaily, + withAdditionalParameters: params, + includedParameters: [.appVersion]) + }) + } + + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: AppDependencyProvider.shared.configurationStore, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ), + duckPlayerStorage: DefaultDuckPlayerStorage() + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() + + let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + purchasePlatform: .appStore) + + subscriptionCookieManager = makeSubscriptionCookieManager() + + let homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient, + privacyProDataReporter: privacyProDataReporter) + + + let previewsSource = TabPreviewsSource() + let historyManager = makeHistoryManager() + let tabsModel = prepareTabsModel(previewsSource: previewsSource) + + privacyProDataReporter.injectTabsModel(tabsModel) + + if shouldPresentInsufficientDiskSpaceAlertAndCrash { + + self.appContext.window = UIWindow(frame: UIScreen.main.bounds) + appContext.window?.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + appContext.window?.makeKeyAndVisible() + + presentInsufficientDiskSpaceAlert() + } else { + let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) + let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) + let main = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + historyManager: historyManager, + homePageConfiguration: homePageConfiguration, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings, + previewsSource: previewsSource, + tabsModel: tabsModel, + syncPausedStateManager: syncErrorHandler, + privacyProDataReporter: privacyProDataReporter, + variantManager: variantManager, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: daxDialogs, + contextualOnboardingPixelReporter: onboardingPixelReporter, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + voiceSearchHelper: voiceSearchHelper, + featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: appContext.didFinishLaunchingStartTime) + + main.loadViewIfNeeded() + syncErrorHandler.alertPresenter = main + + self.appContext.window = UIWindow(frame: UIScreen.main.bounds) + appContext.window?.rootViewController = main + appContext.window?.makeKeyAndVisible() + + autoClear = AutoClear(worker: main) + let applicationState = appContext.application.applicationState + Task { [self] in // todo + await autoClear.clearDataIfEnabled(applicationState: .init(with: applicationState)) + await vpnWorkaround.installRedditSessionWorkaround() + } + } + + voiceSearchHelper.migrateSettingsFlagIfNecessary() - // handle application(_:didFinishLaunchingWithOptions:) logic here + // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. + // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. + AppConfigurationFetch.registerBackgroundRefreshTaskHandler() + + UNUserNotificationCenter.current().delegate = unService + + appContext.window?.windowScene?.screenshotService?.delegate = uiService + ThemeManager.shared.updateUserInterfaceStyle(window: appContext.window) + + // Temporary logic for rollout of Autofill as on by default for new installs only + if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { + AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() + } + + NewTabPageIntroMessageSetup().perform() + + widgetRefreshModel.beginObservingVPNStatus() + + AppDependencyProvider.shared.subscriptionManager.loadInitialData() + + let autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, + eventMapping: EventMapping {event, _, params, _ in + switch event { + case .autofillActiveUser: + Pixel.fire(pixel: .autofillActiveUser) + case .autofillEnabledUser: + Pixel.fire(pixel: .autofillEnabledUser) + case .autofillOnboardedUser: + Pixel.fire(pixel: .autofillOnboardedUser) + case .autofillToggledOn: + Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) + case .autofillToggledOff: + Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) + case .autofillLoginsStacked: + Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) + default: + break + } + }, + installDate: StatisticsUserDefaults().installDate ?? Date()) + + _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, + object: nil, + queue: nil) { /*[weak self]*/ _ in + autofillPixelReporter.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) // todo: autofillPixelReporter is local var + } + + if appContext.didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + self.appContext.didCrashDuringCrashHandlersSetUp = false + } + + tipKitAppEventsHandler.appDidFinishLaunching() } + var appDependencies: AppDependencies { + AppDependencies( + accountManager: accountManager, + vpnWorkaround: vpnWorkaround, + vpnFeatureVisibility: vpnFeatureVisibility, + appSettings: appSettings, + privacyStore: privacyStore, + uiService: uiService, + voiceSearchHelper: voiceSearchHelper, + autoClear: autoClear, + autofillLoginSession: autofillLoginSession, + marketplaceAdPostbackManager: marketplaceAdPostbackManager, + syncService: syncService, + isSyncInProgressCancellable: isSyncInProgressCancellable, + privacyProDataReporter: privacyProDataReporter, + remoteMessagingClient: remoteMessagingClient, + subscriptionService: SubscriptionService(subscriptionCookieManager: subscriptionCookieManager), + onboardingPixelReporter: onboardingPixelReporter + ) + } + + private func presentPreemptiveCrashAlert() { + Task { @MainActor in + let alertController = CriticalAlerts.makePreemptiveCrashAlert() + appContext.window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + } + + private func cleanUpMacPromoExperiment2() { + UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") + } + + private func cleanUpIncrementalRolloutPixelTest() { + UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") + } + + private func clearTmp() { + let tmp = FileManager.default.temporaryDirectory + do { + try FileManager.default.removeItem(at: tmp) + } catch { + Logger.general.error("Failed to delete tmp dir") + } + } + + private func removeEmailWaitlistState() { + EmailWaitlist.removeEmailState() + + let autofillStorage = EmailKeychainManager() + try? autofillStorage.deleteWaitlistState() + + // Remove the authentication state if this is a fresh install. + if !Database.shared.isDatabaseFileInitialized { + try? autofillStorage.deleteAuthenticationState() + } + } + + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { + let historyMessageManager = HistoryMessageManager() + + AtbAndVariantCleanup.cleanup() + variantManager.assignVariantIfNeeded { _ in + let launchOptionsHandler = LaunchOptionsHandler() + + // MARK: perform first time launch logic here + // If it's running UI Tests check if the onboarding should be in a completed state. + if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { + daxDialogs.dismiss() + } else { + daxDialogs.primeForUse() + } + + // New users don't see the message + historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() + } + } + + private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { + let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + currentCookieStore: { //[weak self] in +// guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { // TODO +// // We shouldn't interact with WebKit's cookie store unless we have a WebView, +// // eventually the subscription cookie will be refreshed on opening the first tab +// return nil +// } + return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) + }, eventMapping: SubscriptionCookieManageEventPixelMapping()) + + return subscriptionCookieManager + } + + private func makeHistoryManager() -> HistoryManaging { + + let provider = AppDependencyProvider.shared + + switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, + isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + tld: provider.storageCache.tld) { + + case .failure(let error): + Pixel.fire(pixel: .historyStoreLoadFailed, error: error) + if error.isDiskFull { + self.presentInsufficientDiskSpaceAlert() + } else { + self.presentPreemptiveCrashAlert() + } + return NullHistoryManager() + + case .success(let historyManager): + return historyManager + } + } + + private func presentInsufficientDiskSpaceAlert() { + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + appContext.window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + + private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), + appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { + let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad + let tabsModel: TabsModel + if AutoClearSettingsModel(settings: appSettings) != nil { + tabsModel = TabsModel(desktop: isPadDevice) + tabsModel.save() + previewsSource.removeAllPreviews() + } else { + if let storedModel = TabsModel.get() { + // Save new model in case of migration + storedModel.save() + tabsModel = storedModel + } else { + tabsModel = TabsModel(desktop: isPadDevice) + } + } + return tabsModel + } + + private func makeTextZoomCoordinator() -> TextZoomCoordinator { + let provider = AppDependencyProvider.shared + let storage = TextZoomStorage() + + return TextZoomCoordinator(appSettings: provider.appSettings, + storage: storage, + featureFlagger: provider.featureFlagger) + } + + private func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + return WebCacheManager(cookieStorage: MigratableCookieStorage(), + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager) + } + + } diff --git a/DuckDuckGo/AppServices/SubscriptionService.swift b/DuckDuckGo/AppServices/SubscriptionService.swift new file mode 100644 index 0000000000..538d85795a --- /dev/null +++ b/DuckDuckGo/AppServices/SubscriptionService.swift @@ -0,0 +1,32 @@ +// +// SubscriptionService.swift +// DuckDuckGo +// +// 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 Subscription +import Combine + +final class SubscriptionService { + + init(subscriptionCookieManager: SubscriptionCookieManaging) { + self.subscriptionCookieManager = subscriptionCookieManager + } + + let subscriptionCookieManager: SubscriptionCookieManaging + var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? + +} diff --git a/DuckDuckGo/AppServices/UIService.swift b/DuckDuckGo/AppServices/UIService.swift new file mode 100644 index 0000000000..84e71de30f --- /dev/null +++ b/DuckDuckGo/AppServices/UIService.swift @@ -0,0 +1,104 @@ +// +// UIService.swift +// DuckDuckGo +// +// 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 UIKit + +final class UIService: NSObject { // possibly WindowService? + + var overlayWindow: UIWindow? + var window: UIWindow? + + func displayBlankSnapshotWindow(voiceSearchHelper: VoiceSearchHelper, + addressBarPosition: AddressBarPosition) { + guard overlayWindow == nil, let frame = window?.frame else { return } + + overlayWindow = UIWindow(frame: frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + + // TODO: most likely we do not need voiceSearchHelper for BlankSnapshotVC + let overlay = BlankSnapshotViewController(addressBarPosition: addressBarPosition, voiceSearchHelper: voiceSearchHelper) + overlay.delegate = self + + overlayWindow?.rootViewController = overlay + overlayWindow?.makeKeyAndVisible() + window?.isHidden = true + } + + func removeOverlay() { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + if let overlay = overlayWindow { + overlay.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + } + + func tryToObtainOverlayWindow() { + for window in UIApplication.shared.foregroundSceneWindows where window.rootViewController is BlankSnapshotViewController { + overlayWindow = window + return + } + } + +} + +extension UIService: BlankSnapshotViewRecoveringDelegate { + + func recoverFromPresenting(controller: BlankSnapshotViewController) { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + overlayWindow?.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + +} + +extension UIService: UIScreenshotServiceDelegate { + + func screenshotService(_ screenshotService: UIScreenshotService, + generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { + guard let mainViewController = window?.rootViewController as? MainViewController, + let webView = mainViewController.currentTab?.webView else { + completionHandler(nil, 0, .zero) + return + } + + let zoomScale = webView.scrollView.zoomScale + + // The PDF's coordinate space has its origin at the bottom left, so the view's origin.y needs to be converted + let visibleBounds = CGRect( + x: webView.scrollView.contentOffset.x / zoomScale, + y: (webView.scrollView.contentSize.height - webView.scrollView.contentOffset.y - webView.bounds.height) / zoomScale, + width: webView.bounds.width / zoomScale, + height: webView.bounds.height / zoomScale + ) + + webView.createPDF { result in + let data = try? result.get() + completionHandler(data, 0, visibleBounds) + } + } + +} diff --git a/DuckDuckGo/AppServices/UNService.swift b/DuckDuckGo/AppServices/UNService.swift new file mode 100644 index 0000000000..4ae0addd37 --- /dev/null +++ b/DuckDuckGo/AppServices/UNService.swift @@ -0,0 +1,96 @@ +// +// UNService.swift +// DuckDuckGo +// +// 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 NotificationCenter +import Core +import Subscription + +final class UNService: NSObject { + + let window: () -> UIWindow? // possibly non optional + let accountManager: AccountManager + + init(window: @autoclosure @escaping () -> UIWindow?, + accountManager: AccountManager) { + self.window = window + self.accountManager = accountManager + } + +} + +extension UNService: UNUserNotificationCenterDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler(.banner) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + if response.actionIdentifier == UNNotificationDefaultActionIdentifier { + let identifier = response.notification.request.identifier + + if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { + presentNetworkProtectionStatusSettingsModal() + } + } + + completionHandler() + } + + private func presentNetworkProtectionStatusSettingsModal() { + Task { @MainActor in + if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { + (window()?.rootViewController as? MainViewController)?.segueToVPN() + } else { + (window()?.rootViewController as? MainViewController)?.segueToPrivacyPro() + } + } + } + + private func presentSettings(with viewController: UIViewController) { + guard let window = window(), let rootViewController = window.rootViewController as? MainViewController else { return } + + if let navigationController = rootViewController.presentedViewController as? UINavigationController { + if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { + // Avoid presenting dismissing and re-presenting the view controller if it's already visible: + return + } else { + // Otherwise, replace existing view controllers with the presented one: + navigationController.popToRootViewController(animated: false) + navigationController.pushViewController(viewController, animated: false) + return + } + } + + // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: + rootViewController.clearNavigationStack() + + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { + rootViewController.segueToSettings() + let navigationController = rootViewController.presentedViewController as? UINavigationController + navigationController?.popToRootViewController(animated: false) + navigationController?.pushViewController(viewController, animated: false) + } + } +} diff --git a/DuckDuckGo/AppShortcuts.swift b/DuckDuckGo/AppShortcuts.swift index b4f55d9166..aff44e84cf 100644 --- a/DuckDuckGo/AppShortcuts.swift +++ b/DuckDuckGo/AppShortcuts.swift @@ -18,6 +18,7 @@ // import Subscription +import UIKit extension UIApplication { diff --git a/DuckDuckGo/BlankSnapshotViewController.swift b/DuckDuckGo/BlankSnapshotViewController.swift index 0268aa3f2b..f765c8d02e 100644 --- a/DuckDuckGo/BlankSnapshotViewController.swift +++ b/DuckDuckGo/BlankSnapshotViewController.swift @@ -36,15 +36,15 @@ class BlankSnapshotViewController: UIViewController { let menuButton = MenuButton() var tabSwitcherButton: TabSwitcherButton! - let appSettings: AppSettings + let addressBarPosition: AddressBarPosition let voiceSearchHelper: VoiceSearchHelperProtocol var viewCoordinator: MainViewCoordinator! weak var delegate: BlankSnapshotViewRecoveringDelegate? - init(appSettings: AppSettings, voiceSearchHelper: VoiceSearchHelperProtocol) { - self.appSettings = appSettings + init(addressBarPosition: AddressBarPosition, voiceSearchHelper: VoiceSearchHelperProtocol) { + self.addressBarPosition = addressBarPosition self.voiceSearchHelper = voiceSearchHelper super.init(nibName: nil, bundle: nil) } @@ -59,7 +59,7 @@ class BlankSnapshotViewController: UIViewController { tabSwitcherButton = TabSwitcherButton() viewCoordinator = MainViewFactory.createViewHierarchy(view, voiceSearchHelper: voiceSearchHelper) - if appSettings.currentAddressBarPosition.isBottom { + if addressBarPosition.isBottom { viewCoordinator.moveAddressBarToPosition(.bottom) viewCoordinator.hideToolbarSeparator() } @@ -231,7 +231,7 @@ extension BlankSnapshotViewController { private func updateStatusBarBackgroundColor() { let theme = ThemeManager.shared.currentTheme - if appSettings.currentAddressBarPosition == .bottom { + if addressBarPosition == .bottom { viewCoordinator.statusBackground.backgroundColor = theme.backgroundColor } else { if AppWidthObserver.shared.isPad && traitCollection.horizontalSizeClass == .regular { From 85c6416ff236ca40958608388350dabb33baa312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Mon, 9 Dec 2024 18:25:19 +0100 Subject: [PATCH 06/27] Runnable state --- DuckDuckGo.xcodeproj/project.pbxproj | 28 +- DuckDuckGo/AppDelegate.swift | 8 +- DuckDuckGo/AppDependencies.swift | 18 +- DuckDuckGo/AppLifecycle/AppStateMachine.swift | 43 +- .../AppLifecycle/AppStateTransitions.swift | 33 +- .../AppLifecycle/AppStates/Active.swift | 476 ++++++++++++++++-- .../AppLifecycle/AppStates/Background.swift | 91 +++- .../AppLifecycle/AppStates/Inactive.swift | 40 +- DuckDuckGo/AppLifecycle/AppStates/Init.swift | 17 + .../AppLifecycle/AppStates/Launched.swift | 223 ++++---- .../AppServices/SubscriptionService.swift | 18 +- DuckDuckGo/AppServices/UIService.swift | 31 +- DuckDuckGo/AppServices/UNService.swift | 10 +- DuckDuckGo/AutoClear.swift | 2 +- 14 files changed, 783 insertions(+), 255 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index af4a7d0395..10eed0b0d7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -972,6 +972,13 @@ CB2A7EEF283D185100885F67 /* RulesCompilationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EEE283D185100885F67 /* RulesCompilationMonitor.swift */; }; CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF028410DF700885F67 /* PixelEvent.swift */; }; CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */; }; + CB3C78892D06D3A700A7E4ED /* Active.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Active.swift */; }; + CB3C788A2D06D3A700A7E4ED /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; + CB3C788B2D06D3A700A7E4ED /* Launched.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */; }; + CB3C788C2D06D3A700A7E4ED /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; + CB3C788D2D06D3A700A7E4ED /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; + CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; + CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; CB48D3332B90CE9F00631D8B /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */; }; CB4FA44E2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */; }; CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; @@ -987,13 +994,6 @@ CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */; }; CB9B873E278C93C2001F4906 /* HomeMessage.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB9B873D278C93C2001F4906 /* HomeMessage.xcassets */; }; CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; - CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; - CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */; }; - CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Active.swift */; }; - CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; - CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; - CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; - CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBAD0F0A2CFF418F006267B8 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */; }; CBAD0F0C2CFF4EE1006267B8 /* AppDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */; }; CBAD0F102D0062A7006267B8 /* UIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F0F2D0062A3006267B8 /* UIService.swift */; }; @@ -7578,7 +7578,6 @@ BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, - CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */, 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */, 6FE1273D2C204C2500EB5724 /* FavoritesView.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, @@ -7616,7 +7615,6 @@ 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */, - CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 31DE43C42C2C60E800F8C51F /* DuckPlayerModalPresenter.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, @@ -7653,6 +7651,13 @@ 9FEA22272C2D2BDA006B03BF /* RootDebugViewController+Onboarding.swift in Sources */, 319A37152829A55F0079FBCE /* AutofillListItemTableViewCell.swift in Sources */, 6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */, + CB3C78892D06D3A700A7E4ED /* Active.swift in Sources */, + CB3C788A2D06D3A700A7E4ED /* Background.swift in Sources */, + CB3C788B2D06D3A700A7E4ED /* Launched.swift in Sources */, + CB3C788C2D06D3A700A7E4ED /* AppStateMachine.swift in Sources */, + CB3C788D2D06D3A700A7E4ED /* AppStateTransitions.swift in Sources */, + CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */, + CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */, 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, @@ -7683,7 +7688,6 @@ BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */, 1EE52ABB28FB1D6300B750C1 /* UIImageExtension.swift in Sources */, - CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */, 858650D12469BCDE00C36F8A /* DaxDialogs.swift in Sources */, 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, @@ -7724,7 +7728,6 @@ 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, - CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, 859DB8172CE6263C001F7210 /* TextZoomLevel.swift in Sources */, BDE91CD62C6294020005CB74 /* FeedbackCategoryProviding.swift in Sources */, @@ -7821,7 +7824,6 @@ D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 1D200C9B2BA31A6A00108701 /* AboutView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, - CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */, D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, @@ -8017,7 +8019,6 @@ 311BD1B12836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift in Sources */, B652DF13287C373A00C12A9C /* ScriptSourceProviding.swift in Sources */, 854A012B2A54412600FCC628 /* ActivityViewController.swift in Sources */, - CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */, F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */, 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */, 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */, @@ -8105,7 +8106,6 @@ 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, - CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */, 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 855736e306..64b5252bde 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -40,8 +40,8 @@ import WebKit import os.log @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) + + static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) struct ShortcutKey { static let clipboard = "com.duckduckgo.mobile.ios.clipboard" static let passwords = "com.duckduckgo.mobile.ios.passwords" @@ -117,7 +117,7 @@ import os.log private var didFinishLaunchingStartTime: CFAbsoluteTime? - private let appStateMachine = AppStateMachine() + private let appStateMachine: AppStateMachine = AppStateMachine() override init() { super.init() @@ -133,7 +133,7 @@ import os.log // swiftlint:disable:next cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - appStateMachine.handle(.launching(application, launchOptions: launchOptions)) + appStateMachine.handle(.launching(application)) didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() defer { if let didFinishLaunchingStartTime { diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift index b1e8ee9144..f402a3c18e 100644 --- a/DuckDuckGo/AppDependencies.swift +++ b/DuckDuckGo/AppDependencies.swift @@ -22,29 +22,26 @@ import UIKit import Core import DDGSync import Combine +import BrowserServicesKit -struct AppDependencies { // should we initialize some of these in place or all in Launched state? ; also struct/class? +struct AppDependencies { - // embed in Subscription service let accountManager: AccountManager - // embed in VPN service let vpnWorkaround: VPNRedditSessionWorkaround let vpnFeatureVisibility: DefaultNetworkProtectionVisibility - // embed in DBService let appSettings: AppSettings let privacyStore: PrivacyUserDefaults - // .. let uiService: UIService - - // .. + let mainViewController: MainViewController let voiceSearchHelper: VoiceSearchHelper let autoClear: AutoClear let autofillLoginSession: AutofillLoginSession - let marketplaceAdPostbackManager: MarketplaceAdPostbackManager + let marketplaceAdPostbackManager: MarketplaceAdPostbackManaging let syncService: DDGSync + let syncDataProviders: SyncDataProviders let isSyncInProgressCancellable: AnyCancellable let privacyProDataReporter: PrivacyProDataReporting let remoteMessagingClient: RemoteMessagingClient @@ -52,8 +49,7 @@ struct AppDependencies { // should we initialize some of these in place or all i let subscriptionService: SubscriptionService let onboardingPixelReporter: OnboardingPixelReporter - // .. - - + let widgetRefreshModel: NetworkProtectionWidgetRefreshModel + let autofillPixelReporter: AutofillPixelReporter } diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 89f9698a97..32506119da 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -21,7 +21,7 @@ import UIKit enum AppEvent { - case launching(UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) + case launching(UIApplication) case activating case backgrounding case suspending @@ -53,44 +53,3 @@ final class AppStateMachine: AppEventHandler { } } - -final class AppContext { - - let application: UIApplication - let launchOptions: [UIApplication.LaunchOptionsKey: Any]? - - var didCrashDuringCrashHandlersSetUp: Bool - var window: UIWindow? - var urlToOpen: URL? - var lastBackgroundDate: Date? - var didFinishLaunchingStartTime: CFAbsoluteTime? - var isTesting: Bool - - init(application: UIApplication, - launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil, - didCrashDuringCrashHandlersSetUp: Bool = false, - window: UIWindow? = nil, - urlToOpen: URL? = nil, - lastBackgroundDate: Date? = nil, - didFinishLaunchingStartTime: CFAbsoluteTime? = nil, - isTesting: Bool = false) { - self.application = application - self.launchOptions = launchOptions - self.didCrashDuringCrashHandlersSetUp = didCrashDuringCrashHandlersSetUp - self.window = window - self.urlToOpen = urlToOpen - self.lastBackgroundDate = lastBackgroundDate - self.didFinishLaunchingStartTime = didFinishLaunchingStartTime - self.isTesting = isTesting - } - -} - -struct TransitionContext { - - let event: AppEvent - let sourceState: AppState - -} - - diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index cbb10b0627..0c840d1171 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -24,10 +24,8 @@ extension Init { func apply(event: AppEvent) -> any AppState { switch event { - case .launching(let application, let launchOptions): - return Launched(appContext: AppContext(application: application, // nested type - launchOptions: launchOptions, - didCrashDuringCrashHandlersSetUp: didCrashDuringCrashHandlersSetUp)) + case .launching(let application): + return Launched(stateContext: makeStateContext(application: application)) default: return handleUnexpectedEvent(event) } @@ -40,11 +38,9 @@ extension Launched { mutating func apply(event: AppEvent) -> any AppState { switch event { case .activating: - return Active(appContext: appContext, - transitionContext: TransitionContext(event: event, sourceState: self), - appDependencies: appDependencies) + return Active(stateContext: makeStateContext()) case .openURL(let url): - appContext.urlToOpen = url + urlToOpen = url return self case .launching, .suspending, .backgrounding: return handleUnexpectedEvent(event) @@ -58,9 +54,11 @@ extension Active { func apply(event: AppEvent) -> any AppState { switch event { case .suspending: - return Inactive(appContext: appContext, - appDependencies: appDependencies) - case .launching, .activating, .backgrounding, .openURL: + return Inactive(stateContext: makeStateContext()) + case .openURL(let url): // TODO: update to tech design + openURL(url) + return self + case .launching, .activating, .backgrounding: return handleUnexpectedEvent(event) } } @@ -72,12 +70,9 @@ extension Inactive { func apply(event: AppEvent) -> any AppState { switch event { case .backgrounding: - return Background(appContext: appContext, - appDependencies: appDependencies) + return Background(stateContext: makeStateContext()) case .activating: - return Active(appContext: appContext, - transitionContext: TransitionContext(event: event, sourceState: self), - appDependencies: appDependencies) + return Active(stateContext: makeStateContext()) case .launching, .suspending, .openURL: return handleUnexpectedEvent(event) } @@ -90,11 +85,9 @@ extension Background { mutating func apply(event: AppEvent) -> any AppState { switch event { case .activating: - return Active(appContext: appContext, - transitionContext: TransitionContext(event: event, sourceState: self), - appDependencies: appDependencies) + return Active(stateContext: makeStateContext()) case .openURL(let url): - appContext.urlToOpen = url + urlToOpen = url return self case .launching, .suspending, .backgrounding: return handleUnexpectedEvent(event) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index a01f56c912..6778d43dd3 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -18,63 +18,467 @@ // import Foundation +import UIKit +import BrowserServicesKit +import Core +import WidgetKit +import BackgroundTasks +import Subscription +import NetworkProtection struct Active: AppState { - var appContext: AppContext + let application: UIApplication let appDependencies: AppDependencies - init(appContext: AppContext, - transitionContext: TransitionContext, - appDependencies: AppDependencies) { - self.appContext = appContext - self.appDependencies = appDependencies + private let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults - if transitionContext.sourceState is Background { - // handle applicationWillEnterForeground(_:) logic here + private var window: UIWindow { + appDependencies.uiService.window + } + + private var mainViewController: MainViewController { + appDependencies.mainViewController + } + + // MARK: handle one-time (after launch) logic here + init(stateContext: Launched.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + defer { + let launchTime = CFAbsoluteTimeGetCurrent() - stateContext.didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) } - if let url = appContext.urlToOpen { + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + appDependencies.subscriptionService.subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + let subscriptionCookieManager = appDependencies.subscriptionService.subscriptionCookieManager + appDependencies.subscriptionService.onPrivacyConfigurationUpdate = { [privacyConfigurationManager] in + let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + + Task { @MainActor in + if isEnabled { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } else { + await subscriptionCookieManager.disableSettingSubscriptionCookie() + } + } + } + + // onApplicationLaunch code + Task { @MainActor [self] in // todo? is capturing self here ok? + await beginAuthentication() + initialiseBackgroundFetch(application) + applyAppearanceChanges() + refreshRemoteMessages(remoteMessagingClient: appDependencies.remoteMessagingClient) + } + + if let url = stateContext.urlToOpen { openURL(url) } + activateApp(isTesting: stateContext.isTesting) + } - /* - only once! on sourceState is Launched! - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + // MARK: handle applicationWillEnterForeground(_:) logic here + init(stateContext: Background.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies - // Enable subscriptionCookieManager if feature flag is present - if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { - subscriptionCookieManager.enableSettingSubscriptionCookie() + ThemeManager.shared.updateUserInterfaceStyle() + + let uiService = appDependencies.uiService + let syncService = appDependencies.syncService + let autoClear = appDependencies.autoClear + Task { @MainActor [self] in // todo? is capturing self here ok? + await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) + await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) + uiService.showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } + + if let url = stateContext.urlToOpen { + openURL(url) + } + + activateApp() + } + + init(stateContext: Inactive.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + activateApp() + } + + // MARK: handle applicationDidBecomeActive(_:) logic here + private func activateApp(isTesting: Bool = false) { + guard !isTesting else { return } // Leaving this as is for now to ensure this code is never executed, regardless of where it's called from. + // In the future, we may consider creating separate states specifically for testing purposes. + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) + appDependencies.syncService.initializeIfNeeded() + appDependencies.syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: appDependencies.syncService) + + if !(appDependencies.uiService.overlayWindow?.rootViewController is AuthenticationViewController) { + appDependencies.uiService.removeOverlay() + } + + StatisticsLoader.shared.load { + StatisticsLoader.shared.refreshAppRetentionAtb() + self.fireAppLaunchPixel() + self.reportAdAttribution() + self.appDependencies.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() + } + + mainViewController.showBars() + mainViewController.didReturnFromBackground() + + if !appDependencies.privacyStore.authenticationEnabled { + showKeyboardOnLaunch() + } + + if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false + } + AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() + + AppConfigurationFetch().start { result in + self.sendAppLaunchPostback(marketplaceAdPostbackManager: appDependencies.marketplaceAdPostbackManager) + if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + } + } + + appDependencies.syncService.scheduler.notifyAppLifecycleEvent() + + appDependencies.privacyProDataReporter.injectSyncService(appDependencies.syncService) + + fireFailedCompilationsPixelIfNeeded() + + appDependencies.widgetRefreshModel.refreshVPNWidget() + + if tunnelDefaults.showEntitlementAlert { + presentExpiredEntitlementAlert() + } + + presentExpiredEntitlementNotificationIfNeeded() + + Task { + await stopAndRemoveVPNIfNotAuthenticated() + await application.refreshVPNShortcuts(vpnFeatureVisibility: appDependencies.vpnFeatureVisibility, + accountManager: appDependencies.accountManager) + await appDependencies.vpnWorkaround.installRedditSessionWorkaround() + + if #available(iOS 17.0, *) { + await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() + } + } + + AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in + if isSubscriptionActive { + DailyPixel.fire(pixel: .privacyProSubscriptionActive) + } + } + + Task { + await appDependencies.subscriptionService.subscriptionCookieManager.refreshSubscriptionCookie() + } + + let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: appDependencies.syncService) + importPasswordsStatusHandler.checkSyncSuccessStatus() + + Task { + await appDependencies.privacyProDataReporter.saveWidgetAdded() + } + + AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + } + + // MARK: handle application(_:open:options:) logic here + func openURL(_ url: URL) { // TODO: should we reset URL? probably not + Logger.sync.debug("App launched with url \(url.absoluteString)") + // If showing the onboarding intro ignore deeplinks + guard mainViewController.needsToShowOnboardingIntro() == false else { + return // todo was return false + } + + if handleEmailSignUpDeepLink(url) { + return // todo was return true + } + + NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) + + // The openVPN action handles the navigation stack on its own and does not need it to be cleared + if url != AppDeepLinkSchemes.openVPN.url { + mainViewController.clearNavigationStack() } - // Keep track of feature flag changes - let subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher // TODO: should we move it to Active State? - .receive(on: DispatchQueue.main) - .sink { /*[weak self, weak privacyConfigurationManager] in */ - // guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } // TODO - // - // let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) - // - // Task { @MainActor [weak self] in - // if isEnabled { - // self?.subscriptionCookieManager.enableSettingSubscriptionCookie() - // } else { - // await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() - // } - // } + Task { @MainActor in + // Autoclear should have happened by now + appDependencies.uiService.showKeyboardIfSettingOn = false + + if !handleAppDeepLink(application, mainViewController, url) { + mainViewController.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) } - */ + } + } + + @MainActor + private func beginAuthentication(lastBackgroundDate: Date? = nil) async { + guard appDependencies.privacyStore.authenticationEnabled else { return } + + let uiService = appDependencies.uiService + uiService.removeOverlay() + uiService.displayAuthenticationWindow() + + guard let controller = uiService.overlayWindow?.rootViewController as? AuthenticationViewController else { + uiService.removeOverlay() + return + } + + await controller.beginAuthentication { + uiService.removeOverlay() + showKeyboardOnLaunch(lastBackgroundDate: lastBackgroundDate) + } + } - // handle applicationDidBecomeActive(_:) logic here + private func showKeyboardOnLaunch(lastBackgroundDate: Date? = nil) { + guard KeyboardSettings().onAppLaunch && appDependencies.uiService.showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch(lastBackgroundDate: lastBackgroundDate) else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.mainViewController.enterSearch() + } + appDependencies.uiService.showKeyboardIfSettingOn = false } - private mutating func openURL(_ url: URL) { - defer { - appContext.urlToOpen = nil + private func shouldShowKeyboardOnLaunch(lastBackgroundDate: Date? = nil) -> Bool { + guard let lastBackgroundDate else { return true } + return Date().timeIntervalSince(lastBackgroundDate) > AppDelegate.ShowKeyboardOnLaunchThreshold + } + + private func fireAppLaunchPixel() { + + WidgetCenter.shared.getCurrentConfigurations { result in + let paramKeys: [WidgetFamily: String] = [ + .systemSmall: PixelParameters.widgetSmall, + .systemMedium: PixelParameters.widgetMedium, + .systemLarge: PixelParameters.widgetLarge + ] + + switch result { + case .failure(let error): + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [ + PixelParameters.widgetError: "1", + PixelParameters.widgetErrorCode: "\((error as NSError).code)", + PixelParameters.widgetErrorDomain: (error as NSError).domain + ], includedParameters: [.appVersion, .atb]) + + case .success(let widgetInfo): + let params = widgetInfo.reduce([String: String]()) { + var result = $0 + if let key = paramKeys[$1.family] { + result[key] = "1" + } + return result + } + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: params, includedParameters: [.appVersion, .atb]) + } + + } + } + + private func sendAppLaunchPostback(marketplaceAdPostbackManager: MarketplaceAdPostbackManaging) { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + + private func reportAdAttribution() { + Task.detached(priority: .background) { + await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() + } + } + + private func initialiseBackgroundFetch(_ application: UIApplication) { + guard UIApplication.shared.backgroundRefreshStatus == .available else { + return + } + + // BackgroundTasks will automatically replace an existing task in the queue if one with the same identifier is queued, so we should only + // schedule a task if there are none pending in order to avoid the config task getting perpetually replaced. + BGTaskScheduler.shared.getPendingTaskRequests { tasks in + let hasConfigurationTask = tasks.contains { $0.identifier == AppConfigurationFetch.Constants.backgroundProcessingTaskIdentifier } + if !hasConfigurationTask { + AppConfigurationFetch.scheduleBackgroundRefreshTask() + } + + let hasRemoteMessageFetchTask = tasks.contains { $0.identifier == RemoteMessagingClient.Constants.backgroundRefreshTaskIdentifier } + if !hasRemoteMessageFetchTask { + RemoteMessagingClient.scheduleBackgroundRefreshTask() + } + } + } + + private func applyAppearanceChanges() { + UILabel.appearance(whenContainedInInstancesOf: [UIAlertController.self]).numberOfLines = 0 + } + + /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. + func refreshRemoteMessages(remoteMessagingClient: RemoteMessagingClient) { + Task { + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) + } + } + + private func presentExpiredEntitlementAlert() { + let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak mainViewController] in //todo? + mainViewController?.segueToPrivacyPro() + } + window.rootViewController?.present(alertController, animated: true) { [weak tunnelDefaults] in + tunnelDefaults?.showEntitlementAlert = false + } + } + + private func handleEmailSignUpDeepLink(_ url: URL) -> Bool { + guard url.absoluteString.starts(with: URL.emailProtection.absoluteString), + let navViewController = mainViewController.presentedViewController as? UINavigationController, + let emailSignUpViewController = navViewController.topViewController as? EmailSignupViewController else { + return false + } + emailSignUpViewController.loadUrl(url) + return true + } + + private func fireFailedCompilationsPixelIfNeeded() { + let store = FailedCompilationsStore() + if store.hasAnyFailures { + DailyPixel.fire(pixel: .compilationFailed, withAdditionalParameters: store.summary) { error in + guard error != nil else { return } + store.cleanup() + } } + } + + private func stopAndRemoveVPNIfNotAuthenticated() async { + // Only remove the VPN if the user is not authenticated, and it's installed: + guard !appDependencies.accountManager.isUserAuthenticated, await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { + return + } + + await AppDependencyProvider.shared.networkProtectionTunnelController.stop() + await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN(reason: .didBecomeActiveCheck) + } + + private func presentExpiredEntitlementNotificationIfNeeded() { + let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( + settings: AppDependencyProvider.shared.vpnSettings, + defaults: .networkProtectionGroupDefaults, + wrappee: NetworkProtectionUNNotificationPresenter() + ) + presenter.showEntitlementNotification() + } + + + @MainActor + func handleAppDeepLink(_ app: UIApplication, _ mainViewController: MainViewController?, _ url: URL) -> Bool { + guard let mainViewController else { return false } + + switch AppDeepLinkSchemes.fromURL(url) { + + case .newSearch: + mainViewController.newTab(reuseExisting: true) + mainViewController.enterSearch() + + case .favorites: + mainViewController.newTab(reuseExisting: true, allowingKeyboard: false) + + case .quickLink: + let query = AppDeepLinkSchemes.query(fromQuickLink: url) + mainViewController.loadQueryInNewTab(query, reuseExisting: true) + + case .addFavorite: + mainViewController.startAddFavoriteFlow() + + case .fireButton: + mainViewController.forgetAllWithAnimation() + + case .voiceSearch: + mainViewController.onVoiceSearchPressed() + + case .newEmail: + mainViewController.newEmailAddress() + + case .openVPN: + presentNetworkProtectionStatusSettingsModal() + + case .openPasswords: + var source: AutofillSettingsSource = .homeScreenWidget + + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let queryItems = components.queryItems, + queryItems.first(where: { $0.name == "ls" }) != nil { + Pixel.fire(pixel: .autofillLoginsLaunchWidgetLock) + source = .lockScreenWidget + } else { + Pixel.fire(pixel: .autofillLoginsLaunchWidgetHome) + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { + mainViewController.launchAutofillLogins(openSearch: true, source: source) + } + + default: + guard app.applicationState == .active, + let currentTab = mainViewController.currentTab else { + return false + } + + // If app is in active state, treat this navigation as something initiated form the context of the current tab. + mainViewController.tab(currentTab, + didRequestNewTabForUrl: url, + openedByPage: true, + inheritingAttribution: nil) + } + + return true + } + + @MainActor + func presentNetworkProtectionStatusSettingsModal() { + Task { + if case .success(let hasEntitlements) = await appDependencies.accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { + (window.rootViewController as? MainViewController)?.segueToVPN() + } else { + (window.rootViewController as? MainViewController)?.segueToPrivacyPro() + } + } + } + +} + +extension Active { + + struct StateContext { + + let application: UIApplication + let appDependencies: AppDependencies + + } - // handle application(_:open:options:) logic here + func makeStateContext() -> StateContext { + .init( + application: application, + appDependencies: appDependencies + ) } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 78c7fb3a55..8124e950c9 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -18,36 +18,91 @@ // import Foundation +import Combine +import DDGSync +import UIKit struct Background: AppState { - var appContext: AppContext - let appDependencies: AppDependencies + private let lastBackgroundDate: Date = Date() + private let application: UIApplication + private let appDependencies: AppDependencies + private var syncDidFinishCancellable: AnyCancellable? // TODO: should we pass it through appDependencies to sustain its lifecycle? perhaps we should have a registry to register it + var urlToOpen: URL? - init(appContext: AppContext, appDependencies: AppDependencies) { - self.appContext = appContext - self.appDependencies = appDependencies + init(stateContext: Inactive.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies - // handle applicationDidEnterBackground(_:) logic here - if appDependencies.autoClear.isClearingEnabled || appDependencies.privacyStore.authenticationEnabled { - appDependencies.uiService.displayBlankSnapshotWindow(voiceSearchHelper: appDependencies.voiceSearchHelper, - addressBarPosition: appDependencies.appSettings.currentAddressBarPosition) + let autoClear = appDependencies.autoClear + let privacyStore = appDependencies.privacyStore + let privacyProDataReporter = appDependencies.privacyProDataReporter + let voiceSearchHelper = appDependencies.voiceSearchHelper + let appSettings = appDependencies.appSettings + let autofillLoginSession = appDependencies.autofillLoginSession + let syncService = appDependencies.syncService + let syncDataProviders = appDependencies.syncDataProviders + let uiService = appDependencies.uiService + + if autoClear.isClearingEnabled || privacyStore.authenticationEnabled { + uiService.displayBlankSnapshotWindow(voiceSearchHelper: voiceSearchHelper, + addressBarPosition: appSettings.currentAddressBarPosition) } - appDependencies.autoClear.startClearingTimer() - self.appContext.lastBackgroundDate = Date() - appDependencies.autofillLoginSession.endSession() + autoClear.startClearingTimer() + autofillLoginSession.endSession() + + suspendSync(syncService: syncService) + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(stateContext.application) + privacyProDataReporter.saveApplicationLastSessionEnded() - /* + resetAppStartTime() + } - suspendSync() - syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) - privacyProDataReporter.saveApplicationLastSessionEnded() - resetAppStartTime() + private mutating func suspendSync(syncService: DDGSync) { + if syncService.isSyncInProgress { + Logger.sync.debug("Sync is in progress. Starting background task to allow it to gracefully complete.") - */ + var taskID: UIBackgroundTaskIdentifier! + taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { + Logger.sync.debug("Forcing background task completion") + UIApplication.shared.endBackgroundTask(taskID) + } + syncDidFinishCancellable?.cancel() + syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } + .prefix(1) + .receive(on: DispatchQueue.main) + .sink { _ in + Logger.sync.debug("Ending background task") + UIApplication.shared.endBackgroundTask(taskID) + } + } + syncService.scheduler.cancelSyncAndSuspendSyncQueue() } + private func resetAppStartTime() { +// didFinishLaunchingStartTime = nil // TODO: not needed most likely + appDependencies.mainViewController.appDidFinishLaunchingStartTime = nil + } + +} +extension Background { + + struct StateContext { + + let application: UIApplication + let lastBackgroundDate: Date + let urlToOpen: URL? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + lastBackgroundDate: lastBackgroundDate, + urlToOpen: urlToOpen, + appDependencies: appDependencies) + } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index bdb11f13b9..86fafb506f 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -17,19 +17,41 @@ // limitations under the License. // +import UIKit + struct Inactive: AppState { - let appContext: AppContext - let appDependencies: AppDependencies + private let application: UIApplication + private let appDependencies: AppDependencies + + init(stateContext: Active.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies - init(appContext: AppContext, appDependencies: AppDependencies) { - self.appContext = appContext - self.appDependencies = appDependencies - Task { @MainActor in - await appContext.application.refreshVPNShortcuts(vpnFeatureVisibility: appDependencies.vpnFeatureVisibility, - accountManager: appDependencies.accountManager) - await appDependencies.vpnWorkaround.removeRedditSessionWorkaround() + let vpnFeatureVisibility = appDependencies.vpnFeatureVisibility + let accountManager = appDependencies.accountManager + let vpnWorkaround = appDependencies.vpnWorkaround + Task { @MainActor [application] in + await application.refreshVPNShortcuts(vpnFeatureVisibility: vpnFeatureVisibility, + accountManager: accountManager) + await vpnWorkaround.removeRedditSessionWorkaround() } } } + +extension Inactive { + + struct StateContext { + + let application: UIApplication + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + appDependencies: appDependencies) + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift index 2b6354e4d5..5054d8f27e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Init.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -19,6 +19,7 @@ import Core import Crashes +import UIKit @MainActor struct Init: AppState { @@ -35,3 +36,19 @@ struct Init: AppState { } } + +extension Init { + + struct StateContext { + + let application: UIApplication + let didCrashDuringCrashHandlersSetUp: Bool + + } + + func makeStateContext(application: UIApplication) -> StateContext { + .init(application: application, + didCrashDuringCrashHandlersSetUp: didCrashDuringCrashHandlersSetUp) + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index cd6fbe524e..1cda364d23 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -36,11 +36,12 @@ import Combine @MainActor struct Launched: AppState { - var appContext: AppContext - @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) private var privacyConfigCustomURL: String? + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + private var didCrashDuringCrashHandlersSetUp: Bool + private let crashCollection = CrashCollection(platform: .iOS) private let bookmarksDatabase = BookmarksDatabase.make() private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() @@ -49,7 +50,6 @@ struct Launched: AppState { private let vpnFeatureVisibility = AppDependencyProvider.shared.vpnFeatureVisibility private let appSettings = AppDependencyProvider.shared.appSettings private let privacyStore = PrivacyUserDefaults() - private let uiService = UIService() private let voiceSearchHelper = VoiceSearchHelper() private let autofillLoginSession = AppDependencyProvider.shared.autofillLoginSession private let onboardingPixelReporter = OnboardingPixelReporter() @@ -59,29 +59,38 @@ struct Launched: AppState { private let vpnWorkaround: VPNRedditSessionWorkaround private let privacyProDataReporter: PrivacyProDataReporting - private let unService: UNService - - // TODO + private let isTesting = ProcessInfo().arguments.contains("testing") + private let didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + + // These should ideally be `let` properties instead of force unwrapped. However, due to the large amount of code, the compiler cannot track the initialization order. + // As a result, it complains about accessing `self` before all properties are set. + // This is a temporary state and will be gradually phased out as the class is cleaned up and most of the code is moved into services. + // Note: This cleanup is not part of the current milestone. + private var uiService: UIService! + private var unService: UNService! private var syncDataProviders: SyncDataProviders! private var autoClear: AutoClear! private var syncService: DDGSync! private var isSyncInProgressCancellable: AnyCancellable! private var remoteMessagingClient: RemoteMessagingClient! private var subscriptionCookieManager: SubscriptionCookieManaging! + private var window: UIWindow? + private var autofillPixelReporter: AutofillPixelReporter! + private var mainViewController: MainViewController! + + var urlToOpen: URL? - init(appContext: AppContext) { - self.appContext = appContext + let application: UIApplication + init(stateContext: Init.StateContext) { + + application = stateContext.application privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) - unService = UNService(window: appContext.window, accountManager: accountManager) - self.appContext.didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() defer { - if let didFinishLaunchingStartTime = appContext.didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) - } + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) } #if targetEnvironment(simulator) @@ -117,37 +126,37 @@ struct Launched: AppState { Configuration.setURLProvider(AppConfigurationURLProvider()) } - crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + crashCollection.startAttachingCrashLogMessages { [application] pixelParameters, payloads, sendReport in pixelParameters.forEach { params in Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) } // Async dispatch because rootViewController may otherwise be nil here DispatchQueue.main.async { - guard let viewController = appContext.window?.rootViewController else { return } - + guard let viewController = application.window?.rootViewController else { return } // todo: check if it shows let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) - crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) // test, does it show? + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) } } clearTmp() _ = DefaultUserAgentManager.shared - self.appContext.isTesting = ProcessInfo().arguments.contains("testing") - if appContext.isTesting { + if isTesting { Pixel.isDryRun = true _ = DefaultUserAgentManager.shared Database.shared.loadStore { _, _ in } _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) - self.appContext.window = UIWindow(frame: UIScreen.main.bounds) - self.appContext.window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() let blockingDelegate = BlockingNavigationDelegate() let webView = blockingDelegate.prepareWebView() - self.appContext.window?.rootViewController?.view.addSubview(webView) - self.appContext.window?.rootViewController?.view.backgroundColor = .red + window!.rootViewController?.view.addSubview(webView) + window!.rootViewController?.view.backgroundColor = .red + application.setWindow(window) // to do check if application.delegate?.window is non-nil + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) let request = URLRequest(url: URL(string: "about:blank")!) @@ -162,8 +171,8 @@ struct Launched: AppState { Database.shared.loadStore { context, error in guard let context = context else { - let parameters = [PixelParameters.applicationState: "\(appContext.application.applicationState.rawValue)", - PixelParameters.dataAvailability: "\(appContext.application.isProtectedDataAvailable)"] + let parameters = [PixelParameters.applicationState: "\(stateContext.application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(stateContext.application.isProtectedDataAvailable)"] switch error { case .none: @@ -281,6 +290,7 @@ struct Launched: AppState { purchasePlatform: .appStore) subscriptionCookieManager = makeSubscriptionCookieManager() + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager let homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, remoteMessagingClient: remoteMessagingClient, @@ -295,54 +305,59 @@ struct Launched: AppState { if shouldPresentInsufficientDiskSpaceAlertAndCrash { - self.appContext.window = UIWindow(frame: UIScreen.main.bounds) - appContext.window?.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - appContext.window?.makeKeyAndVisible() + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + window!.makeKeyAndVisible() + application.setWindow(window) presentInsufficientDiskSpaceAlert() } else { let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) - let main = MainViewController(bookmarksDatabase: bookmarksDatabase, - bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, - historyManager: historyManager, - homePageConfiguration: homePageConfiguration, - syncService: syncService, - syncDataProviders: syncDataProviders, - appSettings: AppDependencyProvider.shared.appSettings, - previewsSource: previewsSource, - tabsModel: tabsModel, - syncPausedStateManager: syncErrorHandler, - privacyProDataReporter: privacyProDataReporter, - variantManager: variantManager, - contextualOnboardingPresenter: contextualOnboardingPresenter, - contextualOnboardingLogic: daxDialogs, - contextualOnboardingPixelReporter: onboardingPixelReporter, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - voiceSearchHelper: voiceSearchHelper, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - fireproofing: fireproofing, - subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: makeTextZoomCoordinator(), - websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), - appDidFinishLaunchingStartTime: appContext.didFinishLaunchingStartTime) - - main.loadViewIfNeeded() - syncErrorHandler.alertPresenter = main - - self.appContext.window = UIWindow(frame: UIScreen.main.bounds) - appContext.window?.rootViewController = main - appContext.window?.makeKeyAndVisible() - - autoClear = AutoClear(worker: main) - let applicationState = appContext.application.applicationState - Task { [self] in // todo + mainViewController = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + historyManager: historyManager, + homePageConfiguration: homePageConfiguration, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings, + previewsSource: previewsSource, + tabsModel: tabsModel, + syncPausedStateManager: syncErrorHandler, + privacyProDataReporter: privacyProDataReporter, + variantManager: variantManager, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: daxDialogs, + contextualOnboardingPixelReporter: onboardingPixelReporter, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + voiceSearchHelper: voiceSearchHelper, + featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) + + mainViewController.loadViewIfNeeded() + syncErrorHandler.alertPresenter = mainViewController + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = mainViewController + window.makeKeyAndVisible() + application.setWindow(window) + + let autoClear = AutoClear(worker: mainViewController) + self.autoClear = autoClear + let applicationState = stateContext.application.applicationState + let vpnWorkaround = appDependencies.vpnWorkaround + Task { await autoClear.clearDataIfEnabled(applicationState: .init(with: applicationState)) await vpnWorkaround.installRedditSessionWorkaround() } } - + unService = UNService(window: window!, accountManager: accountManager) + uiService = UIService(window: window!) voiceSearchHelper.migrateSettingsFlagIfNecessary() // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. @@ -351,8 +366,8 @@ struct Launched: AppState { UNUserNotificationCenter.current().delegate = unService - appContext.window?.windowScene?.screenshotService?.delegate = uiService - ThemeManager.shared.updateUserInterfaceStyle(window: appContext.window) + window?.windowScene?.screenshotService?.delegate = uiService + ThemeManager.shared.updateUserInterfaceStyle(window: window) // Temporary logic for rollout of Autofill as on by default for new installs only if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { @@ -365,7 +380,7 @@ struct Launched: AppState { AppDependencyProvider.shared.subscriptionManager.loadInitialData() - let autofillPixelReporter = AutofillPixelReporter( + autofillPixelReporter = AutofillPixelReporter( userDefaults: .standard, autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, eventMapping: EventMapping {event, _, params, _ in @@ -390,19 +405,19 @@ struct Launched: AppState { _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, object: nil, - queue: nil) { /*[weak self]*/ _ in - autofillPixelReporter.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) // todo: autofillPixelReporter is local var + queue: nil) { [autofillPixelReporter] _ in + autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) } - if appContext.didCrashDuringCrashHandlersSetUp { + if stateContext.didCrashDuringCrashHandlersSetUp { Pixel.fire(pixel: .crashOnCrashHandlersSetUp) - self.appContext.didCrashDuringCrashHandlersSetUp = false + didCrashDuringCrashHandlersSetUp = false } tipKitAppEventsHandler.appDidFinishLaunching() } - var appDependencies: AppDependencies { + private var appDependencies: AppDependencies { AppDependencies( accountManager: accountManager, vpnWorkaround: vpnWorkaround, @@ -410,23 +425,28 @@ struct Launched: AppState { appSettings: appSettings, privacyStore: privacyStore, uiService: uiService, + mainViewController: mainViewController, voiceSearchHelper: voiceSearchHelper, autoClear: autoClear, autofillLoginSession: autofillLoginSession, marketplaceAdPostbackManager: marketplaceAdPostbackManager, syncService: syncService, + syncDataProviders: syncDataProviders, isSyncInProgressCancellable: isSyncInProgressCancellable, privacyProDataReporter: privacyProDataReporter, remoteMessagingClient: remoteMessagingClient, - subscriptionService: SubscriptionService(subscriptionCookieManager: subscriptionCookieManager), - onboardingPixelReporter: onboardingPixelReporter + subscriptionService: SubscriptionService(subscriptionCookieManager: subscriptionCookieManager, + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager), + onboardingPixelReporter: onboardingPixelReporter, + widgetRefreshModel: widgetRefreshModel, + autofillPixelReporter: autofillPixelReporter ) } private func presentPreemptiveCrashAlert() { Task { @MainActor in let alertController = CriticalAlerts.makePreemptiveCrashAlert() - appContext.window?.rootViewController?.present(alertController, animated: true, completion: nil) + window?.rootViewController?.present(alertController, animated: true, completion: nil) } } @@ -484,12 +504,13 @@ struct Launched: AppState { private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, - currentCookieStore: { //[weak self] in -// guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { // TODO -// // We shouldn't interact with WebKit's cookie store unless we have a WebView, -// // eventually the subscription cookie will be refreshed on opening the first tab -// return nil -// } + currentCookieStore: { + guard let mainViewController = application.window?.rootViewController as? MainViewController, + mainViewController.tabManager.model.hasActiveTabs else { + // We shouldn't interact with WebKit's cookie store unless we have a WebView, + // eventually the subscription cookie will be refreshed on opening the first tab + return nil + } return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) }, eventMapping: SubscriptionCookieManageEventPixelMapping()) @@ -521,9 +542,10 @@ struct Launched: AppState { private func presentInsufficientDiskSpaceAlert() { let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - appContext.window?.rootViewController?.present(alertController, animated: true, completion: nil) + window?.rootViewController?.present(alertController, animated: true, completion: nil) } + private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), appSettings: AppSettings = AppDependencyProvider.shared.appSettings, isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { @@ -561,5 +583,38 @@ struct Launched: AppState { dataStoreIDManager: dataStoreIDManager) } +} + +extension Launched { + + struct StateContext { + + let application: UIApplication + let isTesting: Bool + let didFinishLaunchingStartTime: CFAbsoluteTime + let urlToOpen: URL? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + isTesting: isTesting, + didFinishLaunchingStartTime: didFinishLaunchingStartTime, + urlToOpen: urlToOpen, + appDependencies: appDependencies) + } + +} + +extension UIApplication { + + func setWindow(_ window: UIWindow?) { + (delegate as? AppDelegate)?.window = window + } + + var window: UIWindow? { + delegate?.window ?? nil + } } diff --git a/DuckDuckGo/AppServices/SubscriptionService.swift b/DuckDuckGo/AppServices/SubscriptionService.swift index 538d85795a..6ac3603dcd 100644 --- a/DuckDuckGo/AppServices/SubscriptionService.swift +++ b/DuckDuckGo/AppServices/SubscriptionService.swift @@ -19,14 +19,26 @@ import Subscription import Combine +import BrowserServicesKit final class SubscriptionService { - init(subscriptionCookieManager: SubscriptionCookieManaging) { + let subscriptionCookieManager: SubscriptionCookieManaging + private var cancellables: Set = [] + + var onPrivacyConfigurationUpdate: (() -> Void)? + + init(subscriptionCookieManager: SubscriptionCookieManaging, + privacyConfigurationManager: PrivacyConfigurationManaging) { self.subscriptionCookieManager = subscriptionCookieManager + privacyConfigurationManager.updatesPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.onPrivacyConfigurationUpdate?() + } + .store(in: &cancellables) } - let subscriptionCookieManager: SubscriptionCookieManaging - var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? + } diff --git a/DuckDuckGo/AppServices/UIService.swift b/DuckDuckGo/AppServices/UIService.swift index 84e71de30f..25fd00983e 100644 --- a/DuckDuckGo/AppServices/UIService.swift +++ b/DuckDuckGo/AppServices/UIService.swift @@ -19,16 +19,22 @@ import UIKit -final class UIService: NSObject { // possibly WindowService? +final class UIService: NSObject { var overlayWindow: UIWindow? - var window: UIWindow? + let window: UIWindow + + var showKeyboardIfSettingOn = true // temporary + + init(window: UIWindow) { + self.window = window + } func displayBlankSnapshotWindow(voiceSearchHelper: VoiceSearchHelper, addressBarPosition: AddressBarPosition) { - guard overlayWindow == nil, let frame = window?.frame else { return } + guard overlayWindow == nil else { return } - overlayWindow = UIWindow(frame: frame) + overlayWindow = UIWindow(frame: window.frame) overlayWindow?.windowLevel = UIWindow.Level.alert // TODO: most likely we do not need voiceSearchHelper for BlankSnapshotVC @@ -37,7 +43,7 @@ final class UIService: NSObject { // possibly WindowService? overlayWindow?.rootViewController = overlay overlayWindow?.makeKeyAndVisible() - window?.isHidden = true + window.isHidden = true } func removeOverlay() { @@ -48,7 +54,7 @@ final class UIService: NSObject { // possibly WindowService? if let overlay = overlayWindow { overlay.isHidden = true overlayWindow = nil - window?.makeKeyAndVisible() + window.makeKeyAndVisible() } } @@ -59,6 +65,15 @@ final class UIService: NSObject { // possibly WindowService? } } + func displayAuthenticationWindow() { + guard overlayWindow == nil else { return } + overlayWindow = UIWindow(frame: window.frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + overlayWindow?.rootViewController = AuthenticationViewController.loadFromStoryboard() + overlayWindow?.makeKeyAndVisible() + window.isHidden = true + } + } extension UIService: BlankSnapshotViewRecoveringDelegate { @@ -70,7 +85,7 @@ extension UIService: BlankSnapshotViewRecoveringDelegate { overlayWindow?.isHidden = true overlayWindow = nil - window?.makeKeyAndVisible() + window.makeKeyAndVisible() } } @@ -79,7 +94,7 @@ extension UIService: UIScreenshotServiceDelegate { func screenshotService(_ screenshotService: UIScreenshotService, generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { - guard let mainViewController = window?.rootViewController as? MainViewController, + guard let mainViewController = window.rootViewController as? MainViewController, // todo, will it be needed? let webView = mainViewController.currentTab?.webView else { completionHandler(nil, 0, .zero) return diff --git a/DuckDuckGo/AppServices/UNService.swift b/DuckDuckGo/AppServices/UNService.swift index 4ae0addd37..ae03a30b84 100644 --- a/DuckDuckGo/AppServices/UNService.swift +++ b/DuckDuckGo/AppServices/UNService.swift @@ -24,10 +24,10 @@ import Subscription final class UNService: NSObject { - let window: () -> UIWindow? // possibly non optional + let window: UIWindow let accountManager: AccountManager - init(window: @autoclosure @escaping () -> UIWindow?, + init(window: UIWindow, accountManager: AccountManager) { self.window = window self.accountManager = accountManager @@ -60,15 +60,15 @@ extension UNService: UNUserNotificationCenterDelegate { private func presentNetworkProtectionStatusSettingsModal() { Task { @MainActor in if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { - (window()?.rootViewController as? MainViewController)?.segueToVPN() + (window.rootViewController as? MainViewController)?.segueToVPN() } else { - (window()?.rootViewController as? MainViewController)?.segueToPrivacyPro() + (window.rootViewController as? MainViewController)?.segueToPrivacyPro() } } } private func presentSettings(with viewController: UIViewController) { - guard let window = window(), let rootViewController = window.rootViewController as? MainViewController else { return } + guard let rootViewController = window.rootViewController as? MainViewController else { return } if let navigationController = rootViewController.presentedViewController as? UINavigationController { if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { diff --git a/DuckDuckGo/AutoClear.swift b/DuckDuckGo/AutoClear.swift index 2e1abbc02b..e2e7f93e20 100644 --- a/DuckDuckGo/AutoClear.swift +++ b/DuckDuckGo/AutoClear.swift @@ -34,7 +34,7 @@ protocol AutoClearWorker { class AutoClear { - private let worker: AutoClearWorker + private let worker: AutoClearWorker // shouldn't it be weak? private var timestamp: TimeInterval? private let appSettings: AppSettings From dfd1e967ed3b6dbe0e5a8cd00754bade280cb89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 10 Dec 2024 10:56:03 +0100 Subject: [PATCH 07/27] Testing --- DuckDuckGo/AppDelegate.swift | 6 +++++- .../AppLifecycle/AppStates/Launched.swift | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 64b5252bde..553c50300e 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -134,6 +134,7 @@ import os.log func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { appStateMachine.handle(.launching(application)) + return true didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() defer { if let didFinishLaunchingStartTime { @@ -601,7 +602,7 @@ import os.log guard !testing else { return } appStateMachine.handle(.activating) - + return defer { if let didFinishLaunchingStartTime { let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime @@ -783,6 +784,7 @@ import os.log } func applicationWillEnterForeground(_ application: UIApplication) { + return ThemeManager.shared.updateUserInterfaceStyle() Task { @MainActor in @@ -795,6 +797,7 @@ import os.log func applicationDidEnterBackground(_ application: UIApplication) { appStateMachine.handle(.backgrounding) + return displayBlankSnapshotWindow() autoClear?.startClearingTimer() lastBackgroundDate = Date() @@ -841,6 +844,7 @@ import os.log func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { Logger.sync.debug("App launched with url \(url.absoluteString)") appStateMachine.handle(.openURL(url)) + return true // If showing the onboarding intro ignore deeplinks guard mainViewController?.needsToShowOnboardingIntro() == false else { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 1cda364d23..8c98c26d90 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -74,9 +74,9 @@ struct Launched: AppState { private var isSyncInProgressCancellable: AnyCancellable! private var remoteMessagingClient: RemoteMessagingClient! private var subscriptionCookieManager: SubscriptionCookieManaging! - private var window: UIWindow? private var autofillPixelReporter: AutofillPixelReporter! private var mainViewController: MainViewController! + private var window: UIWindow? var urlToOpen: URL? @@ -155,7 +155,7 @@ struct Launched: AppState { let webView = blockingDelegate.prepareWebView() window!.rootViewController?.view.addSubview(webView) window!.rootViewController?.view.backgroundColor = .red - application.setWindow(window) // to do check if application.delegate?.window is non-nil + application.setWindow(window!) webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) @@ -342,15 +342,15 @@ struct Launched: AppState { mainViewController.loadViewIfNeeded() syncErrorHandler.alertPresenter = mainViewController - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = mainViewController - window.makeKeyAndVisible() - application.setWindow(window) + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = mainViewController + window!.makeKeyAndVisible() + application.setWindow(window!) let autoClear = AutoClear(worker: mainViewController) self.autoClear = autoClear let applicationState = stateContext.application.applicationState - let vpnWorkaround = appDependencies.vpnWorkaround + let vpnWorkaround = vpnWorkaround Task { await autoClear.clearDataIfEnabled(applicationState: .init(with: applicationState)) await vpnWorkaround.installRedditSessionWorkaround() @@ -366,8 +366,8 @@ struct Launched: AppState { UNUserNotificationCenter.current().delegate = unService - window?.windowScene?.screenshotService?.delegate = uiService - ThemeManager.shared.updateUserInterfaceStyle(window: window) + window!.windowScene?.screenshotService?.delegate = uiService + ThemeManager.shared.updateUserInterfaceStyle(window: window!) // Temporary logic for rollout of Autofill as on by default for new installs only if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { From 46621eb223ddeab315eeb4e8bd35df738d168c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 10 Dec 2024 15:39:41 +0100 Subject: [PATCH 08/27] Refactor the code not to use functions on self --- .../AppLifecycle/AppStates/Launched.swift | 176 +++++++++--------- DuckDuckGo/AppServices/UIService.swift | 9 + DuckDuckGo/AppServices/UNService.swift | 26 --- 3 files changed, 93 insertions(+), 118 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 8c98c26d90..12ff557df1 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -62,10 +62,7 @@ struct Launched: AppState { private let isTesting = ProcessInfo().arguments.contains("testing") private let didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() - // These should ideally be `let` properties instead of force unwrapped. However, due to the large amount of code, the compiler cannot track the initialization order. - // As a result, it complains about accessing `self` before all properties are set. - // This is a temporary state and will be gradually phased out as the class is cleaned up and most of the code is moved into services. - // Note: This cleanup is not part of the current milestone. + // These should ideally be let properties instead of force-unwrapped. However, due to various initialization paths, such as database completion blocks, setting them up in advance is currently not feasible. Refactoring will be done once this code is streamlined. private var uiService: UIService! private var unService: UNService! private var syncDataProviders: SyncDataProviders! @@ -110,7 +107,12 @@ struct Launched: AppState { Pixel.isDryRun = false #endif - ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert + ContentBlocking.shared.onCriticalError = { [application] in + Task { @MainActor [application] in + let alertController = CriticalAlerts.makePreemptiveCrashAlert() + application.window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + } // Explicitly prepare ContentBlockingUpdating instance before Tabs are created _ = ContentBlockingUpdating.shared @@ -118,6 +120,14 @@ struct Launched: AppState { cleanUpMacPromoExperiment2() cleanUpIncrementalRolloutPixelTest() + func cleanUpMacPromoExperiment2() { + UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") + } + + func cleanUpIncrementalRolloutPixelTest() { + UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") + } + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { @@ -141,6 +151,15 @@ struct Launched: AppState { clearTmp() + func clearTmp() { + let tmp = FileManager.default.temporaryDirectory + do { + try FileManager.default.removeItem(at: tmp) + } catch { + Logger.general.error("Failed to delete tmp dir") + } + } + _ = DefaultUserAgentManager.shared if isTesting { Pixel.isDryRun = true @@ -167,6 +186,18 @@ struct Launched: AppState { removeEmailWaitlistState() + func removeEmailWaitlistState() { + EmailWaitlist.removeEmailState() + + let autofillStorage = EmailKeychainManager() + try? autofillStorage.deleteWaitlistState() + + // Remove the authentication state if this is a fresh install. + if !Database.shared.isDatabaseFileInitialized { + try? autofillStorage.deleteAuthenticationState() + } + } + var shouldPresentInsufficientDiskSpaceAlertAndCrash = false Database.shared.loadStore { context, error in guard let context = context else { @@ -226,7 +257,34 @@ struct Launched: AppState { // assign it here, because "did become active" is already too late and "viewWillAppear" // has already been called on the HomeViewController so won't show the home row CTA - cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + cleanUpATBAndAssignVariant(variantManager: variantManager, + daxDialogs: daxDialogs, + marketplaceAdPostbackManager: marketplaceAdPostbackManager) + + func cleanUpATBAndAssignVariant(variantManager: VariantManager, + daxDialogs: DaxDialogs, + marketplaceAdPostbackManager: MarketplaceAdPostbackManager) { + let historyMessageManager = HistoryMessageManager() + + AtbAndVariantCleanup.cleanup() + variantManager.assignVariantIfNeeded { _ in + let launchOptionsHandler = LaunchOptionsHandler() + + // MARK: perform first time launch logic here + // If it's running UI Tests check if the onboarding should be in a completed state. + if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { + daxDialogs.dismiss() + } else { + daxDialogs.primeForUse() + } + + // New users don't see the message + historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() + } + } // MARK: Sync initialisation #if DEBUG @@ -289,7 +347,7 @@ struct Launched: AppState { privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, purchasePlatform: .appStore) - subscriptionCookieManager = makeSubscriptionCookieManager() + subscriptionCookieManager = Self.makeSubscriptionCookieManager(application: application) let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager let homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, @@ -298,8 +356,8 @@ struct Launched: AppState { let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() - let tabsModel = prepareTabsModel(previewsSource: previewsSource) + let historyManager = Self.makeHistoryManager() + let tabsModel = Self.prepareTabsModel(previewsSource: previewsSource) privacyProDataReporter.injectTabsModel(tabsModel) @@ -311,7 +369,7 @@ struct Launched: AppState { window!.makeKeyAndVisible() application.setWindow(window) - presentInsufficientDiskSpaceAlert() + window!.presentInsufficientDiskSpaceAlert() } else { let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) @@ -335,8 +393,8 @@ struct Launched: AppState { featureFlagger: AppDependencyProvider.shared.featureFlagger, fireproofing: fireproofing, subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: makeTextZoomCoordinator(), - websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + textZoomCoordinator: Self.makeTextZoomCoordinator(), + websiteDataManager: Self.makeWebsiteDataManager(fireproofing: fireproofing), appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) mainViewController.loadViewIfNeeded() @@ -443,66 +501,7 @@ struct Launched: AppState { ) } - private func presentPreemptiveCrashAlert() { - Task { @MainActor in - let alertController = CriticalAlerts.makePreemptiveCrashAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - } - - private func cleanUpMacPromoExperiment2() { - UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") - } - - private func cleanUpIncrementalRolloutPixelTest() { - UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") - } - - private func clearTmp() { - let tmp = FileManager.default.temporaryDirectory - do { - try FileManager.default.removeItem(at: tmp) - } catch { - Logger.general.error("Failed to delete tmp dir") - } - } - - private func removeEmailWaitlistState() { - EmailWaitlist.removeEmailState() - - let autofillStorage = EmailKeychainManager() - try? autofillStorage.deleteWaitlistState() - - // Remove the authentication state if this is a fresh install. - if !Database.shared.isDatabaseFileInitialized { - try? autofillStorage.deleteAuthenticationState() - } - } - - private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { - let historyMessageManager = HistoryMessageManager() - - AtbAndVariantCleanup.cleanup() - variantManager.assignVariantIfNeeded { _ in - let launchOptionsHandler = LaunchOptionsHandler() - - // MARK: perform first time launch logic here - // If it's running UI Tests check if the onboarding should be in a completed state. - if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { - daxDialogs.dismiss() - } else { - daxDialogs.primeForUse() - } - - // New users don't see the message - historyMessageManager.dismiss() - - // Setup storage for marketplace postback - marketplaceAdPostbackManager.updateReturningUserValue() - } - } - - private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { + private static func makeSubscriptionCookieManager(application: UIApplication) -> SubscriptionCookieManaging { let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, currentCookieStore: { guard let mainViewController = application.window?.rootViewController as? MainViewController, @@ -517,10 +516,8 @@ struct Launched: AppState { return subscriptionCookieManager } - private func makeHistoryManager() -> HistoryManaging { - + private static func makeHistoryManager() -> HistoryManaging { let provider = AppDependencyProvider.shared - switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, @@ -528,11 +525,12 @@ struct Launched: AppState { case .failure(let error): Pixel.fire(pixel: .historyStoreLoadFailed, error: error) - if error.isDiskFull { - self.presentInsufficientDiskSpaceAlert() - } else { - self.presentPreemptiveCrashAlert() - } +// Commenting out as it didn't work anyway - the window was just always nil at this point +// if error.isDiskFull { +// self.presentInsufficientDiskSpaceAlert() +// } else { +// self.presentPreemptiveCrashAlert() +// } return NullHistoryManager() case .success(let historyManager): @@ -540,15 +538,9 @@ struct Launched: AppState { } } - private func presentInsufficientDiskSpaceAlert() { - let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - - - private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), - appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { + private static func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), + appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad let tabsModel: TabsModel if AutoClearSettingsModel(settings: appSettings) != nil { @@ -567,7 +559,7 @@ struct Launched: AppState { return tabsModel } - private func makeTextZoomCoordinator() -> TextZoomCoordinator { + private static func makeTextZoomCoordinator() -> TextZoomCoordinator { let provider = AppDependencyProvider.shared let storage = TextZoomStorage() @@ -576,8 +568,8 @@ struct Launched: AppState { featureFlagger: provider.featureFlagger) } - private func makeWebsiteDataManager(fireproofing: Fireproofing, - dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + private static func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { return WebCacheManager(cookieStorage: MigratableCookieStorage(), fireproofing: fireproofing, dataStoreIDManager: dataStoreIDManager) diff --git a/DuckDuckGo/AppServices/UIService.swift b/DuckDuckGo/AppServices/UIService.swift index 25fd00983e..11ddf04000 100644 --- a/DuckDuckGo/AppServices/UIService.swift +++ b/DuckDuckGo/AppServices/UIService.swift @@ -76,6 +76,15 @@ final class UIService: NSObject { } +extension UIWindow { + + func presentInsufficientDiskSpaceAlert() { + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + rootViewController?.present(alertController, animated: true, completion: nil) + } + +} + extension UIService: BlankSnapshotViewRecoveringDelegate { func recoverFromPresenting(controller: BlankSnapshotViewController) { diff --git a/DuckDuckGo/AppServices/UNService.swift b/DuckDuckGo/AppServices/UNService.swift index ae03a30b84..f7874d03f7 100644 --- a/DuckDuckGo/AppServices/UNService.swift +++ b/DuckDuckGo/AppServices/UNService.swift @@ -67,30 +67,4 @@ extension UNService: UNUserNotificationCenterDelegate { } } - private func presentSettings(with viewController: UIViewController) { - guard let rootViewController = window.rootViewController as? MainViewController else { return } - - if let navigationController = rootViewController.presentedViewController as? UINavigationController { - if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { - // Avoid presenting dismissing and re-presenting the view controller if it's already visible: - return - } else { - // Otherwise, replace existing view controllers with the presented one: - navigationController.popToRootViewController(animated: false) - navigationController.pushViewController(viewController, animated: false) - return - } - } - - // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: - rootViewController.clearNavigationStack() - - // Give the `clearNavigationStack` call time to complete. - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { - rootViewController.segueToSettings() - let navigationController = rootViewController.presentedViewController as? UINavigationController - navigationController?.popToRootViewController(animated: false) - navigationController?.pushViewController(viewController, animated: false) - } - } } From b64e651aafd0275993886d5a10dfa3b896ecf260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 11 Dec 2024 17:09:48 +0100 Subject: [PATCH 09/27] Add code to Launch that has been added on main the meantime --- DuckDuckGo/AppDelegate.swift | 11 ++- DuckDuckGo/AppDependencies.swift | 1 + .../AppLifecycle/AppStates/Launched.swift | 73 ++++++++++++++++--- DuckDuckGo/AppServices/UIService.swift | 9 --- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 553c50300e..013e301afb 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -121,12 +121,11 @@ import os.log override init() { super.init() - - if !didCrashDuringCrashHandlersSetUp { - didCrashDuringCrashHandlersSetUp = true - CrashLogMessageExtractor.setUp(swapCxaThrow: false) - didCrashDuringCrashHandlersSetUp = false - } +// if !didCrashDuringCrashHandlersSetUp { +// didCrashDuringCrashHandlersSetUp = true +// CrashLogMessageExtractor.setUp(swapCxaThrow: false) +// didCrashDuringCrashHandlersSetUp = false +// } } // swiftlint:disable:next function_body_length diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift index f402a3c18e..409e6498b1 100644 --- a/DuckDuckGo/AppDependencies.swift +++ b/DuckDuckGo/AppDependencies.swift @@ -51,5 +51,6 @@ struct AppDependencies { let onboardingPixelReporter: OnboardingPixelReporter let widgetRefreshModel: NetworkProtectionWidgetRefreshModel let autofillPixelReporter: AutofillPixelReporter + let crashReportUploaderOnboarding: CrashCollectionOnboarding } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 12ff557df1..87dd289562 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -61,6 +61,7 @@ struct Launched: AppState { private let privacyProDataReporter: PrivacyProDataReporting private let isTesting = ProcessInfo().arguments.contains("testing") private let didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + private let crashReportUploaderOnboarding: CrashCollectionOnboarding // These should ideally be let properties instead of force-unwrapped. However, due to various initialization paths, such as database completion blocks, setting them up in advance is currently not feasible. Refactoring will be done once this code is streamlined. private var uiService: UIService! @@ -77,12 +78,13 @@ struct Launched: AppState { var urlToOpen: URL? - let application: UIApplication + private let application: UIApplication init(stateContext: Init.StateContext) { application = stateContext.application privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) + crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) defer { let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime @@ -136,15 +138,24 @@ struct Launched: AppState { Configuration.setURLProvider(AppConfigurationURLProvider()) } - crashCollection.startAttachingCrashLogMessages { [application] pixelParameters, payloads, sendReport in + crashCollection.startAttachingCrashLogMessages { [application, crashReportUploaderOnboarding] pixelParameters, payloads, sendReport in pixelParameters.forEach { params in Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) + + // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. + // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. + // If for some reason the parameter can't be found, fall back to the current version. + if let crashAppVersion = params[PixelParameters.appVersion] { + let dailyParameters = [PixelParameters.appVersion: crashAppVersion] +// DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) //todo uncomment after merge + } else { +// DailyPixel.fireDaily(.dbCrashDetectedDaily) //todo uncomment after merge + } } // Async dispatch because rootViewController may otherwise be nil here DispatchQueue.main.async { guard let viewController = application.window?.rootViewController else { return } // todo: check if it shows - let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) } } @@ -174,10 +185,10 @@ struct Launched: AppState { let webView = blockingDelegate.prepareWebView() window!.rootViewController?.view.addSubview(webView) window!.rootViewController?.view.backgroundColor = .red - application.setWindow(window!) - webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + application.setWindow(window!) + let request = URLRequest(url: URL(string: "about:blank")!) webView.load(request) @@ -300,6 +311,33 @@ struct Launched: AppState { ).wrappedValue ) ?? defaultEnvironment + var dryRun = false +#if DEBUG + dryRun = true +#endif + let isPhone = UIDevice.current.userInterfaceIdiom == .phone +// let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS +// PixelKit.setUp(dryRun: dryRun, +// appVersion: AppVersion.shared.versionNumber, +// source: source.rawValue, +// defaultHeaders: [:], +// defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in +// +// let url = URL.pixelUrl(forPixelNamed: pixelName) +// let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) +// let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) +// Task { +// do { +// _ = try await DefaultAPIService().fetch(request: request) +// onComplete(true, nil) +// } catch { +// onComplete(false, error) +// } +// } +// } +// PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, +// eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + let syncErrorHandler = SyncErrorHandler() syncDataProviders = SyncDataProviders( @@ -308,7 +346,8 @@ struct Launched: AppState { settingHandlers: [FavoritesDisplayModeSyncHandler()], favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared + faviconStoring: Favicons.shared//, todo: comment out after merge +// tld: AppDependencyProvider.shared.storageCache.tld comment out after merge ) syncService = DDGSync( @@ -348,7 +387,6 @@ struct Launched: AppState { purchasePlatform: .appStore) subscriptionCookieManager = Self.makeSubscriptionCookieManager(application: application) - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager let homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, remoteMessagingClient: remoteMessagingClient, @@ -369,7 +407,11 @@ struct Launched: AppState { window!.makeKeyAndVisible() application.setWindow(window) - window!.presentInsufficientDiskSpaceAlert() + presentInsufficientDiskSpaceAlert() + func presentInsufficientDiskSpaceAlert() { + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + window!.rootViewController?.present(alertController, animated: true, completion: nil) + } } else { let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) @@ -407,7 +449,7 @@ struct Launched: AppState { let autoClear = AutoClear(worker: mainViewController) self.autoClear = autoClear - let applicationState = stateContext.application.applicationState + let applicationState = application.applicationState let vpnWorkaround = vpnWorkaround Task { await autoClear.clearDataIfEnabled(applicationState: .init(with: applicationState)) @@ -416,6 +458,7 @@ struct Launched: AppState { } unService = UNService(window: window!, accountManager: accountManager) uiService = UIService(window: window!) + voiceSearchHelper.migrateSettingsFlagIfNecessary() // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. @@ -438,6 +481,7 @@ struct Launched: AppState { AppDependencyProvider.shared.subscriptionManager.loadInitialData() + let autofillUsageMonitor = AutofillUsageMonitor() autofillPixelReporter = AutofillPixelReporter( userDefaults: .standard, autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, @@ -451,8 +495,16 @@ struct Launched: AppState { Pixel.fire(pixel: .autofillOnboardedUser) case .autofillToggledOn: Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) +// if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled {// todo: uncomment after merge +// Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, +// withAdditionalParameters: params ?? [:]) +// } case .autofillToggledOff: Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) +// if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled {// todo: uncomment after merge +// Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, +// withAdditionalParameters: params ?? [:]) +// } case .autofillLoginsStacked: Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) default: @@ -497,7 +549,8 @@ struct Launched: AppState { privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager), onboardingPixelReporter: onboardingPixelReporter, widgetRefreshModel: widgetRefreshModel, - autofillPixelReporter: autofillPixelReporter + autofillPixelReporter: autofillPixelReporter, + crashReportUploaderOnboarding: crashReportUploaderOnboarding ) } diff --git a/DuckDuckGo/AppServices/UIService.swift b/DuckDuckGo/AppServices/UIService.swift index 11ddf04000..25fd00983e 100644 --- a/DuckDuckGo/AppServices/UIService.swift +++ b/DuckDuckGo/AppServices/UIService.swift @@ -76,15 +76,6 @@ final class UIService: NSObject { } -extension UIWindow { - - func presentInsufficientDiskSpaceAlert() { - let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - rootViewController?.present(alertController, animated: true, completion: nil) - } - -} - extension UIService: BlankSnapshotViewRecoveringDelegate { func recoverFromPresenting(controller: BlankSnapshotViewController) { From 8e69357576330b98809af9409767d2aa8e76c032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 11 Dec 2024 18:29:30 +0100 Subject: [PATCH 10/27] Fix subscription code --- DuckDuckGo/AppLifecycle/AppStates/Active.swift | 11 +++-------- DuckDuckGo/AppLifecycle/AppStates/Launched.swift | 7 +++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 6778d43dd3..1064c4b2ce 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -53,11 +53,6 @@ struct Active: AppState { withAdditionalParameters: [PixelParameters.time: String(launchTime)]) } - // Enable subscriptionCookieManager if feature flag is present - if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { - appDependencies.subscriptionService.subscriptionCookieManager.enableSettingSubscriptionCookie() - } - // Keep track of feature flag changes let subscriptionCookieManager = appDependencies.subscriptionService.subscriptionCookieManager appDependencies.subscriptionService.onPrivacyConfigurationUpdate = { [privacyConfigurationManager] in @@ -73,7 +68,7 @@ struct Active: AppState { } // onApplicationLaunch code - Task { @MainActor [self] in // todo? is capturing self here ok? + Task { @MainActor [self] in // is capturing self here ok? await beginAuthentication() initialiseBackgroundFetch(application) applyAppearanceChanges() @@ -207,11 +202,11 @@ struct Active: AppState { Logger.sync.debug("App launched with url \(url.absoluteString)") // If showing the onboarding intro ignore deeplinks guard mainViewController.needsToShowOnboardingIntro() == false else { - return // todo was return false + return } if handleEmailSignUpDeepLink(url) { - return // todo was return true + return } NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 87dd289562..6d6e3d49c6 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -566,6 +566,13 @@ struct Launched: AppState { return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) }, eventMapping: SubscriptionCookieManageEventPixelMapping()) + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + return subscriptionCookieManager } From 33e7bde75686fd5a95997e35281ecbfd2ac7051f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 11 Dec 2024 18:34:38 +0100 Subject: [PATCH 11/27] Fix sync cancellable --- DuckDuckGo/AppDelegate.swift | 4 ++++ DuckDuckGo/AppDependencies.swift | 2 ++ DuckDuckGo/AppLifecycle/AppStates/Background.swift | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 013e301afb..586848e88a 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -706,6 +706,10 @@ import os.log func applicationWillResignActive(_ application: UIApplication) { appStateMachine.handle(.suspending) + Task { @MainActor in + await refreshShortcuts() + await vpnWorkaround.removeRedditSessionWorkaround() + } } private func fireAppLaunchPixel() { diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift index 409e6498b1..4c3cb5df0e 100644 --- a/DuckDuckGo/AppDependencies.swift +++ b/DuckDuckGo/AppDependencies.swift @@ -53,4 +53,6 @@ struct AppDependencies { let autofillPixelReporter: AutofillPixelReporter let crashReportUploaderOnboarding: CrashCollectionOnboarding + var syncDidFinishCancellable: AnyCancellable? + } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 8124e950c9..cfb604c42b 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -27,12 +27,14 @@ struct Background: AppState { private let lastBackgroundDate: Date = Date() private let application: UIApplication private let appDependencies: AppDependencies - private var syncDidFinishCancellable: AnyCancellable? // TODO: should we pass it through appDependencies to sustain its lifecycle? perhaps we should have a registry to register it + private var syncDidFinishCancellable: AnyCancellable? + var urlToOpen: URL? init(stateContext: Inactive.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies + syncDidFinishCancellable = appDependencies.syncDidFinishCancellable let autoClear = appDependencies.autoClear let privacyStore = appDependencies.privacyStore From ac486b4aff6097fad361b94596c29b1ea4f1fe34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 11 Dec 2024 18:48:56 +0100 Subject: [PATCH 12/27] Make it buildable after merge --- .../AppLifecycle/AppStateTransitions.swift | 51 +++++++------ .../AppLifecycle/AppStates/Active.swift | 6 +- .../AppLifecycle/AppStates/Background.swift | 2 +- .../AppLifecycle/AppStates/Inactive.swift | 4 ++ .../AppLifecycle/AppStates/Launched.swift | 72 ++++++++++--------- 5 files changed, 73 insertions(+), 62 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 9e726a642d..2fdc86e6e4 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -57,7 +57,7 @@ extension Active { switch event { case .suspending: return Inactive(stateContext: makeStateContext()) - case .openURL(let url): // TODO: update to tech design + case .openURL(let url): openURL(url) return self case .launching, .activating, .backgrounding: @@ -69,13 +69,16 @@ extension Active { extension Inactive { - func apply(event: AppEvent) -> any AppState { + mutating func apply(event: AppEvent) -> any AppState { switch event { case .backgrounding: return Background(stateContext: makeStateContext()) case .activating: return Active(stateContext: makeStateContext()) - case .launching, .suspending, .openURL: + case .openURL(let url): + urlToOpen = url + return self + case .launching, .suspending: return handleUnexpectedEvent(event) } } @@ -104,16 +107,17 @@ extension DoubleBackground { func apply(event: AppEvent) -> any AppState { // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - _ = handleUnexpectedEvent(event) - - switch event { - case .activating(let application): - return Active(application: application) - case .suspending(let application): - return Inactive(application: application) - case .launching, .backgrounding, .openURL: - return self - } + handleUnexpectedEvent(event) + + //todo: to be removed +// switch event { +// case .activating(let application): +// return Active(application: application) +// case .suspending(let application): +// return Inactive(application: application) +// case .launching, .backgrounding, .openURL: +// return self +// } } @@ -123,16 +127,17 @@ extension InactiveBackground { func apply(event: AppEvent) -> any AppState { // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - _ = handleUnexpectedEvent(event) - - switch event { - case .activating(let application): - return Active(application: application) - case .suspending(let application): - return Inactive(application: application) - case .launching, .backgrounding, .openURL: - return self - } + handleUnexpectedEvent(event) + + //todo: to be removed +// switch event { +// case .activating(let application): +// return Active(application: application) +// case .suspending(let application): +// return Inactive(application: application) +// case .launching, .backgrounding, .openURL: +// return self +// } } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 1064c4b2ce..6ff2fe29b6 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -92,7 +92,7 @@ struct Active: AppState { let uiService = appDependencies.uiService let syncService = appDependencies.syncService let autoClear = appDependencies.autoClear - Task { @MainActor [self] in // todo? is capturing self here ok? + Task { @MainActor [self] in // is capturing self here ok? await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) uiService.showKeyboardIfSettingOn = true @@ -198,7 +198,7 @@ struct Active: AppState { } // MARK: handle application(_:open:options:) logic here - func openURL(_ url: URL) { // TODO: should we reset URL? probably not + func openURL(_ url: URL) { Logger.sync.debug("App launched with url \(url.absoluteString)") // If showing the onboarding intro ignore deeplinks guard mainViewController.needsToShowOnboardingIntro() == false else { @@ -335,7 +335,7 @@ struct Active: AppState { } private func presentExpiredEntitlementAlert() { - let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak mainViewController] in //todo? + let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak mainViewController] in mainViewController?.segueToPrivacyPro() } window.rootViewController?.present(alertController, animated: true) { [weak tunnelDefaults] in diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 3e44956b59..da5f4f1571 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -34,6 +34,7 @@ struct Background: AppState { init(stateContext: Inactive.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies + urlToOpen = stateContext.urlToOpen syncDidFinishCancellable = appDependencies.syncDidFinishCancellable let autoClear = appDependencies.autoClear @@ -83,7 +84,6 @@ struct Background: AppState { } private func resetAppStartTime() { -// didFinishLaunchingStartTime = nil // TODO: not needed most likely appDependencies.mainViewController.appDidFinishLaunchingStartTime = nil } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index 86fafb506f..10837bd2c8 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -24,6 +24,8 @@ struct Inactive: AppState { private let application: UIApplication private let appDependencies: AppDependencies + var urlToOpen: URL? + init(stateContext: Active.StateContext) { application = stateContext.application appDependencies = stateContext.appDependencies @@ -45,12 +47,14 @@ extension Inactive { struct StateContext { let application: UIApplication + let urlToOpen: URL? let appDependencies: AppDependencies } func makeStateContext() -> StateContext { .init(application: application, + urlToOpen: urlToOpen, appDependencies: appDependencies) } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 6d6e3d49c6..8b1dbfc795 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -32,6 +32,8 @@ import Subscription import WebKit import Common import Combine +import PixelKit +import PixelExperimentKit @MainActor struct Launched: AppState { @@ -147,15 +149,15 @@ struct Launched: AppState { // If for some reason the parameter can't be found, fall back to the current version. if let crashAppVersion = params[PixelParameters.appVersion] { let dailyParameters = [PixelParameters.appVersion: crashAppVersion] -// DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) //todo uncomment after merge + DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) } else { -// DailyPixel.fireDaily(.dbCrashDetectedDaily) //todo uncomment after merge + DailyPixel.fireDaily(.dbCrashDetectedDaily) } } // Async dispatch because rootViewController may otherwise be nil here DispatchQueue.main.async { - guard let viewController = application.window?.rootViewController else { return } // todo: check if it shows + guard let viewController = application.window?.rootViewController else { return } crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) } } @@ -273,7 +275,7 @@ struct Launched: AppState { marketplaceAdPostbackManager: marketplaceAdPostbackManager) func cleanUpATBAndAssignVariant(variantManager: VariantManager, - daxDialogs: DaxDialogs, + daxDialogs: DaxDialogs, marketplaceAdPostbackManager: MarketplaceAdPostbackManager) { let historyMessageManager = HistoryMessageManager() @@ -316,27 +318,27 @@ struct Launched: AppState { dryRun = true #endif let isPhone = UIDevice.current.userInterfaceIdiom == .phone -// let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS -// PixelKit.setUp(dryRun: dryRun, -// appVersion: AppVersion.shared.versionNumber, -// source: source.rawValue, -// defaultHeaders: [:], -// defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in -// -// let url = URL.pixelUrl(forPixelNamed: pixelName) -// let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) -// let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) -// Task { -// do { -// _ = try await DefaultAPIService().fetch(request: request) -// onComplete(true, nil) -// } catch { -// onComplete(false, error) -// } -// } -// } -// PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, -// eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: source.rawValue, + defaultHeaders: [:], + defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) + let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) + Task { + do { + _ = try await DefaultAPIService().fetch(request: request) + onComplete(true, nil) + } catch { + onComplete(false, error) + } + } + } + PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, + eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) let syncErrorHandler = SyncErrorHandler() @@ -346,8 +348,8 @@ struct Launched: AppState { settingHandlers: [FavoritesDisplayModeSyncHandler()], favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared//, todo: comment out after merge -// tld: AppDependencyProvider.shared.storageCache.tld comment out after merge + faviconStoring: Favicons.shared, + tld: AppDependencyProvider.shared.storageCache.tld ) syncService = DDGSync( @@ -495,16 +497,16 @@ struct Launched: AppState { Pixel.fire(pixel: .autofillOnboardedUser) case .autofillToggledOn: Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) -// if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled {// todo: uncomment after merge -// Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, -// withAdditionalParameters: params ?? [:]) -// } + if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } case .autofillToggledOff: Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) -// if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled {// todo: uncomment after merge -// Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, -// withAdditionalParameters: params ?? [:]) -// } + if let autofillExtensionToggled = autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } case .autofillLoginsStacked: Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) default: From 017d6555c74749bd6f588606bc4283c17ee264aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 12 Dec 2024 16:47:14 +0100 Subject: [PATCH 13/27] Make appdelegate work in dual mode and handle shortcuts --- DuckDuckGo/AppDelegate.swift | 881 +++++++++--------- DuckDuckGo/AppLifecycle/AppStateMachine.swift | 1 + .../AppLifecycle/AppStateTransitions.swift | 11 +- .../AppLifecycle/AppStates/Active.swift | 29 + .../AppLifecycle/AppStates/Launched.swift | 3 + DuckDuckGo/AppSettings.swift | 2 + DuckDuckGo/AppUserDefaults.swift | 15 +- 7 files changed, 522 insertions(+), 420 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index dae49a9647..fbbd353dc5 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -41,6 +41,13 @@ import PixelExperimentKit import WebKit import os.log +enum AppBehavior: String { + + case existing + case stateMachine + +} + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) @@ -51,14 +58,14 @@ import os.log } private var testing = false - var appIsLaunching = false - var overlayWindow: UIWindow? + private var appIsLaunching = false + private var overlayWindow: UIWindow? var window: UIWindow? private lazy var privacyStore = PrivacyUserDefaults() - private var bookmarksDatabase: CoreDataDatabase = BookmarksDatabase.make() + private var bookmarksDatabase: CoreDataDatabase! - private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() + private var widgetRefreshModel: NetworkProtectionWidgetRefreshModel! private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults @MainActor @@ -83,20 +90,20 @@ import os.log private var syncStateCancellable: AnyCancellable? private var isSyncInProgressCancellable: AnyCancellable? - private let crashCollection = CrashCollection(platform: .iOS) + private var crashCollection: CrashCollection! private var crashReportUploaderOnboarding: CrashCollectionOnboarding? private var autofillPixelReporter: AutofillPixelReporter? - private var autofillUsageMonitor = AutofillUsageMonitor() + private var autofillUsageMonitor: AutofillUsageMonitor! private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! private var subscriptionCookieManager: SubscriptionCookieManaging! private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? - var privacyProDataReporter: PrivacyProDataReporting! + private var privacyProDataReporter: PrivacyProDataReporting! // MARK: - Feature specific app event handlers - private let tipKitAppEventsHandler = TipKitAppEventHandler() + private var tipKitAppEventsHandler: TipKitAppEventHandler! // MARK: lifecycle @@ -110,378 +117,403 @@ import os.log @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) private var didCrashDuringCrashHandlersSetUp: Bool - private let launchOptionsHandler = LaunchOptionsHandler() - private let onboardingPixelReporter = OnboardingPixelReporter() - - private let voiceSearchHelper = VoiceSearchHelper() + private var launchOptionsHandler: LaunchOptionsHandler! + private var onboardingPixelReporter: OnboardingPixelReporter! - private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() + private var voiceSearchHelper: VoiceSearchHelper! + private var marketplaceAdPostbackManager: MarketplaceAdPostbackManager! private var didFinishLaunchingStartTime: CFAbsoluteTime? private let appStateMachine: AppStateMachine = AppStateMachine() + private let appBehavior: AppBehavior = { + if let appBehavior = AppDependencyProvider.shared.appSettings.appBehavior { + return appBehavior + } + let appBehavior: AppBehavior = Double.random(in: 0..<1) < 0.2 ? .stateMachine : .existing // 20% of users will run through new flow + AppDependencyProvider.shared.appSettings.appBehavior = appBehavior + return appBehavior + }() override init() { super.init() -// if !didCrashDuringCrashHandlersSetUp { -// didCrashDuringCrashHandlersSetUp = true -// CrashLogMessageExtractor.setUp(swapCxaThrow: false) -// didCrashDuringCrashHandlersSetUp = false -// } + + if appBehavior == .existing { + + bookmarksDatabase = BookmarksDatabase.make() + widgetRefreshModel = NetworkProtectionWidgetRefreshModel() + crashCollection = CrashCollection(platform: .iOS) + autofillUsageMonitor = AutofillUsageMonitor() + tipKitAppEventsHandler = TipKitAppEventHandler() + launchOptionsHandler = LaunchOptionsHandler() + onboardingPixelReporter = OnboardingPixelReporter() + voiceSearchHelper = VoiceSearchHelper() + marketplaceAdPostbackManager = MarketplaceAdPostbackManager() + + + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp(swapCxaThrow: false) + didCrashDuringCrashHandlersSetUp = false + } + } + } // swiftlint:disable:next function_body_length // swiftlint:disable:next cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - appStateMachine.handle(.launching(application)) - return true - didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + if appBehavior == .stateMachine { + appStateMachine.handle(.launching(application)) + return true + } else { + didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } } - } #if targetEnvironment(simulator) - if ProcessInfo.processInfo.environment["UITESTING"] == "true" { - // Disable hardware keyboards. - let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") - UITextInputMode.activeInputModes - // Filter `UIKeyboardInputMode`s. - .filter({ $0.responds(to: setHardwareLayout) }) - .forEach { $0.perform(setHardwareLayout, with: nil) } - } + if ProcessInfo.processInfo.environment["UITESTING"] == "true" { + // Disable hardware keyboards. + let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") + UITextInputMode.activeInputModes + // Filter `UIKeyboardInputMode`s. + .filter({ $0.responds(to: setHardwareLayout) }) + .forEach { $0.perform(setHardwareLayout, with: nil) } + } #endif #if DEBUG - Pixel.isDryRun = true + Pixel.isDryRun = true #else - Pixel.isDryRun = false + Pixel.isDryRun = false #endif - ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert - // Explicitly prepare ContentBlockingUpdating instance before Tabs are created - _ = ContentBlockingUpdating.shared + ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert + // Explicitly prepare ContentBlockingUpdating instance before Tabs are created + _ = ContentBlockingUpdating.shared - // Can be removed after a couple of versions - cleanUpMacPromoExperiment2() - cleanUpIncrementalRolloutPixelTest() + // Can be removed after a couple of versions + cleanUpMacPromoExperiment2() + cleanUpIncrementalRolloutPixelTest() - APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) - if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { - Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) - } else { - Configuration.setURLProvider(AppConfigurationURLProvider()) - } + if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } - crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in - pixelParameters.forEach { params in - Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) + crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + pixelParameters.forEach { params in + Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) - // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. - // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. - // If for some reason the parameter can't be found, fall back to the current version. - if let crashAppVersion = params[PixelParameters.appVersion] { - let dailyParameters = [PixelParameters.appVersion: crashAppVersion] - DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) - } else { - DailyPixel.fireDaily(.dbCrashDetectedDaily) + // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. + // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. + // If for some reason the parameter can't be found, fall back to the current version. + if let crashAppVersion = params[PixelParameters.appVersion] { + let dailyParameters = [PixelParameters.appVersion: crashAppVersion] + DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) + } else { + DailyPixel.fireDaily(.dbCrashDetectedDaily) + } } - } - // Async dispatch because rootViewController may otherwise be nil here - DispatchQueue.main.async { - guard let viewController = self.window?.rootViewController else { return } + // Async dispatch because rootViewController may otherwise be nil here + DispatchQueue.main.async { + guard let viewController = self.window?.rootViewController else { return } - let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) - crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) - self.crashReportUploaderOnboarding = crashReportUploaderOnboarding + let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) + self.crashReportUploaderOnboarding = crashReportUploaderOnboarding + } } - } - clearTmp() + clearTmp() - _ = DefaultUserAgentManager.shared - testing = ProcessInfo().arguments.contains("testing") - if testing { - Pixel.isDryRun = true _ = DefaultUserAgentManager.shared - Database.shared.loadStore { _, _ in } - _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + testing = ProcessInfo().arguments.contains("testing") + if testing { + Pixel.isDryRun = true + _ = DefaultUserAgentManager.shared + Database.shared.loadStore { _, _ in } + _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + + let blockingDelegate = BlockingNavigationDelegate() + let webView = blockingDelegate.prepareWebView() + window?.rootViewController?.view.addSubview(webView) + window?.rootViewController?.view.backgroundColor = .red + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + + let request = URLRequest(url: URL(string: "about:blank")!) + webView.load(request) + + return true + } - let blockingDelegate = BlockingNavigationDelegate() - let webView = blockingDelegate.prepareWebView() - window?.rootViewController?.view.addSubview(webView) - window?.rootViewController?.view.backgroundColor = .red - webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + removeEmailWaitlistState() - let request = URLRequest(url: URL(string: "about:blank")!) - webView.load(request) + var shouldPresentInsufficientDiskSpaceAlertAndCrash = false + Database.shared.loadStore { context, error in + guard let context = context else { - return true - } + let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] - removeEmailWaitlistState() - - var shouldPresentInsufficientDiskSpaceAlertAndCrash = false - Database.shared.loadStore { context, error in - guard let context = context else { - - let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", - PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] - - switch error { - case .none: - fatalError("Could not create database stack: Unknown Error") - case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): - Pixel.fire(pixel: .dbContainerInitializationError, - error: underlyingError, - withAdditionalParameters: parameters) - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(underlyingError.localizedDescription)") - case .some(let error): - Pixel.fire(pixel: .dbInitializationError, - error: error, - withAdditionalParameters: parameters) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - return - } else { + switch error { + case .none: + fatalError("Could not create database stack: Unknown Error") + case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): + Pixel.fire(pixel: .dbContainerInitializationError, + error: underlyingError, + withAdditionalParameters: parameters) Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") + fatalError("Could not create database stack: \(underlyingError.localizedDescription)") + case .some(let error): + Pixel.fire(pixel: .dbInitializationError, + error: error, + withAdditionalParameters: parameters) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + return + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } } } + DatabaseMigration.migrate(to: context) } - DatabaseMigration.migrate(to: context) - } - switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { - case .success: - break - case .failure(let error): - Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, - error: error) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - } else { - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") + switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { + case .success: + break + case .failure(let error): + Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, + error: error) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } } - } - - WidgetCenter.shared.reloadAllTimelines() - Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { WidgetCenter.shared.reloadAllTimelines() - } - - PrivacyFeatures.httpsUpgrade.loadDataAsync() - - let variantManager = DefaultVariantManager() - let daxDialogs = DaxDialogs.shared - // assign it here, because "did become active" is already too late and "viewWillAppear" - // has already been called on the HomeViewController so won't show the home row CTA - cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { + WidgetCenter.shared.reloadAllTimelines() + } + + PrivacyFeatures.httpsUpgrade.loadDataAsync() + + let variantManager = DefaultVariantManager() + let daxDialogs = DaxDialogs.shared - // MARK: Sync initialisation + // assign it here, because "did become active" is already too late and "viewWillAppear" + // has already been called on the HomeViewController so won't show the home row CTA + cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + + // MARK: Sync initialisation #if DEBUG - let defaultEnvironment = ServerEnvironment.development + let defaultEnvironment = ServerEnvironment.development #else - let defaultEnvironment = ServerEnvironment.production + let defaultEnvironment = ServerEnvironment.production #endif - let environment = ServerEnvironment( - UserDefaultsWrapper( - key: .syncEnvironment, - defaultValue: defaultEnvironment.description - ).wrappedValue - ) ?? defaultEnvironment + let environment = ServerEnvironment( + UserDefaultsWrapper( + key: .syncEnvironment, + defaultValue: defaultEnvironment.description + ).wrappedValue + ) ?? defaultEnvironment - var dryRun = false + var dryRun = false #if DEBUG - dryRun = true + dryRun = true #endif - let isPhone = UIDevice.current.userInterfaceIdiom == .phone - let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: source.rawValue, - defaultHeaders: [:], - defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) - let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) - Task { - do { - _ = try await DefaultAPIService().fetch(request: request) - onComplete(true, nil) - } catch { - onComplete(false, error) + let isPhone = UIDevice.current.userInterfaceIdiom == .phone + let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: source.rawValue, + defaultHeaders: [:], + defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) + let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) + Task { + do { + _ = try await DefaultAPIService().fetch(request: request) + onComplete(true, nil) + } catch { + onComplete(false, error) + } } } - } - PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, - eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) - - let syncErrorHandler = SyncErrorHandler() - - syncDataProviders = SyncDataProviders( - bookmarksDatabase: bookmarksDatabase, - secureVaultErrorReporter: SecureVaultReporter(), - settingHandlers: [FavoritesDisplayModeSyncHandler()], - favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), - syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared, - tld: AppDependencyProvider.shared.storageCache.tld - ) - - let syncService = DDGSync( - dataProvidersSource: syncDataProviders, - errorEvents: SyncErrorHandler(), - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - environment: environment - ) - syncService.initializeIfNeeded() - self.syncService = syncService - - let fireproofing = UserDefaultsFireproofing.xshared - privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) - - isSyncInProgressCancellable = syncService.isSyncInProgressPublisher - .filter { $0 } - .sink { [weak syncService] _ in - DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) - syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in - Pixel.fire(pixel: .syncSuccessRateDaily, - withAdditionalParameters: params, - includedParameters: [.appVersion]) - }) - } + PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, + eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + + let syncErrorHandler = SyncErrorHandler() + + syncDataProviders = SyncDataProviders( + bookmarksDatabase: bookmarksDatabase, + secureVaultErrorReporter: SecureVaultReporter(), + settingHandlers: [FavoritesDisplayModeSyncHandler()], + favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), + syncErrorHandler: syncErrorHandler, + faviconStoring: Favicons.shared, + tld: AppDependencyProvider.shared.storageCache.tld + ) + + let syncService = DDGSync( + dataProvidersSource: syncDataProviders, + errorEvents: SyncErrorHandler(), + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + environment: environment + ) + syncService.initializeIfNeeded() + self.syncService = syncService + + let fireproofing = UserDefaultsFireproofing.xshared + privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) + + isSyncInProgressCancellable = syncService.isSyncInProgressPublisher + .filter { $0 } + .sink { [weak syncService] _ in + DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) + syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in + Pixel.fire(pixel: .syncSuccessRateDaily, + withAdditionalParameters: params, + includedParameters: [.appVersion]) + }) + } - remoteMessagingClient = RemoteMessagingClient( - bookmarksDatabase: bookmarksDatabase, - appSettings: AppDependencyProvider.shared.appSettings, - internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - configurationStore: AppDependencyProvider.shared.configurationStore, - database: Database.shared, - errorEvents: RemoteMessagingStoreErrorHandling(), - remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager - ), - duckPlayerStorage: DefaultDuckPlayerStorage() - ) - remoteMessagingClient.registerBackgroundRefreshTaskHandler() + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: AppDependencyProvider.shared.configurationStore, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ), + duckPlayerStorage: DefaultDuckPlayerStorage() + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() - subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - purchasePlatform: .appStore) + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + purchasePlatform: .appStore) - subscriptionCookieManager = makeSubscriptionCookieManager() + subscriptionCookieManager = makeSubscriptionCookieManager() - homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, - remoteMessagingClient: remoteMessagingClient, - privacyProDataReporter: privacyProDataReporter) + homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient, + privacyProDataReporter: privacyProDataReporter) - let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() - let tabsModel = prepareTabsModel(previewsSource: previewsSource) + let previewsSource = TabPreviewsSource() + let historyManager = makeHistoryManager() + let tabsModel = prepareTabsModel(previewsSource: previewsSource) - privacyProDataReporter.injectTabsModel(tabsModel) - - if shouldPresentInsufficientDiskSpaceAlertAndCrash { + privacyProDataReporter.injectTabsModel(tabsModel) - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - window?.makeKeyAndVisible() + if shouldPresentInsufficientDiskSpaceAlertAndCrash { - presentInsufficientDiskSpaceAlert() - } else { - let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) - let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) - let main = MainViewController(bookmarksDatabase: bookmarksDatabase, - bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, - historyManager: historyManager, - homePageConfiguration: homePageConfiguration, - syncService: syncService, - syncDataProviders: syncDataProviders, - appSettings: AppDependencyProvider.shared.appSettings, - previewsSource: previewsSource, - tabsModel: tabsModel, - syncPausedStateManager: syncErrorHandler, - privacyProDataReporter: privacyProDataReporter, - variantManager: variantManager, - contextualOnboardingPresenter: contextualOnboardingPresenter, - contextualOnboardingLogic: daxDialogs, - contextualOnboardingPixelReporter: onboardingPixelReporter, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - voiceSearchHelper: voiceSearchHelper, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - fireproofing: fireproofing, - subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: makeTextZoomCoordinator(), - websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), - appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) - - main.loadViewIfNeeded() - syncErrorHandler.alertPresenter = main - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = main - window?.makeKeyAndVisible() + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + window?.makeKeyAndVisible() - autoClear = AutoClear(worker: main) - let applicationState = application.applicationState - Task { - await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) - await vpnWorkaround.installRedditSessionWorkaround() + presentInsufficientDiskSpaceAlert() + } else { + let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) + let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) + let main = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + historyManager: historyManager, + homePageConfiguration: homePageConfiguration, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings, + previewsSource: previewsSource, + tabsModel: tabsModel, + syncPausedStateManager: syncErrorHandler, + privacyProDataReporter: privacyProDataReporter, + variantManager: variantManager, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: daxDialogs, + contextualOnboardingPixelReporter: onboardingPixelReporter, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + voiceSearchHelper: voiceSearchHelper, + featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) + + main.loadViewIfNeeded() + syncErrorHandler.alertPresenter = main + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = main + window?.makeKeyAndVisible() + + autoClear = AutoClear(worker: main) + let applicationState = application.applicationState + Task { + await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) + await vpnWorkaround.installRedditSessionWorkaround() + } } - } - self.voiceSearchHelper.migrateSettingsFlagIfNecessary() + self.voiceSearchHelper.migrateSettingsFlagIfNecessary() - // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. - // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. - AppConfigurationFetch.registerBackgroundRefreshTaskHandler() + // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. + // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. + AppConfigurationFetch.registerBackgroundRefreshTaskHandler() - UNUserNotificationCenter.current().delegate = self - - window?.windowScene?.screenshotService?.delegate = self - ThemeManager.shared.updateUserInterfaceStyle(window: window) + UNUserNotificationCenter.current().delegate = self - appIsLaunching = true + window?.windowScene?.screenshotService?.delegate = self + ThemeManager.shared.updateUserInterfaceStyle(window: window) - // Temporary logic for rollout of Autofill as on by default for new installs only - if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { - AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() - } + appIsLaunching = true - NewTabPageIntroMessageSetup().perform() + // Temporary logic for rollout of Autofill as on by default for new installs only + if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { + AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() + } - widgetRefreshModel.beginObservingVPNStatus() + NewTabPageIntroMessageSetup().perform() - AppDependencyProvider.shared.subscriptionManager.loadInitialData() + widgetRefreshModel.beginObservingVPNStatus() - setUpAutofillPixelReporter() + AppDependencyProvider.shared.subscriptionManager.loadInitialData() - if didCrashDuringCrashHandlersSetUp { - Pixel.fire(pixel: .crashOnCrashHandlersSetUp) - didCrashDuringCrashHandlersSetUp = false - } + setUpAutofillPixelReporter() - tipKitAppEventsHandler.appDidFinishLaunching() + if didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + didCrashDuringCrashHandlersSetUp = false + } - return true + tipKitAppEventsHandler.appDidFinishLaunching() + + return true + } } private func makeWebsiteDataManager(fireproofing: Fireproofing, @@ -638,100 +670,102 @@ import os.log } func applicationDidBecomeActive(_ application: UIApplication) { - guard !testing else { return } - - appStateMachine.handle(.activating) - return - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + if appBehavior == .stateMachine { + appStateMachine.handle(.activating) + } else { + guard !testing else { return } + + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } } - } - StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) - syncService.initializeIfNeeded() - syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) + syncService.initializeIfNeeded() + syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) - if !(overlayWindow?.rootViewController is AuthenticationViewController) { - removeOverlay() - } - - StatisticsLoader.shared.load { - StatisticsLoader.shared.refreshAppRetentionAtb() - self.fireAppLaunchPixel() - self.reportAdAttribution() - self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() - } - - if appIsLaunching { - appIsLaunching = false - onApplicationLaunch(application) - } + if !(overlayWindow?.rootViewController is AuthenticationViewController) { + removeOverlay() + } - mainViewController?.showBars() - mainViewController?.didReturnFromBackground() - - if !privacyStore.authenticationEnabled { - showKeyboardOnLaunch() - } + StatisticsLoader.shared.load { + StatisticsLoader.shared.refreshAppRetentionAtb() + self.fireAppLaunchPixel() + self.reportAdAttribution() + self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() + } - if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false - } - AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() + if appIsLaunching { + appIsLaunching = false + onApplicationLaunch(application) + } + + mainViewController?.showBars() + mainViewController?.didReturnFromBackground() + + if !privacyStore.authenticationEnabled { + showKeyboardOnLaunch() + } - AppConfigurationFetch().start { result in - self.sendAppLaunchPostback() - if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { + if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false } - } + AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() - syncService.scheduler.notifyAppLifecycleEvent() - - privacyProDataReporter.injectSyncService(syncService) + AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() + if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + } + } - fireFailedCompilationsPixelIfNeeded() + syncService.scheduler.notifyAppLifecycleEvent() - widgetRefreshModel.refreshVPNWidget() + privacyProDataReporter.injectSyncService(syncService) - if tunnelDefaults.showEntitlementAlert { - presentExpiredEntitlementAlert() - } + fireFailedCompilationsPixelIfNeeded() - presentExpiredEntitlementNotificationIfNeeded() + widgetRefreshModel.refreshVPNWidget() - Task { - await stopAndRemoveVPNIfNotAuthenticated() - await refreshShortcuts() - await vpnWorkaround.installRedditSessionWorkaround() + if tunnelDefaults.showEntitlementAlert { + presentExpiredEntitlementAlert() + } + + presentExpiredEntitlementNotificationIfNeeded() + + Task { + await stopAndRemoveVPNIfNotAuthenticated() + await refreshShortcuts() + await vpnWorkaround.installRedditSessionWorkaround() - if #available(iOS 17.0, *) { - await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() + if #available(iOS 17.0, *) { + await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() + } } - } - AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in - if isSubscriptionActive { - DailyPixel.fire(pixel: .privacyProSubscriptionActive) + AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in + if isSubscriptionActive { + DailyPixel.fire(pixel: .privacyProSubscriptionActive) + } } - } - Task { - await subscriptionCookieManager.refreshSubscriptionCookie() - } + Task { + await subscriptionCookieManager.refreshSubscriptionCookie() + } - let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) - importPasswordsStatusHandler.checkSyncSuccessStatus() + let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) + importPasswordsStatusHandler.checkSyncSuccessStatus() - Task { - await privacyProDataReporter.saveWidgetAdded() - } + Task { + await privacyProDataReporter.saveWidgetAdded() + } - AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + } } private func stopAndRemoveVPNIfNotAuthenticated() async { @@ -745,10 +779,13 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { - appStateMachine.handle(.suspending) - Task { @MainActor in - await refreshShortcuts() - await vpnWorkaround.removeRedditSessionWorkaround() + if appBehavior == .stateMachine { + appStateMachine.handle(.suspending) + } else { + Task { @MainActor in + await refreshShortcuts() + await vpnWorkaround.removeRedditSessionWorkaround() + } } } @@ -827,28 +864,31 @@ import os.log } func applicationWillEnterForeground(_ application: UIApplication) { - return - ThemeManager.shared.updateUserInterfaceStyle() + if appBehavior == .existing { + ThemeManager.shared.updateUserInterfaceStyle() - Task { @MainActor in - await beginAuthentication() - await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) - showKeyboardIfSettingOn = true - syncService.scheduler.resumeSyncQueue() + Task { @MainActor in + await beginAuthentication() + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) + showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } } } func applicationDidEnterBackground(_ application: UIApplication) { - appStateMachine.handle(.backgrounding) - return - displayBlankSnapshotWindow() - autoClear?.startClearingTimer() - lastBackgroundDate = Date() - AppDependencyProvider.shared.autofillLoginSession.endSession() - suspendSync() - syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) - privacyProDataReporter.saveApplicationLastSessionEnded() - resetAppStartTime() + if appBehavior == .stateMachine { + appStateMachine.handle(.backgrounding) + } else { + displayBlankSnapshotWindow() + autoClear?.startClearingTimer() + lastBackgroundDate = Date() + AppDependencyProvider.shared.autofillLoginSession.endSession() + suspendSync() + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) + privacyProDataReporter.saveApplicationLastSessionEnded() + resetAppStartTime() + } } private func resetAppStartTime() { @@ -881,40 +921,47 @@ import os.log func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - handleShortCutItem(shortcutItem) + if appBehavior == .stateMachine { + appStateMachine.handle(.handleShortcutItem(shortcutItem)) + } else { + handleShortCutItem(shortcutItem) + } } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - Logger.sync.debug("App launched with url \(url.absoluteString)") - appStateMachine.handle(.openURL(url)) - return true + if appBehavior == .stateMachine { + appStateMachine.handle(.openURL(url)) + return true + } else { + Logger.sync.debug("App launched with url \(url.absoluteString)") - // If showing the onboarding intro ignore deeplinks - guard mainViewController?.needsToShowOnboardingIntro() == false else { - return false - } + // If showing the onboarding intro ignore deeplinks + guard mainViewController?.needsToShowOnboardingIntro() == false else { + return false + } - if handleEmailSignUpDeepLink(url) { - return true - } + if handleEmailSignUpDeepLink(url) { + return true + } - NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) + NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) - // The openVPN action handles the navigation stack on its own and does not need it to be cleared - if url != AppDeepLinkSchemes.openVPN.url { - mainViewController?.clearNavigationStack() - } + // The openVPN action handles the navigation stack on its own and does not need it to be cleared + if url != AppDeepLinkSchemes.openVPN.url { + mainViewController?.clearNavigationStack() + } - Task { @MainActor in - // Autoclear should have happened by now - showKeyboardIfSettingOn = false + Task { @MainActor in + // Autoclear should have happened by now + showKeyboardIfSettingOn = false - if !handleAppDeepLink(app, mainViewController, url) { - mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) + if !handleAppDeepLink(app, mainViewController, url) { + mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) + } } - } - return true + return true + } } func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { @@ -1148,7 +1195,7 @@ import os.log } @MainActor - func refreshShortcuts() async { + private func refreshShortcuts() async { guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { UIApplication.shared.shortcutItems = nil return diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 32506119da..a555782ae0 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -27,6 +27,7 @@ enum AppEvent { case suspending case openURL(URL) + case handleShortcutItem(UIApplicationShortcutItem) } diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 2fdc86e6e4..3c2d4040d3 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -42,6 +42,9 @@ extension Launched { case .openURL(let url): urlToOpen = url return self + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + return self case .backgrounding: return InactiveBackground() case .launching, .suspending: @@ -60,6 +63,9 @@ extension Active { case .openURL(let url): openURL(url) return self + case .handleShortcutItem(let shortcutItem): + handleShortcutItem(shortcutItem) + return self case .launching, .activating, .backgrounding: return handleUnexpectedEvent(event) } @@ -78,7 +84,7 @@ extension Inactive { case .openURL(let url): urlToOpen = url return self - case .launching, .suspending: + case .launching, .suspending, .handleShortcutItem: return handleUnexpectedEvent(event) } } @@ -96,7 +102,7 @@ extension Background { return self case .backgrounding: return DoubleBackground() - case .launching, .suspending: + case .launching, .suspending, .handleShortcutItem: return handleUnexpectedEvent(event) } } @@ -151,6 +157,7 @@ extension AppEvent { case .backgrounding: return "backgrounding" case .suspending: return "suspending" case .openURL: return "openURL" + case .handleShortcutItem: return "handleShortcutItem" } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 6ff2fe29b6..d28753fe61 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -77,6 +77,8 @@ struct Active: AppState { if let url = stateContext.urlToOpen { openURL(url) + } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { + handleShortcutItem(shortcutItemToHandle) } activateApp(isTesting: stateContext.isTesting) @@ -458,6 +460,33 @@ struct Active: AppState { } } + func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { + Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") + + Task { @MainActor in + if shortcutItem.type == AppDelegate.ShortcutKey.clipboard, let query = UIPasteboard.general.string { + mainViewController.clearNavigationStack() + mainViewController.loadQueryInNewTab(query) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.passwords { + mainViewController.clearNavigationStack() + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [application] in + (application.window?.rootViewController as? MainViewController)?.launchAutofillLogins(openSearch: true, source: .appIconShortcut) + } + Pixel.fire(pixel: .autofillLoginsLaunchAppShortcut) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.openVPNSettings { + presentNetworkProtectionStatusSettingsModal() + } + + } + } + } extension Active { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 8b1dbfc795..50049ec5cd 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -79,6 +79,7 @@ struct Launched: AppState { private var window: UIWindow? var urlToOpen: URL? + var shortcutItemToHandle: UIApplicationShortcutItem? private let application: UIApplication init(stateContext: Init.StateContext) { @@ -647,6 +648,7 @@ extension Launched { let isTesting: Bool let didFinishLaunchingStartTime: CFAbsoluteTime let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? let appDependencies: AppDependencies } @@ -656,6 +658,7 @@ extension Launched { isTesting: isTesting, didFinishLaunchingStartTime: didFinishLaunchingStartTime, urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, appDependencies: appDependencies) } diff --git a/DuckDuckGo/AppSettings.swift b/DuckDuckGo/AppSettings.swift index f7242d9a88..106a92d9f9 100644 --- a/DuckDuckGo/AppSettings.swift +++ b/DuckDuckGo/AppSettings.swift @@ -84,6 +84,8 @@ protocol AppSettings: AnyObject, AppDebugSettings { var duckPlayerMode: DuckPlayerMode { get set } var duckPlayerAskModeOverlayHidden: Bool { get set } var duckPlayerOpenInNewTab: Bool { get set } + + var appBehavior: AppBehavior? { get set } } protocol AppDebugSettings { diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index 2c17e2ac1e..dafbd93848 100644 --- a/DuckDuckGo/AppUserDefaults.swift +++ b/DuckDuckGo/AppUserDefaults.swift @@ -80,6 +80,8 @@ public class AppUserDefaults: AppSettings { static let duckPlayerMode = "com.duckduckgo.ios.duckPlayerMode" static let duckPlayerAskModeOverlayHidden = "com.duckduckgo.ios.duckPlayerAskModeOverlayHidden" static let duckPlayerOpenInNewTab = "com.duckduckgo.ios.duckPlayerOpenInNewTab" + + static let appBehavior = "com.duckduckgo.ios.appBehavior" } private struct DebugKeys { @@ -146,7 +148,18 @@ public class AppUserDefaults: AppSettings { } } - + + var appBehavior: AppBehavior? { + get { + let value = userDefaults?.string(forKey: Keys.appBehavior) ?? "" + return AppBehavior(rawValue: value) + } + + set { + userDefaults?.setValue(newValue?.rawValue, forKey: Keys.appBehavior) + } + } + var autoClearAction: AutoClearSettingsModel.Action { get { From 128f049683597fe726f3905be174c84bf76f674e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 10:14:28 +0100 Subject: [PATCH 14/27] Add autoclear code to handleShortcutItem --- DuckDuckGo/AppLifecycle/AppStates/Active.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index d28753fe61..7e8ae2fd90 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -78,7 +78,7 @@ struct Active: AppState { if let url = stateContext.urlToOpen { openURL(url) } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { - handleShortcutItem(shortcutItemToHandle) + handleShortcutItem(shortcutItemToHandle, appIsLaunching: true) } activateApp(isTesting: stateContext.isTesting) @@ -460,10 +460,17 @@ struct Active: AppState { } } - func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { + func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem, appIsLaunching: Bool = false) { Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") - + let autoClear = appDependencies.autoClear Task { @MainActor in + + if appIsLaunching { + await autoClear.clearDataIfEnabled() + } else { + await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) + } + if shortcutItem.type == AppDelegate.ShortcutKey.clipboard, let query = UIPasteboard.general.string { mainViewController.clearNavigationStack() mainViewController.loadQueryInNewTab(query) From 7fee87d36d465d0ecf8a4dc8905405cd96fbc4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 13:41:44 +0100 Subject: [PATCH 15/27] Add comment --- DuckDuckGo/AppLifecycle/AppStates/Active.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 7e8ae2fd90..a70361b217 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -465,6 +465,7 @@ struct Active: AppState { let autoClear = appDependencies.autoClear Task { @MainActor in + // This if/else could potentially be removed by ensuring previous autoClear calls (triggered during both Launch and Active states) are completed before proceeding. To be looked at in next milestones if appIsLaunching { await autoClear.clearDataIfEnabled() } else { From d6edc0b881e81e8623180dac0bfc8ec0c4259400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 16:58:15 +0100 Subject: [PATCH 16/27] Handle new cases in state machine --- DuckDuckGo.xcodeproj/project.pbxproj | 4 -- .../AppLifecycle/AppStateTransitions.swift | 44 ++----------------- .../AppLifecycle/AppStates/Background.swift | 21 ++++++--- 3 files changed, 17 insertions(+), 52 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d42edfcf6a..d96996c8b7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -981,7 +981,6 @@ CB3C788D2D06D3A700A7E4ED /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; - CB3C78912D08484800A7E4ED /* InactiveBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3C78902D08483F00A7E4ED /* InactiveBackground.swift */; }; CB48D3332B90CE9F00631D8B /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */; }; CB4FA44E2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */; }; CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; @@ -2876,7 +2875,6 @@ CB2A7EF028410DF700885F67 /* PixelEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelEvent.swift; sourceTree = ""; }; CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLastCompiledRulesStore.swift; sourceTree = ""; }; CB2C47822AF6D55800AEDCD9 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; - CB3C78902D08483F00A7E4ED /* InactiveBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactiveBackground.swift; sourceTree = ""; }; CB4448752AF6D51D001F93F7 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = ""; }; CB48D3312B90CE9F00631D8B /* PageRefreshStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageRefreshStore.swift; sourceTree = ""; }; CB4FA44D2C78AACE00A16F5A /* SpecialErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScript.swift; sourceTree = ""; }; @@ -5610,7 +5608,6 @@ CBAD0EFC2CFE1D48006267B8 /* Active.swift */, CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, CBAD0F002CFE1D54006267B8 /* Background.swift */, - CB3C78902D08483F00A7E4ED /* InactiveBackground.swift */, ); path = AppStates; sourceTree = ""; @@ -8247,7 +8244,6 @@ 1DEAADF42BA47B5300E25A97 /* WebTrackingProtectionView.swift in Sources */, F15D43201E706CC500BF2CDC /* AutocompleteViewController.swift in Sources */, BD862E092B30F63E0073E2EE /* VPNMetadataCollector.swift in Sources */, - CB3C78912D08484800A7E4ED /* InactiveBackground.swift in Sources */, D6E83C682B23B6A3006C8AFB /* FontSettings.swift in Sources */, 7BF78E022CA2CC3E0026A1FC /* TipKitAppEventHandling.swift in Sources */, 1DEAADF62BA4809400E25A97 /* CookiePopUpProtectionView.swift in Sources */, diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 3c2d4040d3..e9e1f213ad 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -46,7 +46,7 @@ extension Launched { shortcutItemToHandle = shortcutItem return self case .backgrounding: - return InactiveBackground() + return Background(stateContext: makeStateContext()) case .launching, .suspending: return handleUnexpectedEvent(event) } @@ -101,7 +101,8 @@ extension Background { urlToOpen = url return self case .backgrounding: - return DoubleBackground() + run() + return self case .launching, .suspending, .handleShortcutItem: return handleUnexpectedEvent(event) } @@ -109,45 +110,6 @@ extension Background { } -extension DoubleBackground { - - func apply(event: AppEvent) -> any AppState { - // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - handleUnexpectedEvent(event) - - //todo: to be removed -// switch event { -// case .activating(let application): -// return Active(application: application) -// case .suspending(let application): -// return Inactive(application: application) -// case .launching, .backgrounding, .openURL: -// return self -// } - - } - -} - -extension InactiveBackground { - - func apply(event: AppEvent) -> any AppState { - // report event so we know what events can be called at this moment, but do not let SM be stuck in this state just not to be flooded with these events - handleUnexpectedEvent(event) - - //todo: to be removed -// switch event { -// case .activating(let application): -// return Active(application: application) -// case .suspending(let application): -// return Inactive(application: application) -// case .launching, .backgrounding, .openURL: -// return self -// } - } - -} - extension AppEvent { var rawValue: String { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index da5f4f1571..4e74e59c3e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -35,8 +35,19 @@ struct Background: AppState { application = stateContext.application appDependencies = stateContext.appDependencies urlToOpen = stateContext.urlToOpen - syncDidFinishCancellable = appDependencies.syncDidFinishCancellable + run() + } + + init(stateContext: Launched.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + urlToOpen = stateContext.urlToOpen + + run() + } + + mutating func run() { let autoClear = appDependencies.autoClear let privacyStore = appDependencies.privacyStore let privacyProDataReporter = appDependencies.privacyProDataReporter @@ -55,7 +66,7 @@ struct Background: AppState { autofillLoginSession.endSession() suspendSync(syncService: syncService) - syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(stateContext.application) + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) privacyProDataReporter.saveApplicationLastSessionEnded() resetAppStartTime() @@ -70,7 +81,7 @@ struct Background: AppState { Logger.sync.debug("Forcing background task completion") UIApplication.shared.endBackgroundTask(taskID) } - syncDidFinishCancellable?.cancel() + appDependencies.syncDidFinishCancellable?.cancel() syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } .prefix(1) .receive(on: DispatchQueue.main) @@ -108,7 +119,3 @@ extension Background { } } - -struct DoubleBackground: AppState { - -} From d6ed3dee643925e286efb5383a1c204795c8c507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 17:04:16 +0100 Subject: [PATCH 17/27] Fix swiftlint --- DuckDuckGo/AppDelegate.swift | 1 + DuckDuckGo/AppLifecycle/AppStates/Launched.swift | 2 ++ DuckDuckGo/AppServices/SubscriptionService.swift | 2 -- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index fbbd353dc5..0642e32ad9 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -669,6 +669,7 @@ enum AppBehavior: String { } } + // swiftlint:disable:next cyclomatic_complexity func applicationDidBecomeActive(_ application: UIApplication) { if appBehavior == .stateMachine { appStateMachine.handle(.activating) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 50049ec5cd..15d5c01978 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -82,6 +82,8 @@ struct Launched: AppState { var shortcutItemToHandle: UIApplicationShortcutItem? private let application: UIApplication + + // swiftlint:disable:next cyclomatic_complexity init(stateContext: Init.StateContext) { application = stateContext.application diff --git a/DuckDuckGo/AppServices/SubscriptionService.swift b/DuckDuckGo/AppServices/SubscriptionService.swift index 6ac3603dcd..fb5c9680d8 100644 --- a/DuckDuckGo/AppServices/SubscriptionService.swift +++ b/DuckDuckGo/AppServices/SubscriptionService.swift @@ -39,6 +39,4 @@ final class SubscriptionService { .store(in: &cancellables) } - - } From 0174398e99384964b8b37f5fdcb3fb70a2ffe7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 17:56:31 +0100 Subject: [PATCH 18/27] Fix compilation issue --- DuckDuckGo/AppDelegate.swift | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 0642e32ad9..e3afd8e686 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -99,7 +99,14 @@ enum AppBehavior: String { private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! private var subscriptionCookieManager: SubscriptionCookieManaging! private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? - private var privacyProDataReporter: PrivacyProDataReporting! + private var _privacyProDataReporter: PrivacyProDataReporting? + var privacyProDataReporter: PrivacyProDataReporting? { + if appBehavior == .stateMachine { + return (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter + } else { + return _privacyProDataReporter + } + } // MARK: - Feature specific app event handlers @@ -386,7 +393,7 @@ enum AppBehavior: String { self.syncService = syncService let fireproofing = UserDefaultsFireproofing.xshared - privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) + _privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) isSyncInProgressCancellable = syncService.isSyncInProgressPublisher .filter { $0 } @@ -421,13 +428,13 @@ enum AppBehavior: String { homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, remoteMessagingClient: remoteMessagingClient, - privacyProDataReporter: privacyProDataReporter) + privacyProDataReporter: _privacyProDataReporter!) let previewsSource = TabPreviewsSource() let historyManager = makeHistoryManager() let tabsModel = prepareTabsModel(previewsSource: previewsSource) - privacyProDataReporter.injectTabsModel(tabsModel) + _privacyProDataReporter!.injectTabsModel(tabsModel) if shouldPresentInsufficientDiskSpaceAlertAndCrash { @@ -450,7 +457,7 @@ enum AppBehavior: String { previewsSource: previewsSource, tabsModel: tabsModel, syncPausedStateManager: syncErrorHandler, - privacyProDataReporter: privacyProDataReporter, + privacyProDataReporter: _privacyProDataReporter!, variantManager: variantManager, contextualOnboardingPresenter: contextualOnboardingPresenter, contextualOnboardingLogic: daxDialogs, @@ -726,7 +733,7 @@ enum AppBehavior: String { syncService.scheduler.notifyAppLifecycleEvent() - privacyProDataReporter.injectSyncService(syncService) + _privacyProDataReporter?.injectSyncService(syncService) fireFailedCompilationsPixelIfNeeded() @@ -762,7 +769,7 @@ enum AppBehavior: String { importPasswordsStatusHandler.checkSyncSuccessStatus() Task { - await privacyProDataReporter.saveWidgetAdded() + await _privacyProDataReporter?.saveWidgetAdded() } AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } @@ -887,7 +894,7 @@ enum AppBehavior: String { AppDependencyProvider.shared.autofillLoginSession.endSession() suspendSync() syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) - privacyProDataReporter.saveApplicationLastSessionEnded() + _privacyProDataReporter?.saveApplicationLastSessionEnded() resetAppStartTime() } } From f19431bd289f8bee2d704294a405cc19253c28e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 13 Dec 2024 19:16:05 +0100 Subject: [PATCH 19/27] Fix tests --- DuckDuckGoTests/AppSettingsMock.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index 13ced3eb65..6b73e06dc0 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,6 +22,8 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { + var appBehavior: DuckDuckGo.AppBehavior? = .existing + var defaultTextZoomLevel: DuckDuckGo.TextZoomLevel = .percent100 var recentlyVisitedSites: Bool = false From 5e901f81c60bbb2d1718cca15240a94efceb5a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Mon, 16 Dec 2024 11:36:41 +0100 Subject: [PATCH 20/27] Fix tests --- DuckDuckGo/AppDelegate.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index e3afd8e686..107a00bfd8 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -134,6 +134,7 @@ enum AppBehavior: String { private let appStateMachine: AppStateMachine = AppStateMachine() private let appBehavior: AppBehavior = { + guard !ProcessInfo().arguments.contains("testing") else { return .existing } if let appBehavior = AppDependencyProvider.shared.appSettings.appBehavior { return appBehavior } From 29564d27ee8be399780fb5efa5b1364daa2a8964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Mon, 16 Dec 2024 15:41:04 +0100 Subject: [PATCH 21/27] Divide code into old and new app delegates --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + DuckDuckGo/AppDelegate+AppDeepLinks.swift | 2 +- DuckDuckGo/AppDelegate.swift | 1252 +------------------- DuckDuckGo/NewAppDelegate.swift | 65 ++ DuckDuckGo/OldAppDelegate.swift | 1263 +++++++++++++++++++++ 5 files changed, 1365 insertions(+), 1225 deletions(-) create mode 100644 DuckDuckGo/NewAppDelegate.swift create mode 100644 DuckDuckGo/OldAppDelegate.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d96996c8b7..2b9911e8cf 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1010,6 +1010,8 @@ CBD4F13E279EBFAB00B20FD7 /* HomeMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF14FC227970072001D94D0 /* HomeMessageView.swift */; }; CBD4F13F279EBFAF00B20FD7 /* HomeMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF14FC427970AB0001D94D0 /* HomeMessageViewModel.swift */; }; CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */; }; + CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */; }; + CBD79F4A2D1061E200DBB45A /* OldAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */; }; CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */; }; @@ -2913,6 +2915,8 @@ CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLCredentialCreator.swift; sourceTree = ""; }; CBC8DC252AF6D4CD00BA681A /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; + CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAppDelegate.swift; sourceTree = ""; }; + CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldAppDelegate.swift; sourceTree = ""; }; CBD7AE812AF6D5B6009052FD /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIHeadersTests.swift; sourceTree = ""; }; CBDD5DE029A6741300832877 /* MockBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBundle.swift; sourceTree = ""; }; @@ -6530,6 +6534,8 @@ 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, + CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */, + CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */, 85DB12EC2A1FED0C000A4A72 /* AppDelegate+AppDeepLinks.swift */, 98B31291218CCB8C00E54DE1 /* AppDependencyProvider.swift */, 85BA58591F3506AE00C6E8CA /* AppSettings.swift */, @@ -7723,6 +7729,7 @@ B623C1C42862CD670043013E /* WKDownloadSession.swift in Sources */, 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */, 1E8AD1D927C4FEC100ABA377 /* DownloadsListSectioningHelper.swift in Sources */, + CBD79F4A2D1061E200DBB45A /* OldAppDelegate.swift in Sources */, D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */, 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */, C12726F02A5FF89900215B02 /* EmailSignupPromptViewModel.swift in Sources */, @@ -7784,6 +7791,7 @@ CB3C788E2D06D3A700A7E4ED /* Init.swift in Sources */, CB3C788F2D06D3A700A7E4ED /* Inactive.swift in Sources */, 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */, + CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate+AppDeepLinks.swift b/DuckDuckGo/AppDelegate+AppDeepLinks.swift index 280b7c32bb..8b59faa969 100644 --- a/DuckDuckGo/AppDelegate+AppDeepLinks.swift +++ b/DuckDuckGo/AppDelegate+AppDeepLinks.swift @@ -20,7 +20,7 @@ import UIKit import Core -extension AppDelegate { +extension OldAppDelegate { func handleAppDeepLink(_ app: UIApplication, _ mainViewController: MainViewController?, _ url: URL) -> Bool { guard let mainViewController else { return false } diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 107a00bfd8..de85aa9d11 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -18,28 +18,7 @@ // import UIKit -import Combine -import Common import Core -import UserNotifications -import Kingfisher -import WidgetKit -import BackgroundTasks -import BrowserServicesKit -import Bookmarks -import Persistence -import Crashes -import Configuration -import Networking -import DDGSync -import RemoteMessaging -import SyncDataProviders -import Subscription -import NetworkProtection -import PixelKit -import PixelExperimentKit -import WebKit -import os.log enum AppBehavior: String { @@ -48,6 +27,15 @@ enum AppBehavior: String { } +protocol DDGAppDelegate { + + var privacyProDataReporter: PrivacyProDataReporting? { get } + + func initialize() + func refreshRemoteMessages() + +} + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) @@ -57,82 +45,12 @@ enum AppBehavior: String { static let openVPNSettings = "com.duckduckgo.mobile.ios.vpn.open-settings" } - private var testing = false - private var appIsLaunching = false - private var overlayWindow: UIWindow? var window: UIWindow? - private lazy var privacyStore = PrivacyUserDefaults() - private var bookmarksDatabase: CoreDataDatabase! - - private var widgetRefreshModel: NetworkProtectionWidgetRefreshModel! - private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults - - @MainActor - private lazy var vpnWorkaround: VPNRedditSessionWorkaround = { - return VPNRedditSessionWorkaround( - accountManager: AppDependencyProvider.shared.accountManager, - tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController - ) - }() - - private var autoClear: AutoClear? - private var showKeyboardIfSettingOn = true - private var lastBackgroundDate: Date? - - private(set) var homePageConfiguration: HomePageConfiguration! - - private(set) var remoteMessagingClient: RemoteMessagingClient! - - private(set) var syncService: DDGSync! - private(set) var syncDataProviders: SyncDataProviders! - private var syncDidFinishCancellable: AnyCancellable? - private var syncStateCancellable: AnyCancellable? - private var isSyncInProgressCancellable: AnyCancellable? - - private var crashCollection: CrashCollection! - private var crashReportUploaderOnboarding: CrashCollectionOnboarding? - - private var autofillPixelReporter: AutofillPixelReporter? - private var autofillUsageMonitor: AutofillUsageMonitor! - - private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! - private var subscriptionCookieManager: SubscriptionCookieManaging! - private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? - private var _privacyProDataReporter: PrivacyProDataReporting? var privacyProDataReporter: PrivacyProDataReporting? { - if appBehavior == .stateMachine { - return (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter - } else { - return _privacyProDataReporter - } - } - - // MARK: - Feature specific app event handlers - - private var tipKitAppEventsHandler: TipKitAppEventHandler! - - // MARK: lifecycle - - @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) - private var privacyConfigCustomURL: String? - - var accountManager: AccountManager { - AppDependencyProvider.shared.accountManager + realDelegate.privacyProDataReporter } - @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) - private var didCrashDuringCrashHandlersSetUp: Bool - - private var launchOptionsHandler: LaunchOptionsHandler! - private var onboardingPixelReporter: OnboardingPixelReporter! - - private var voiceSearchHelper: VoiceSearchHelper! - - private var marketplaceAdPostbackManager: MarketplaceAdPostbackManager! - private var didFinishLaunchingStartTime: CFAbsoluteTime? - - private let appStateMachine: AppStateMachine = AppStateMachine() private let appBehavior: AppBehavior = { guard !ProcessInfo().arguments.contains("testing") else { return .existing } if let appBehavior = AppDependencyProvider.shared.appSettings.appBehavior { @@ -143,834 +61,47 @@ enum AppBehavior: String { return appBehavior }() - override init() { - super.init() - + private lazy var realDelegate: UIApplicationDelegate & DDGAppDelegate = { if appBehavior == .existing { - - bookmarksDatabase = BookmarksDatabase.make() - widgetRefreshModel = NetworkProtectionWidgetRefreshModel() - crashCollection = CrashCollection(platform: .iOS) - autofillUsageMonitor = AutofillUsageMonitor() - tipKitAppEventsHandler = TipKitAppEventHandler() - launchOptionsHandler = LaunchOptionsHandler() - onboardingPixelReporter = OnboardingPixelReporter() - voiceSearchHelper = VoiceSearchHelper() - marketplaceAdPostbackManager = MarketplaceAdPostbackManager() - - - if !didCrashDuringCrashHandlersSetUp { - didCrashDuringCrashHandlersSetUp = true - CrashLogMessageExtractor.setUp(swapCxaThrow: false) - didCrashDuringCrashHandlersSetUp = false - } - } - - } - - // swiftlint:disable:next function_body_length - // swiftlint:disable:next cyclomatic_complexity - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - if appBehavior == .stateMachine { - appStateMachine.handle(.launching(application)) - return true - } else { - didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) - } - } - - -#if targetEnvironment(simulator) - if ProcessInfo.processInfo.environment["UITESTING"] == "true" { - // Disable hardware keyboards. - let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") - UITextInputMode.activeInputModes - // Filter `UIKeyboardInputMode`s. - .filter({ $0.responds(to: setHardwareLayout) }) - .forEach { $0.perform(setHardwareLayout, with: nil) } - } -#endif - -#if DEBUG - Pixel.isDryRun = true -#else - Pixel.isDryRun = false -#endif - - ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert - // Explicitly prepare ContentBlockingUpdating instance before Tabs are created - _ = ContentBlockingUpdating.shared - - // Can be removed after a couple of versions - cleanUpMacPromoExperiment2() - cleanUpIncrementalRolloutPixelTest() - - APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) - - if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { - Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) - } else { - Configuration.setURLProvider(AppConfigurationURLProvider()) - } - - crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in - pixelParameters.forEach { params in - Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) - - // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. - // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. - // If for some reason the parameter can't be found, fall back to the current version. - if let crashAppVersion = params[PixelParameters.appVersion] { - let dailyParameters = [PixelParameters.appVersion: crashAppVersion] - DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) - } else { - DailyPixel.fireDaily(.dbCrashDetectedDaily) - } - } - - // Async dispatch because rootViewController may otherwise be nil here - DispatchQueue.main.async { - guard let viewController = self.window?.rootViewController else { return } - - let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) - crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) - self.crashReportUploaderOnboarding = crashReportUploaderOnboarding - } - } - - clearTmp() - - _ = DefaultUserAgentManager.shared - testing = ProcessInfo().arguments.contains("testing") - if testing { - Pixel.isDryRun = true - _ = DefaultUserAgentManager.shared - Database.shared.loadStore { _, _ in } - _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() - - let blockingDelegate = BlockingNavigationDelegate() - let webView = blockingDelegate.prepareWebView() - window?.rootViewController?.view.addSubview(webView) - window?.rootViewController?.view.backgroundColor = .red - webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) - - let request = URLRequest(url: URL(string: "about:blank")!) - webView.load(request) - - return true - } - - removeEmailWaitlistState() - - var shouldPresentInsufficientDiskSpaceAlertAndCrash = false - Database.shared.loadStore { context, error in - guard let context = context else { - - let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", - PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] - - switch error { - case .none: - fatalError("Could not create database stack: Unknown Error") - case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): - Pixel.fire(pixel: .dbContainerInitializationError, - error: underlyingError, - withAdditionalParameters: parameters) - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(underlyingError.localizedDescription)") - case .some(let error): - Pixel.fire(pixel: .dbInitializationError, - error: error, - withAdditionalParameters: parameters) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - return - } else { - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") - } - } - } - DatabaseMigration.migrate(to: context) - } - - switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { - case .success: - break - case .failure(let error): - Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, - error: error) - if error.isDiskFull { - shouldPresentInsufficientDiskSpaceAlertAndCrash = true - } else { - Thread.sleep(forTimeInterval: 1) - fatalError("Could not create database stack: \(error.localizedDescription)") - } - } - - WidgetCenter.shared.reloadAllTimelines() - - Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { - WidgetCenter.shared.reloadAllTimelines() - } - - PrivacyFeatures.httpsUpgrade.loadDataAsync() - - let variantManager = DefaultVariantManager() - let daxDialogs = DaxDialogs.shared - - // assign it here, because "did become active" is already too late and "viewWillAppear" - // has already been called on the HomeViewController so won't show the home row CTA - cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) - - // MARK: Sync initialisation -#if DEBUG - let defaultEnvironment = ServerEnvironment.development -#else - let defaultEnvironment = ServerEnvironment.production -#endif - - let environment = ServerEnvironment( - UserDefaultsWrapper( - key: .syncEnvironment, - defaultValue: defaultEnvironment.description - ).wrappedValue - ) ?? defaultEnvironment - - var dryRun = false -#if DEBUG - dryRun = true -#endif - let isPhone = UIDevice.current.userInterfaceIdiom == .phone - let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS - PixelKit.setUp(dryRun: dryRun, - appVersion: AppVersion.shared.versionNumber, - source: source.rawValue, - defaultHeaders: [:], - defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in - - let url = URL.pixelUrl(forPixelNamed: pixelName) - let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) - let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) - Task { - do { - _ = try await DefaultAPIService().fetch(request: request) - onComplete(true, nil) - } catch { - onComplete(false, error) - } - } - } - PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, - eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) - - let syncErrorHandler = SyncErrorHandler() - - syncDataProviders = SyncDataProviders( - bookmarksDatabase: bookmarksDatabase, - secureVaultErrorReporter: SecureVaultReporter(), - settingHandlers: [FavoritesDisplayModeSyncHandler()], - favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), - syncErrorHandler: syncErrorHandler, - faviconStoring: Favicons.shared, - tld: AppDependencyProvider.shared.storageCache.tld - ) - - let syncService = DDGSync( - dataProvidersSource: syncDataProviders, - errorEvents: SyncErrorHandler(), - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - environment: environment - ) - syncService.initializeIfNeeded() - self.syncService = syncService - - let fireproofing = UserDefaultsFireproofing.xshared - _privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) - - isSyncInProgressCancellable = syncService.isSyncInProgressPublisher - .filter { $0 } - .sink { [weak syncService] _ in - DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) - syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in - Pixel.fire(pixel: .syncSuccessRateDaily, - withAdditionalParameters: params, - includedParameters: [.appVersion]) - }) - } - - remoteMessagingClient = RemoteMessagingClient( - bookmarksDatabase: bookmarksDatabase, - appSettings: AppDependencyProvider.shared.appSettings, - internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - configurationStore: AppDependencyProvider.shared.configurationStore, - database: Database.shared, - errorEvents: RemoteMessagingStoreErrorHandling(), - remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager - ), - duckPlayerStorage: DefaultDuckPlayerStorage() - ) - remoteMessagingClient.registerBackgroundRefreshTaskHandler() - - subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( - privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - purchasePlatform: .appStore) - - subscriptionCookieManager = makeSubscriptionCookieManager() - - homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, - remoteMessagingClient: remoteMessagingClient, - privacyProDataReporter: _privacyProDataReporter!) - - let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() - let tabsModel = prepareTabsModel(previewsSource: previewsSource) - - _privacyProDataReporter!.injectTabsModel(tabsModel) - - if shouldPresentInsufficientDiskSpaceAlertAndCrash { - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - window?.makeKeyAndVisible() - - presentInsufficientDiskSpaceAlert() - } else { - let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) - let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) - let main = MainViewController(bookmarksDatabase: bookmarksDatabase, - bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, - historyManager: historyManager, - homePageConfiguration: homePageConfiguration, - syncService: syncService, - syncDataProviders: syncDataProviders, - appSettings: AppDependencyProvider.shared.appSettings, - previewsSource: previewsSource, - tabsModel: tabsModel, - syncPausedStateManager: syncErrorHandler, - privacyProDataReporter: _privacyProDataReporter!, - variantManager: variantManager, - contextualOnboardingPresenter: contextualOnboardingPresenter, - contextualOnboardingLogic: daxDialogs, - contextualOnboardingPixelReporter: onboardingPixelReporter, - subscriptionFeatureAvailability: subscriptionFeatureAvailability, - voiceSearchHelper: voiceSearchHelper, - featureFlagger: AppDependencyProvider.shared.featureFlagger, - fireproofing: fireproofing, - subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: makeTextZoomCoordinator(), - websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), - appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) - - main.loadViewIfNeeded() - syncErrorHandler.alertPresenter = main - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = main - window?.makeKeyAndVisible() - - autoClear = AutoClear(worker: main) - let applicationState = application.applicationState - Task { - await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) - await vpnWorkaround.installRedditSessionWorkaround() - } - } - - self.voiceSearchHelper.migrateSettingsFlagIfNecessary() - - // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. - // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. - AppConfigurationFetch.registerBackgroundRefreshTaskHandler() - - UNUserNotificationCenter.current().delegate = self - - window?.windowScene?.screenshotService?.delegate = self - ThemeManager.shared.updateUserInterfaceStyle(window: window) - - appIsLaunching = true - - // Temporary logic for rollout of Autofill as on by default for new installs only - if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { - AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() - } - - NewTabPageIntroMessageSetup().perform() - - widgetRefreshModel.beginObservingVPNStatus() - - AppDependencyProvider.shared.subscriptionManager.loadInitialData() - - setUpAutofillPixelReporter() - - if didCrashDuringCrashHandlersSetUp { - Pixel.fire(pixel: .crashOnCrashHandlersSetUp) - didCrashDuringCrashHandlersSetUp = false - } - - tipKitAppEventsHandler.appDidFinishLaunching() - - return true - } - } - - private func makeWebsiteDataManager(fireproofing: Fireproofing, - dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { - return WebCacheManager(cookieStorage: MigratableCookieStorage(), - fireproofing: fireproofing, - dataStoreIDManager: dataStoreIDManager) - } - - private func makeTextZoomCoordinator() -> TextZoomCoordinator { - let provider = AppDependencyProvider.shared - let storage = TextZoomStorage() - - return TextZoomCoordinator(appSettings: provider.appSettings, - storage: storage, - featureFlagger: provider.featureFlagger) - } - - private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { - let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, - currentCookieStore: { [weak self] in - guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { - // We shouldn't interact with WebKit's cookie store unless we have a WebView, - // eventually the subscription cookie will be refreshed on opening the first tab - return nil - } - - return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) - }, eventMapping: SubscriptionCookieManageEventPixelMapping()) - - - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager - - // Enable subscriptionCookieManager if feature flag is present - if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { - subscriptionCookieManager.enableSettingSubscriptionCookie() - } - - // Keep track of feature flag changes - subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self, weak privacyConfigurationManager] in - guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } - - let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) - - Task { @MainActor [weak self] in - if isEnabled { - self?.subscriptionCookieManager.enableSettingSubscriptionCookie() - } else { - await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() - } - } - } - - return subscriptionCookieManager - } - - private func makeHistoryManager() -> HistoryManaging { - - let provider = AppDependencyProvider.shared - - switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, - isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, - privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, - tld: provider.storageCache.tld) { - - case .failure(let error): - Pixel.fire(pixel: .historyStoreLoadFailed, error: error) - if error.isDiskFull { - self.presentInsufficientDiskSpaceAlert() - } else { - self.presentPreemptiveCrashAlert() - } - return NullHistoryManager() - - case .success(let historyManager): - return historyManager - } - } - - private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), - appSettings: AppSettings = AppDependencyProvider.shared.appSettings, - isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { - let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad - let tabsModel: TabsModel - if AutoClearSettingsModel(settings: appSettings) != nil { - tabsModel = TabsModel(desktop: isPadDevice) - tabsModel.save() - previewsSource.removeAllPreviews() + return OldAppDelegate(with: self) } else { - if let storedModel = TabsModel.get() { - // Save new model in case of migration - storedModel.save() - tabsModel = storedModel - } else { - tabsModel = TabsModel(desktop: isPadDevice) - } - } - return tabsModel - } - - private func presentPreemptiveCrashAlert() { - Task { @MainActor in - let alertController = CriticalAlerts.makePreemptiveCrashAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - } - - private func presentInsufficientDiskSpaceAlert() { - let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - window?.rootViewController?.present(alertController, animated: true, completion: nil) - } - - private func presentExpiredEntitlementAlert() { - let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak self] in - self?.mainViewController?.segueToPrivacyPro() - } - window?.rootViewController?.present(alertController, animated: true) { [weak self] in - self?.tunnelDefaults.showEntitlementAlert = false + return NewAppDelegate() } - } - - private func presentExpiredEntitlementNotificationIfNeeded() { - let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( - settings: AppDependencyProvider.shared.vpnSettings, - defaults: .networkProtectionGroupDefaults, - wrappee: NetworkProtectionUNNotificationPresenter() - ) - presenter.showEntitlementNotification() - } - - private func cleanUpMacPromoExperiment2() { - UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") - } - - private func cleanUpIncrementalRolloutPixelTest() { - UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") - } + }() - private func clearTmp() { - let tmp = FileManager.default.temporaryDirectory - do { - try FileManager.default.removeItem(at: tmp) - } catch { - Logger.general.error("Failed to delete tmp dir") - } + override init() { + super.init() + realDelegate.initialize() } - private func reportAdAttribution() { - Task.detached(priority: .background) { - await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() - } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + realDelegate.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false } - // swiftlint:disable:next cyclomatic_complexity func applicationDidBecomeActive(_ application: UIApplication) { - if appBehavior == .stateMachine { - appStateMachine.handle(.activating) - } else { - guard !testing else { return } - - defer { - if let didFinishLaunchingStartTime { - let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime - Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), - withAdditionalParameters: [PixelParameters.time: String(launchTime)]) - } - } - - StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) - syncService.initializeIfNeeded() - syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) - - if !(overlayWindow?.rootViewController is AuthenticationViewController) { - removeOverlay() - } - - StatisticsLoader.shared.load { - StatisticsLoader.shared.refreshAppRetentionAtb() - self.fireAppLaunchPixel() - self.reportAdAttribution() - self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() - } - - if appIsLaunching { - appIsLaunching = false - onApplicationLaunch(application) - } - - mainViewController?.showBars() - mainViewController?.didReturnFromBackground() - - if !privacyStore.authenticationEnabled { - showKeyboardOnLaunch() - } - - if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false - } - AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() - - AppConfigurationFetch().start { result in - self.sendAppLaunchPostback() - if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() - } - } - - syncService.scheduler.notifyAppLifecycleEvent() - - _privacyProDataReporter?.injectSyncService(syncService) - - fireFailedCompilationsPixelIfNeeded() - - widgetRefreshModel.refreshVPNWidget() - - if tunnelDefaults.showEntitlementAlert { - presentExpiredEntitlementAlert() - } - - presentExpiredEntitlementNotificationIfNeeded() - - Task { - await stopAndRemoveVPNIfNotAuthenticated() - await refreshShortcuts() - await vpnWorkaround.installRedditSessionWorkaround() - - if #available(iOS 17.0, *) { - await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() - } - } - - AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in - if isSubscriptionActive { - DailyPixel.fire(pixel: .privacyProSubscriptionActive) - } - } - - Task { - await subscriptionCookieManager.refreshSubscriptionCookie() - } - - let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) - importPasswordsStatusHandler.checkSyncSuccessStatus() - - Task { - await _privacyProDataReporter?.saveWidgetAdded() - } - - AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } - } - } - - private func stopAndRemoveVPNIfNotAuthenticated() async { - // Only remove the VPN if the user is not authenticated, and it's installed: - guard !accountManager.isUserAuthenticated, await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { - return - } - - await AppDependencyProvider.shared.networkProtectionTunnelController.stop() - await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN(reason: .didBecomeActiveCheck) + realDelegate.applicationDidBecomeActive?(application) } func applicationWillResignActive(_ application: UIApplication) { - if appBehavior == .stateMachine { - appStateMachine.handle(.suspending) - } else { - Task { @MainActor in - await refreshShortcuts() - await vpnWorkaround.removeRedditSessionWorkaround() - } - } - } - - private func fireAppLaunchPixel() { - - WidgetCenter.shared.getCurrentConfigurations { result in - let paramKeys: [WidgetFamily: String] = [ - .systemSmall: PixelParameters.widgetSmall, - .systemMedium: PixelParameters.widgetMedium, - .systemLarge: PixelParameters.widgetLarge - ] - - switch result { - case .failure(let error): - Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [ - PixelParameters.widgetError: "1", - PixelParameters.widgetErrorCode: "\((error as NSError).code)", - PixelParameters.widgetErrorDomain: (error as NSError).domain - ], includedParameters: [.appVersion, .atb]) - - case .success(let widgetInfo): - let params = widgetInfo.reduce([String: String]()) { - var result = $0 - if let key = paramKeys[$1.family] { - result[key] = "1" - } - return result - } - Pixel.fire(pixel: .appLaunch, withAdditionalParameters: params, includedParameters: [.appVersion, .atb]) - } - - } - } - - private func fireFailedCompilationsPixelIfNeeded() { - let store = FailedCompilationsStore() - if store.hasAnyFailures { - DailyPixel.fire(pixel: .compilationFailed, withAdditionalParameters: store.summary) { error in - guard error != nil else { return } - store.cleanup() - } - } - } - - private func shouldShowKeyboardOnLaunch() -> Bool { - guard let date = lastBackgroundDate else { return true } - return Date().timeIntervalSince(date) > AppDelegate.ShowKeyboardOnLaunchThreshold - } - - private func showKeyboardOnLaunch() { - guard KeyboardSettings().onAppLaunch && showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch() else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.mainViewController?.enterSearch() - } - showKeyboardIfSettingOn = false - } - - private func onApplicationLaunch(_ application: UIApplication) { - Task { @MainActor in - await beginAuthentication() - initialiseBackgroundFetch(application) - applyAppearanceChanges() - refreshRemoteMessages() - } - } - - private func applyAppearanceChanges() { - UILabel.appearance(whenContainedInInstancesOf: [UIAlertController.self]).numberOfLines = 0 - } - - /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. - func refreshRemoteMessages() { - Task { - try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) - } + realDelegate.applicationWillResignActive?(application) } func applicationWillEnterForeground(_ application: UIApplication) { - if appBehavior == .existing { - ThemeManager.shared.updateUserInterfaceStyle() - - Task { @MainActor in - await beginAuthentication() - await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) - showKeyboardIfSettingOn = true - syncService.scheduler.resumeSyncQueue() - } - } + realDelegate.applicationWillEnterForeground?(application) } func applicationDidEnterBackground(_ application: UIApplication) { - if appBehavior == .stateMachine { - appStateMachine.handle(.backgrounding) - } else { - displayBlankSnapshotWindow() - autoClear?.startClearingTimer() - lastBackgroundDate = Date() - AppDependencyProvider.shared.autofillLoginSession.endSession() - suspendSync() - syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) - _privacyProDataReporter?.saveApplicationLastSessionEnded() - resetAppStartTime() - } - } - - private func resetAppStartTime() { - didFinishLaunchingStartTime = nil - mainViewController?.appDidFinishLaunchingStartTime = nil - } - - private func suspendSync() { - if syncService.isSyncInProgress { - Logger.sync.debug("Sync is in progress. Starting background task to allow it to gracefully complete.") - - var taskID: UIBackgroundTaskIdentifier! - taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { - Logger.sync.debug("Forcing background task completion") - UIApplication.shared.endBackgroundTask(taskID) - } - syncDidFinishCancellable?.cancel() - syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } - .prefix(1) - .receive(on: DispatchQueue.main) - .sink { _ in - Logger.sync.debug("Ending background task") - UIApplication.shared.endBackgroundTask(taskID) - } - } - - syncService.scheduler.cancelSyncAndSuspendSyncQueue() + realDelegate.applicationDidEnterBackground?(application) } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - if appBehavior == .stateMachine { - appStateMachine.handle(.handleShortcutItem(shortcutItem)) - } else { - handleShortCutItem(shortcutItem) - } + realDelegate.application?(application, performActionFor: shortcutItem, completionHandler: completionHandler) } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - if appBehavior == .stateMachine { - appStateMachine.handle(.openURL(url)) - return true - } else { - Logger.sync.debug("App launched with url \(url.absoluteString)") - - // If showing the onboarding intro ignore deeplinks - guard mainViewController?.needsToShowOnboardingIntro() == false else { - return false - } - - if handleEmailSignUpDeepLink(url) { - return true - } - - NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) - - // The openVPN action handles the navigation stack on its own and does not need it to be cleared - if url != AppDeepLinkSchemes.openVPN.url { - mainViewController?.clearNavigationStack() - } - - Task { @MainActor in - // Autoclear should have happened by now - showKeyboardIfSettingOn = false - - if !handleAppDeepLink(app, mainViewController, url) { - mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) - } - } - - return true - } + realDelegate.application?(app, open: url, options: options) ?? false } func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { @@ -991,338 +122,11 @@ enum AppBehavior: String { return true } - // MARK: private - - private func sendAppLaunchPostback() { - // Attribution support - let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager - if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { - marketplaceAdPostbackManager.sendAppLaunchPostback() - } - } - - private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { - let historyMessageManager = HistoryMessageManager() - - AtbAndVariantCleanup.cleanup() - variantManager.assignVariantIfNeeded { _ in - // MARK: perform first time launch logic here - // If it's running UI Tests check if the onboarding should be in a completed state. - if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { - daxDialogs.dismiss() - } else { - daxDialogs.primeForUse() - } - - // New users don't see the message - historyMessageManager.dismiss() - - // Setup storage for marketplace postback - marketplaceAdPostbackManager.updateReturningUserValue() - } - } - - private func initialiseBackgroundFetch(_ application: UIApplication) { - guard UIApplication.shared.backgroundRefreshStatus == .available else { - return - } - - // BackgroundTasks will automatically replace an existing task in the queue if one with the same identifier is queued, so we should only - // schedule a task if there are none pending in order to avoid the config task getting perpetually replaced. - BGTaskScheduler.shared.getPendingTaskRequests { tasks in - let hasConfigurationTask = tasks.contains { $0.identifier == AppConfigurationFetch.Constants.backgroundProcessingTaskIdentifier } - if !hasConfigurationTask { - AppConfigurationFetch.scheduleBackgroundRefreshTask() - } - - let hasRemoteMessageFetchTask = tasks.contains { $0.identifier == RemoteMessagingClient.Constants.backgroundRefreshTaskIdentifier } - if !hasRemoteMessageFetchTask { - RemoteMessagingClient.scheduleBackgroundRefreshTask() - } - } - } - - private func displayAuthenticationWindow() { - guard overlayWindow == nil, let frame = window?.frame else { return } - overlayWindow = UIWindow(frame: frame) - overlayWindow?.windowLevel = UIWindow.Level.alert - overlayWindow?.rootViewController = AuthenticationViewController.loadFromStoryboard() - overlayWindow?.makeKeyAndVisible() - window?.isHidden = true - } - - private func displayBlankSnapshotWindow() { - guard overlayWindow == nil, let frame = window?.frame else { return } - guard autoClear?.isClearingEnabled ?? false || privacyStore.authenticationEnabled else { return } - - overlayWindow = UIWindow(frame: frame) - overlayWindow?.windowLevel = UIWindow.Level.alert - - let overlay = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, - voiceSearchHelper: voiceSearchHelper) - overlay.delegate = self - - overlayWindow?.rootViewController = overlay - overlayWindow?.makeKeyAndVisible() - window?.isHidden = true - } - - private func beginAuthentication() async { - - guard privacyStore.authenticationEnabled else { return } - - removeOverlay() - displayAuthenticationWindow() - - guard let controller = overlayWindow?.rootViewController as? AuthenticationViewController else { - removeOverlay() - return - } - - await controller.beginAuthentication { [weak self] in - self?.removeOverlay() - self?.showKeyboardOnLaunch() - } - } - - private func tryToObtainOverlayWindow() { - for window in UIApplication.shared.foregroundSceneWindows where window.rootViewController is BlankSnapshotViewController { - overlayWindow = window - return - } - } - - private func removeOverlay() { - if overlayWindow == nil { - tryToObtainOverlayWindow() - } - - if let overlay = overlayWindow { - overlay.isHidden = true - overlayWindow = nil - window?.makeKeyAndVisible() - } - } - - private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) { - Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") - - Task { @MainActor in - - if appIsLaunching { - await autoClear?.clearDataIfEnabled() - } else { - await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) - } - - if shortcutItem.type == ShortcutKey.clipboard, let query = UIPasteboard.general.string { - mainViewController?.clearNavigationStack() - mainViewController?.loadQueryInNewTab(query) - return - } - - if shortcutItem.type == ShortcutKey.passwords { - mainViewController?.clearNavigationStack() - // Give the `clearNavigationStack` call time to complete. - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in - self?.mainViewController?.launchAutofillLogins(openSearch: true, source: .appIconShortcut) - } - Pixel.fire(pixel: .autofillLoginsLaunchAppShortcut) - return - } - - if shortcutItem.type == ShortcutKey.openVPNSettings { - presentNetworkProtectionStatusSettingsModal() - } - - } - } - - private func removeEmailWaitlistState() { - EmailWaitlist.removeEmailState() - - let autofillStorage = EmailKeychainManager() - try? autofillStorage.deleteWaitlistState() - - // Remove the authentication state if this is a fresh install. - if !Database.shared.isDatabaseFileInitialized { - try? autofillStorage.deleteAuthenticationState() - } - } - - private func handleEmailSignUpDeepLink(_ url: URL) -> Bool { - guard url.absoluteString.starts(with: URL.emailProtection.absoluteString), - let navViewController = mainViewController?.presentedViewController as? UINavigationController, - let emailSignUpViewController = navViewController.topViewController as? EmailSignupViewController else { - return false - } - emailSignUpViewController.loadUrl(url) - return true - } - - private var mainViewController: MainViewController? { - return window?.rootViewController as? MainViewController - } - - private func setUpAutofillPixelReporter() { - autofillPixelReporter = AutofillPixelReporter( - userDefaults: .standard, - autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, - eventMapping: EventMapping {[weak self] event, _, params, _ in - switch event { - case .autofillActiveUser: - Pixel.fire(pixel: .autofillActiveUser) - case .autofillEnabledUser: - Pixel.fire(pixel: .autofillEnabledUser) - case .autofillOnboardedUser: - Pixel.fire(pixel: .autofillOnboardedUser) - case .autofillToggledOn: - Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) - if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { - Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, - withAdditionalParameters: params ?? [:]) - } - case .autofillToggledOff: - Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) - if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { - Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, - withAdditionalParameters: params ?? [:]) - } - case .autofillLoginsStacked: - Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) - default: - break - } - }, - installDate: StatisticsUserDefaults().installDate ?? Date()) - - _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, - object: nil, - queue: nil) { [weak self] _ in - self?.autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) - } - } - - @MainActor - private func refreshShortcuts() async { - guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { - UIApplication.shared.shortcutItems = nil - return - } - - if case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { - let items = [ - UIApplicationShortcutItem(type: ShortcutKey.openVPNSettings, - localizedTitle: UserText.netPOpenVPNQuickAction, - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), - userInfo: nil) - ] - - UIApplication.shared.shortcutItems = items - } else { - UIApplication.shared.shortcutItems = nil - } - } -} - -extension AppDelegate: BlankSnapshotViewRecoveringDelegate { - - func recoverFromPresenting(controller: BlankSnapshotViewController) { - if overlayWindow == nil { - tryToObtainOverlayWindow() - } - - overlayWindow?.isHidden = true - overlayWindow = nil - window?.makeKeyAndVisible() - } - -} - -extension AppDelegate: UIScreenshotServiceDelegate { - func screenshotService(_ screenshotService: UIScreenshotService, - generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { - guard let webView = mainViewController?.currentTab?.webView else { - completionHandler(nil, 0, .zero) - return - } - - let zoomScale = webView.scrollView.zoomScale - - // The PDF's coordinate space has its origin at the bottom left, so the view's origin.y needs to be converted - let visibleBounds = CGRect( - x: webView.scrollView.contentOffset.x / zoomScale, - y: (webView.scrollView.contentSize.height - webView.scrollView.contentOffset.y - webView.bounds.height) / zoomScale, - width: webView.bounds.width / zoomScale, - height: webView.bounds.height / zoomScale - ) - - webView.createPDF { result in - let data = try? result.get() - completionHandler(data, 0, visibleBounds) - } - } -} - -extension AppDelegate: UNUserNotificationCenterDelegate { - - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler(.banner) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - let identifier = response.notification.request.identifier - - if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { - presentNetworkProtectionStatusSettingsModal() - } - } - - completionHandler() - } - - func presentNetworkProtectionStatusSettingsModal() { - Task { - if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { - (window?.rootViewController as? MainViewController)?.segueToVPN() - } else { - (window?.rootViewController as? MainViewController)?.segueToPrivacyPro() - } - } + /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. + func refreshRemoteMessages() { + realDelegate.refreshRemoteMessages() } - private func presentSettings(with viewController: UIViewController) { - guard let window = window, let rootViewController = window.rootViewController as? MainViewController else { return } - - if let navigationController = rootViewController.presentedViewController as? UINavigationController { - if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { - // Avoid presenting dismissing and re-presenting the view controller if it's already visible: - return - } else { - // Otherwise, replace existing view controllers with the presented one: - navigationController.popToRootViewController(animated: false) - navigationController.pushViewController(viewController, animated: false) - return - } - } - - // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: - rootViewController.clearNavigationStack() - - // Give the `clearNavigationStack` call time to complete. - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { - rootViewController.segueToSettings() - let navigationController = rootViewController.presentedViewController as? UINavigationController - navigationController?.popToRootViewController(animated: false) - navigationController?.pushViewController(viewController, animated: false) - } - } } extension DataStoreWarmup.ApplicationState { diff --git a/DuckDuckGo/NewAppDelegate.swift b/DuckDuckGo/NewAppDelegate.swift new file mode 100644 index 0000000000..1f341b3389 --- /dev/null +++ b/DuckDuckGo/NewAppDelegate.swift @@ -0,0 +1,65 @@ +// +// NewAppDelegate.swift +// DuckDuckGo +// +// 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 UIKit + +final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGAppDelegate { + + private let appStateMachine: AppStateMachine = AppStateMachine() + + var privacyProDataReporter: PrivacyProDataReporting? { + (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this antipattern + } + + func initialize() { } // init code will happen inside AppStateMachine/Init state .init() + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + appStateMachine.handle(.launching(application)) + return true + } + + func applicationDidBecomeActive(_ application: UIApplication) { + appStateMachine.handle(.activating) + } + + func applicationWillResignActive(_ application: UIApplication) { + appStateMachine.handle(.suspending) + } + + func applicationDidEnterBackground(_ application: UIApplication) { + appStateMachine.handle(.backgrounding) + } + + func application(_ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void) { + appStateMachine.handle(.handleShortcutItem(shortcutItem)) + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + appStateMachine.handle(.openURL(url)) + return true + } + + func refreshRemoteMessages() { + // part of debug menu, let's not support it in the first iteration + } + + +} diff --git a/DuckDuckGo/OldAppDelegate.swift b/DuckDuckGo/OldAppDelegate.swift new file mode 100644 index 0000000000..ee8413394a --- /dev/null +++ b/DuckDuckGo/OldAppDelegate.swift @@ -0,0 +1,1263 @@ +// +// AppDelegate.swift +// DuckDuckGo +// +// Copyright © 2017 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 UIKit +import Combine +import Common +import Core +import UserNotifications +import Kingfisher +import WidgetKit +import BackgroundTasks +import BrowserServicesKit +import Bookmarks +import Persistence +import Crashes +import Configuration +import Networking +import DDGSync +import RemoteMessaging +import SyncDataProviders +import Subscription +import NetworkProtection +import PixelKit +import PixelExperimentKit +import WebKit +import os.log + +@MainActor +final class OldAppDelegate: NSObject, UIApplicationDelegate, DDGAppDelegate { + + private var testing = false + var appIsLaunching = false + var overlayWindow: UIWindow? + var window: UIWindow? { + get { + appDelegate?.window + } + set { + appDelegate?.window = newValue + } + } + + private lazy var privacyStore = PrivacyUserDefaults() + private var bookmarksDatabase: CoreDataDatabase = BookmarksDatabase.make() + + private let widgetRefreshModel = NetworkProtectionWidgetRefreshModel() + private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults + + @MainActor + private lazy var vpnWorkaround: VPNRedditSessionWorkaround = { + return VPNRedditSessionWorkaround( + accountManager: AppDependencyProvider.shared.accountManager, + tunnelController: AppDependencyProvider.shared.networkProtectionTunnelController + ) + }() + + private var autoClear: AutoClear? + private var showKeyboardIfSettingOn = true + private var lastBackgroundDate: Date? + + private(set) var homePageConfiguration: HomePageConfiguration! + + private(set) var remoteMessagingClient: RemoteMessagingClient! + + private(set) var syncService: DDGSync! + private(set) var syncDataProviders: SyncDataProviders! + private var syncDidFinishCancellable: AnyCancellable? + private var syncStateCancellable: AnyCancellable? + private var isSyncInProgressCancellable: AnyCancellable? + + private let crashCollection = CrashCollection(platform: .iOS) + private var crashReportUploaderOnboarding: CrashCollectionOnboarding? + + private var autofillPixelReporter: AutofillPixelReporter? + private var autofillUsageMonitor = AutofillUsageMonitor() + + private(set) var subscriptionFeatureAvailability: SubscriptionFeatureAvailability! + private var subscriptionCookieManager: SubscriptionCookieManaging! + private var subscriptionCookieManagerFeatureFlagCancellable: AnyCancellable? + var privacyProDataReporter: PrivacyProDataReporting? + + // MARK: - Feature specific app event handlers + + private let tipKitAppEventsHandler = TipKitAppEventHandler() + + // MARK: lifecycle + + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + private var privacyConfigCustomURL: String? + + var accountManager: AccountManager { + AppDependencyProvider.shared.accountManager + } + + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + private var didCrashDuringCrashHandlersSetUp: Bool + + private let launchOptionsHandler = LaunchOptionsHandler() + private let onboardingPixelReporter = OnboardingPixelReporter() + + private let voiceSearchHelper = VoiceSearchHelper() + + private let marketplaceAdPostbackManager = MarketplaceAdPostbackManager() + + private var didFinishLaunchingStartTime: CFAbsoluteTime? + + private weak var appDelegate: AppDelegate? + init(with appDelegate: AppDelegate) { + self.appDelegate = appDelegate + } + + func initialize() { + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp(swapCxaThrow: false) + didCrashDuringCrashHandlersSetUp = false + } + } + + // swiftlint:disable:next function_body_length + // swiftlint:disable:next cyclomatic_complexity + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidFinishLaunchingTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } + } + + +#if targetEnvironment(simulator) + if ProcessInfo.processInfo.environment["UITESTING"] == "true" { + // Disable hardware keyboards. + let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") + UITextInputMode.activeInputModes + // Filter `UIKeyboardInputMode`s. + .filter({ $0.responds(to: setHardwareLayout) }) + .forEach { $0.perform(setHardwareLayout, with: nil) } + } +#endif + +#if DEBUG + Pixel.isDryRun = true +#else + Pixel.isDryRun = false +#endif + + ContentBlocking.shared.onCriticalError = presentPreemptiveCrashAlert + // Explicitly prepare ContentBlockingUpdating instance before Tabs are created + _ = ContentBlockingUpdating.shared + + // Can be removed after a couple of versions + cleanUpMacPromoExperiment2() + cleanUpIncrementalRolloutPixelTest() + + APIRequest.Headers.setUserAgent(DefaultUserAgentManager.duckDuckGoUserAgent) + + if isDebugBuild, let privacyConfigCustomURL, let url = URL(string: privacyConfigCustomURL) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } + + crashCollection.startAttachingCrashLogMessages { pixelParameters, payloads, sendReport in + pixelParameters.forEach { params in + Pixel.fire(pixel: .dbCrashDetected, withAdditionalParameters: params, includedParameters: []) + + // Each crash comes with an `appVersion` parameter representing the version that the crash occurred on. + // This is to disambiguate the situation where a crash occurs, but isn't sent until the next update. + // If for some reason the parameter can't be found, fall back to the current version. + if let crashAppVersion = params[PixelParameters.appVersion] { + let dailyParameters = [PixelParameters.appVersion: crashAppVersion] + DailyPixel.fireDaily(.dbCrashDetectedDaily, withAdditionalParameters: dailyParameters) + } else { + DailyPixel.fireDaily(.dbCrashDetectedDaily) + } + } + + // Async dispatch because rootViewController may otherwise be nil here + DispatchQueue.main.async { + guard let viewController = self.window?.rootViewController else { return } + + let crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) + self.crashReportUploaderOnboarding = crashReportUploaderOnboarding + } + } + + clearTmp() + + _ = DefaultUserAgentManager.shared + testing = ProcessInfo().arguments.contains("testing") + if testing { + Pixel.isDryRun = true + _ = DefaultUserAgentManager.shared + Database.shared.loadStore { _, _ in } + _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + + let blockingDelegate = BlockingNavigationDelegate() + let webView = blockingDelegate.prepareWebView() + window?.rootViewController?.view.addSubview(webView) + window?.rootViewController?.view.backgroundColor = .red + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + + let request = URLRequest(url: URL(string: "about:blank")!) + webView.load(request) + + return true + } + + removeEmailWaitlistState() + + var shouldPresentInsufficientDiskSpaceAlertAndCrash = false + Database.shared.loadStore { context, error in + guard let context = context else { + + let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"] + + switch error { + case .none: + fatalError("Could not create database stack: Unknown Error") + case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)): + Pixel.fire(pixel: .dbContainerInitializationError, + error: underlyingError, + withAdditionalParameters: parameters) + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(underlyingError.localizedDescription)") + case .some(let error): + Pixel.fire(pixel: .dbInitializationError, + error: error, + withAdditionalParameters: parameters) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + return + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + } + DatabaseMigration.migrate(to: context) + } + + switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) { + case .success: + break + case .failure(let error): + Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase, + error: error) + if error.isDiskFull { + shouldPresentInsufficientDiskSpaceAlertAndCrash = true + } else { + Thread.sleep(forTimeInterval: 1) + fatalError("Could not create database stack: \(error.localizedDescription)") + } + } + + WidgetCenter.shared.reloadAllTimelines() + + Favicons.shared.migrateFavicons(to: Favicons.Constants.maxFaviconSize) { + WidgetCenter.shared.reloadAllTimelines() + } + + PrivacyFeatures.httpsUpgrade.loadDataAsync() + + let variantManager = DefaultVariantManager() + let daxDialogs = DaxDialogs.shared + + // assign it here, because "did become active" is already too late and "viewWillAppear" + // has already been called on the HomeViewController so won't show the home row CTA + cleanUpATBAndAssignVariant(variantManager: variantManager, daxDialogs: daxDialogs) + + // MARK: Sync initialisation +#if DEBUG + let defaultEnvironment = ServerEnvironment.development +#else + let defaultEnvironment = ServerEnvironment.production +#endif + + let environment = ServerEnvironment( + UserDefaultsWrapper( + key: .syncEnvironment, + defaultValue: defaultEnvironment.description + ).wrappedValue + ) ?? defaultEnvironment + + var dryRun = false +#if DEBUG + dryRun = true +#endif + let isPhone = UIDevice.current.userInterfaceIdiom == .phone + let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: source.rawValue, + defaultHeaders: [:], + defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) + let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) + Task { + do { + _ = try await DefaultAPIService().fetch(request: request) + onComplete(true, nil) + } catch { + onComplete(false, error) + } + } + } + PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, + eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + + let syncErrorHandler = SyncErrorHandler() + + syncDataProviders = SyncDataProviders( + bookmarksDatabase: bookmarksDatabase, + secureVaultErrorReporter: SecureVaultReporter(), + settingHandlers: [FavoritesDisplayModeSyncHandler()], + favoritesDisplayModeStorage: FavoritesDisplayModeStorage(), + syncErrorHandler: syncErrorHandler, + faviconStoring: Favicons.shared, + tld: AppDependencyProvider.shared.storageCache.tld + ) + + let syncService = DDGSync( + dataProvidersSource: syncDataProviders, + errorEvents: SyncErrorHandler(), + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + environment: environment + ) + syncService.initializeIfNeeded() + self.syncService = syncService + + let fireproofing = UserDefaultsFireproofing.xshared + privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) + + isSyncInProgressCancellable = syncService.isSyncInProgressPublisher + .filter { $0 } + .sink { [weak syncService] _ in + DailyPixel.fire(pixel: .syncDaily, includedParameters: [.appVersion]) + syncService?.syncDailyStats.sendStatsIfNeeded(handler: { params in + Pixel.fire(pixel: .syncSuccessRateDaily, + withAdditionalParameters: params, + includedParameters: [.appVersion]) + }) + } + + remoteMessagingClient = RemoteMessagingClient( + bookmarksDatabase: bookmarksDatabase, + appSettings: AppDependencyProvider.shared.appSettings, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + configurationStore: AppDependencyProvider.shared.configurationStore, + database: Database.shared, + errorEvents: RemoteMessagingStoreErrorHandling(), + remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager + ), + duckPlayerStorage: DefaultDuckPlayerStorage() + ) + remoteMessagingClient.registerBackgroundRefreshTaskHandler() + + subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability( + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + purchasePlatform: .appStore) + + subscriptionCookieManager = makeSubscriptionCookieManager() + + homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient, + privacyProDataReporter: privacyProDataReporter!) + + let previewsSource = TabPreviewsSource() + let historyManager = makeHistoryManager() + let tabsModel = prepareTabsModel(previewsSource: previewsSource) + + privacyProDataReporter?.injectTabsModel(tabsModel) + + if shouldPresentInsufficientDiskSpaceAlertAndCrash { + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + window?.makeKeyAndVisible() + + presentInsufficientDiskSpaceAlert() + } else { + let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) + let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) + let main = MainViewController(bookmarksDatabase: bookmarksDatabase, + bookmarksDatabaseCleaner: syncDataProviders.bookmarksAdapter.databaseCleaner, + historyManager: historyManager, + homePageConfiguration: homePageConfiguration, + syncService: syncService, + syncDataProviders: syncDataProviders, + appSettings: AppDependencyProvider.shared.appSettings, + previewsSource: previewsSource, + tabsModel: tabsModel, + syncPausedStateManager: syncErrorHandler, + privacyProDataReporter: privacyProDataReporter!, + variantManager: variantManager, + contextualOnboardingPresenter: contextualOnboardingPresenter, + contextualOnboardingLogic: daxDialogs, + contextualOnboardingPixelReporter: onboardingPixelReporter, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + voiceSearchHelper: voiceSearchHelper, + featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, + subscriptionCookieManager: subscriptionCookieManager, + textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) + + main.loadViewIfNeeded() + syncErrorHandler.alertPresenter = main + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = main + window?.makeKeyAndVisible() + + autoClear = AutoClear(worker: main) + let applicationState = application.applicationState + Task { + await autoClear?.clearDataIfEnabled(applicationState: .init(with: applicationState)) + await vpnWorkaround.installRedditSessionWorkaround() + } + } + + self.voiceSearchHelper.migrateSettingsFlagIfNecessary() + + // Task handler registration needs to happen before the end of `didFinishLaunching`, otherwise submitting a task can throw an exception. + // Having both in `didBecomeActive` can sometimes cause the exception when running on a physical device, so registration happens here. + AppConfigurationFetch.registerBackgroundRefreshTaskHandler() + + UNUserNotificationCenter.current().delegate = self + + window?.windowScene?.screenshotService?.delegate = self + ThemeManager.shared.updateUserInterfaceStyle(window: window) + + appIsLaunching = true + + // Temporary logic for rollout of Autofill as on by default for new installs only + if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { + AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() + } + + NewTabPageIntroMessageSetup().perform() + + widgetRefreshModel.beginObservingVPNStatus() + + AppDependencyProvider.shared.subscriptionManager.loadInitialData() + + setUpAutofillPixelReporter() + + if didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + didCrashDuringCrashHandlersSetUp = false + } + + tipKitAppEventsHandler.appDidFinishLaunching() + + return true + } + + private func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + return WebCacheManager(cookieStorage: MigratableCookieStorage(), + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager) + } + + private func makeTextZoomCoordinator() -> TextZoomCoordinator { + let provider = AppDependencyProvider.shared + let storage = TextZoomStorage() + + return TextZoomCoordinator(appSettings: provider.appSettings, + storage: storage, + featureFlagger: provider.featureFlagger) + } + + private func makeSubscriptionCookieManager() -> SubscriptionCookieManaging { + let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + currentCookieStore: { [weak self] in + guard self?.mainViewController?.tabManager.model.hasActiveTabs ?? false else { + // We shouldn't interact with WebKit's cookie store unless we have a WebView, + // eventually the subscription cookie will be refreshed on opening the first tab + return nil + } + + return WKHTTPCookieStoreWrapper(store: WKWebsiteDataStore.current().httpCookieStore) + }, eventMapping: SubscriptionCookieManageEventPixelMapping()) + + + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + // Keep track of feature flag changes + subscriptionCookieManagerFeatureFlagCancellable = privacyConfigurationManager.updatesPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self, weak privacyConfigurationManager] in + guard let self, !self.appIsLaunching, let privacyConfigurationManager else { return } + + let isEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) + + Task { @MainActor [weak self] in + if isEnabled { + self?.subscriptionCookieManager.enableSettingSubscriptionCookie() + } else { + await self?.subscriptionCookieManager.disableSettingSubscriptionCookie() + } + } + } + + return subscriptionCookieManager + } + + private func makeHistoryManager() -> HistoryManaging { + + let provider = AppDependencyProvider.shared + + switch HistoryManager.make(isAutocompleteEnabledByUser: provider.appSettings.autocomplete, + isRecentlyVisitedSitesEnabledByUser: provider.appSettings.recentlyVisitedSites, + privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + tld: provider.storageCache.tld) { + + case .failure(let error): + Pixel.fire(pixel: .historyStoreLoadFailed, error: error) + if error.isDiskFull { + self.presentInsufficientDiskSpaceAlert() + } else { + self.presentPreemptiveCrashAlert() + } + return NullHistoryManager() + + case .success(let historyManager): + return historyManager + } + } + + private func prepareTabsModel(previewsSource: TabPreviewsSource = TabPreviewsSource(), + appSettings: AppSettings = AppDependencyProvider.shared.appSettings, + isDesktop: Bool = UIDevice.current.userInterfaceIdiom == .pad) -> TabsModel { + let isPadDevice = UIDevice.current.userInterfaceIdiom == .pad + let tabsModel: TabsModel + if AutoClearSettingsModel(settings: appSettings) != nil { + tabsModel = TabsModel(desktop: isPadDevice) + tabsModel.save() + previewsSource.removeAllPreviews() + } else { + if let storedModel = TabsModel.get() { + // Save new model in case of migration + storedModel.save() + tabsModel = storedModel + } else { + tabsModel = TabsModel(desktop: isPadDevice) + } + } + return tabsModel + } + + private func presentPreemptiveCrashAlert() { + Task { @MainActor in + let alertController = CriticalAlerts.makePreemptiveCrashAlert() + window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + } + + private func presentInsufficientDiskSpaceAlert() { + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + window?.rootViewController?.present(alertController, animated: true, completion: nil) + } + + private func presentExpiredEntitlementAlert() { + let alertController = CriticalAlerts.makeExpiredEntitlementAlert { [weak self] in + self?.mainViewController?.segueToPrivacyPro() + } + window?.rootViewController?.present(alertController, animated: true) { [weak self] in + self?.tunnelDefaults.showEntitlementAlert = false + } + } + + private func presentExpiredEntitlementNotificationIfNeeded() { + let presenter = NetworkProtectionNotificationsPresenterTogglableDecorator( + settings: AppDependencyProvider.shared.vpnSettings, + defaults: .networkProtectionGroupDefaults, + wrappee: NetworkProtectionUNNotificationPresenter() + ) + presenter.showEntitlementNotification() + } + + private func cleanUpMacPromoExperiment2() { + UserDefaults.standard.removeObject(forKey: "com.duckduckgo.ios.macPromoMay23.exp2.cohort") + } + + private func cleanUpIncrementalRolloutPixelTest() { + UserDefaults.standard.removeObject(forKey: "network-protection.incremental-feature-flag-test.has-sent-pixel") + } + + private func clearTmp() { + let tmp = FileManager.default.temporaryDirectory + do { + try FileManager.default.removeItem(at: tmp) + } catch { + Logger.general.error("Failed to delete tmp dir") + } + } + + private func reportAdAttribution() { + Task.detached(priority: .background) { + await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() + } + } + + func applicationDidBecomeActive(_ application: UIApplication) { + guard !testing else { return } + + defer { + if let didFinishLaunchingStartTime { + let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime + Pixel.fire(pixel: .appDidBecomeActiveTime(time: Pixel.Event.BucketAggregation(number: launchTime)), + withAdditionalParameters: [PixelParameters.time: String(launchTime)]) + } + } + + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) + syncService.initializeIfNeeded() + syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: syncService) + + if !(overlayWindow?.rootViewController is AuthenticationViewController) { + removeOverlay() + } + + StatisticsLoader.shared.load { + StatisticsLoader.shared.refreshAppRetentionAtb() + self.fireAppLaunchPixel() + self.reportAdAttribution() + self.onboardingPixelReporter.fireEnqueuedPixelsIfNeeded() + } + + if appIsLaunching { + appIsLaunching = false + onApplicationLaunch(application) + } + + mainViewController?.showBars() + mainViewController?.didReturnFromBackground() + + if !privacyStore.authenticationEnabled { + showKeyboardOnLaunch() + } + + if AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + AppConfigurationFetch.shouldScheduleRulesCompilationOnAppLaunch = false + } + AppDependencyProvider.shared.configurationManager.loadPrivacyConfigFromDiskIfNeeded() + + AppConfigurationFetch().start { result in + self.sendAppLaunchPostback() + if case .assetsUpdated(let protectionsUpdated) = result, protectionsUpdated { + ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + } + } + + syncService.scheduler.notifyAppLifecycleEvent() + + privacyProDataReporter?.injectSyncService(syncService) + + fireFailedCompilationsPixelIfNeeded() + + widgetRefreshModel.refreshVPNWidget() + + if tunnelDefaults.showEntitlementAlert { + presentExpiredEntitlementAlert() + } + + presentExpiredEntitlementNotificationIfNeeded() + + Task { + await stopAndRemoveVPNIfNotAuthenticated() + await refreshShortcuts() + await vpnWorkaround.installRedditSessionWorkaround() + + if #available(iOS 17.0, *) { + await VPNSnoozeLiveActivityManager().endSnoozeActivityIfNecessary() + } + } + + AppDependencyProvider.shared.subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in + if isSubscriptionActive { + DailyPixel.fire(pixel: .privacyProSubscriptionActive) + } + } + + Task { + await subscriptionCookieManager.refreshSubscriptionCookie() + } + + let importPasswordsStatusHandler = ImportPasswordsStatusHandler(syncService: syncService) + importPasswordsStatusHandler.checkSyncSuccessStatus() + + Task { + await privacyProDataReporter?.saveWidgetAdded() + } + + AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + } + + private func stopAndRemoveVPNIfNotAuthenticated() async { + // Only remove the VPN if the user is not authenticated, and it's installed: + guard !accountManager.isUserAuthenticated, await AppDependencyProvider.shared.networkProtectionTunnelController.isInstalled else { + return + } + + await AppDependencyProvider.shared.networkProtectionTunnelController.stop() + await AppDependencyProvider.shared.networkProtectionTunnelController.removeVPN(reason: .didBecomeActiveCheck) + } + + func applicationWillResignActive(_ application: UIApplication) { + Task { @MainActor in + await refreshShortcuts() + await vpnWorkaround.removeRedditSessionWorkaround() + } + } + + private func fireAppLaunchPixel() { + + WidgetCenter.shared.getCurrentConfigurations { result in + let paramKeys: [WidgetFamily: String] = [ + .systemSmall: PixelParameters.widgetSmall, + .systemMedium: PixelParameters.widgetMedium, + .systemLarge: PixelParameters.widgetLarge + ] + + switch result { + case .failure(let error): + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [ + PixelParameters.widgetError: "1", + PixelParameters.widgetErrorCode: "\((error as NSError).code)", + PixelParameters.widgetErrorDomain: (error as NSError).domain + ], includedParameters: [.appVersion, .atb]) + + case .success(let widgetInfo): + let params = widgetInfo.reduce([String: String]()) { + var result = $0 + if let key = paramKeys[$1.family] { + result[key] = "1" + } + return result + } + Pixel.fire(pixel: .appLaunch, withAdditionalParameters: params, includedParameters: [.appVersion, .atb]) + } + + } + } + + private func fireFailedCompilationsPixelIfNeeded() { + let store = FailedCompilationsStore() + if store.hasAnyFailures { + DailyPixel.fire(pixel: .compilationFailed, withAdditionalParameters: store.summary) { error in + guard error != nil else { return } + store.cleanup() + } + } + } + + private func shouldShowKeyboardOnLaunch() -> Bool { + guard let date = lastBackgroundDate else { return true } + return Date().timeIntervalSince(date) > AppDelegate.ShowKeyboardOnLaunchThreshold + } + + private func showKeyboardOnLaunch() { + guard KeyboardSettings().onAppLaunch && showKeyboardIfSettingOn && shouldShowKeyboardOnLaunch() else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.mainViewController?.enterSearch() + } + showKeyboardIfSettingOn = false + } + + private func onApplicationLaunch(_ application: UIApplication) { + Task { @MainActor in + await beginAuthentication() + initialiseBackgroundFetch(application) + applyAppearanceChanges() + refreshRemoteMessages() + } + } + + private func applyAppearanceChanges() { + UILabel.appearance(whenContainedInInstancesOf: [UIAlertController.self]).numberOfLines = 0 + } + + /// It's public in order to allow refreshing on demand via Debug menu. Otherwise it shouldn't be called from outside. + func refreshRemoteMessages() { + Task { + try? await remoteMessagingClient.fetchAndProcess(using: remoteMessagingClient.store) + } + } + + func applicationWillEnterForeground(_ application: UIApplication) { + ThemeManager.shared.updateUserInterfaceStyle() + + Task { @MainActor in + await beginAuthentication() + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) + showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } + } + + func applicationDidEnterBackground(_ application: UIApplication) { + displayBlankSnapshotWindow() + autoClear?.startClearingTimer() + lastBackgroundDate = Date() + AppDependencyProvider.shared.autofillLoginSession.endSession() + suspendSync() + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) + privacyProDataReporter?.saveApplicationLastSessionEnded() + resetAppStartTime() + } + + private func resetAppStartTime() { + didFinishLaunchingStartTime = nil + mainViewController?.appDidFinishLaunchingStartTime = nil + } + + private func suspendSync() { + if syncService.isSyncInProgress { + Logger.sync.debug("Sync is in progress. Starting background task to allow it to gracefully complete.") + + var taskID: UIBackgroundTaskIdentifier! + taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { + Logger.sync.debug("Forcing background task completion") + UIApplication.shared.endBackgroundTask(taskID) + } + syncDidFinishCancellable?.cancel() + syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } + .prefix(1) + .receive(on: DispatchQueue.main) + .sink { _ in + Logger.sync.debug("Ending background task") + UIApplication.shared.endBackgroundTask(taskID) + } + } + + syncService.scheduler.cancelSyncAndSuspendSyncQueue() + } + + func application(_ application: UIApplication, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void) { + handleShortCutItem(shortcutItem) + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + Logger.sync.debug("App launched with url \(url.absoluteString)") + + // If showing the onboarding intro ignore deeplinks + guard mainViewController?.needsToShowOnboardingIntro() == false else { + return false + } + + if handleEmailSignUpDeepLink(url) { + return true + } + + NotificationCenter.default.post(name: AutofillLoginListAuthenticator.Notifications.invalidateContext, object: nil) + + // The openVPN action handles the navigation stack on its own and does not need it to be cleared + if url != AppDeepLinkSchemes.openVPN.url { + mainViewController?.clearNavigationStack() + } + + Task { @MainActor in + // Autoclear should have happened by now + showKeyboardIfSettingOn = false + + if !handleAppDeepLink(app, mainViewController, url) { + mainViewController?.loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil, fromExternalLink: true) + } + } + + return true + } + + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + + Logger.lifecycle.debug(#function) + + AppConfigurationFetch().start(isBackgroundFetch: true) { result in + switch result { + case .noData: + completionHandler(.noData) + case .assetsUpdated: + completionHandler(.newData) + } + } + } + + func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + return true + } + + // MARK: private + + private func sendAppLaunchPostback() { + // Attribution support + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + if privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .marketplaceAdPostback) { + marketplaceAdPostbackManager.sendAppLaunchPostback() + } + } + + private func cleanUpATBAndAssignVariant(variantManager: VariantManager, daxDialogs: DaxDialogs) { + let historyMessageManager = HistoryMessageManager() + + AtbAndVariantCleanup.cleanup() + variantManager.assignVariantIfNeeded { _ in + // MARK: perform first time launch logic here + // If it's running UI Tests check if the onboarding should be in a completed state. + if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted { + daxDialogs.dismiss() + } else { + daxDialogs.primeForUse() + } + + // New users don't see the message + historyMessageManager.dismiss() + + // Setup storage for marketplace postback + marketplaceAdPostbackManager.updateReturningUserValue() + } + } + + private func initialiseBackgroundFetch(_ application: UIApplication) { + guard UIApplication.shared.backgroundRefreshStatus == .available else { + return + } + + // BackgroundTasks will automatically replace an existing task in the queue if one with the same identifier is queued, so we should only + // schedule a task if there are none pending in order to avoid the config task getting perpetually replaced. + BGTaskScheduler.shared.getPendingTaskRequests { tasks in + let hasConfigurationTask = tasks.contains { $0.identifier == AppConfigurationFetch.Constants.backgroundProcessingTaskIdentifier } + if !hasConfigurationTask { + AppConfigurationFetch.scheduleBackgroundRefreshTask() + } + + let hasRemoteMessageFetchTask = tasks.contains { $0.identifier == RemoteMessagingClient.Constants.backgroundRefreshTaskIdentifier } + if !hasRemoteMessageFetchTask { + RemoteMessagingClient.scheduleBackgroundRefreshTask() + } + } + } + + private func displayAuthenticationWindow() { + guard overlayWindow == nil, let frame = window?.frame else { return } + overlayWindow = UIWindow(frame: frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + overlayWindow?.rootViewController = AuthenticationViewController.loadFromStoryboard() + overlayWindow?.makeKeyAndVisible() + window?.isHidden = true + } + + private func displayBlankSnapshotWindow() { + guard overlayWindow == nil, let frame = window?.frame else { return } + guard autoClear?.isClearingEnabled ?? false || privacyStore.authenticationEnabled else { return } + + overlayWindow = UIWindow(frame: frame) + overlayWindow?.windowLevel = UIWindow.Level.alert + + let overlay = BlankSnapshotViewController(addressBarPosition: AppDependencyProvider.shared.appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + overlay.delegate = self + + overlayWindow?.rootViewController = overlay + overlayWindow?.makeKeyAndVisible() + window?.isHidden = true + } + + private func beginAuthentication() async { + + guard privacyStore.authenticationEnabled else { return } + + removeOverlay() + displayAuthenticationWindow() + + guard let controller = overlayWindow?.rootViewController as? AuthenticationViewController else { + removeOverlay() + return + } + + await controller.beginAuthentication { [weak self] in + self?.removeOverlay() + self?.showKeyboardOnLaunch() + } + } + + private func tryToObtainOverlayWindow() { + for window in UIApplication.shared.foregroundSceneWindows where window.rootViewController is BlankSnapshotViewController { + overlayWindow = window + return + } + } + + private func removeOverlay() { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + if let overlay = overlayWindow { + overlay.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + } + + private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) { + Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") + + Task { @MainActor in + + if appIsLaunching { + await autoClear?.clearDataIfEnabled() + } else { + await autoClear?.clearDataIfEnabledAndTimeExpired(applicationState: .active) + } + + if shortcutItem.type == AppDelegate.ShortcutKey.clipboard, let query = UIPasteboard.general.string { + mainViewController?.clearNavigationStack() + mainViewController?.loadQueryInNewTab(query) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.passwords { + mainViewController?.clearNavigationStack() + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in + self?.mainViewController?.launchAutofillLogins(openSearch: true, source: .appIconShortcut) + } + Pixel.fire(pixel: .autofillLoginsLaunchAppShortcut) + return + } + + if shortcutItem.type == AppDelegate.ShortcutKey.openVPNSettings { + presentNetworkProtectionStatusSettingsModal() + } + + } + } + + private func removeEmailWaitlistState() { + EmailWaitlist.removeEmailState() + + let autofillStorage = EmailKeychainManager() + try? autofillStorage.deleteWaitlistState() + + // Remove the authentication state if this is a fresh install. + if !Database.shared.isDatabaseFileInitialized { + try? autofillStorage.deleteAuthenticationState() + } + } + + private func handleEmailSignUpDeepLink(_ url: URL) -> Bool { + guard url.absoluteString.starts(with: URL.emailProtection.absoluteString), + let navViewController = mainViewController?.presentedViewController as? UINavigationController, + let emailSignUpViewController = navViewController.topViewController as? EmailSignupViewController else { + return false + } + emailSignUpViewController.loadUrl(url) + return true + } + + private var mainViewController: MainViewController? { + return window?.rootViewController as? MainViewController + } + + private func setUpAutofillPixelReporter() { + autofillPixelReporter = AutofillPixelReporter( + userDefaults: .standard, + autofillEnabled: AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled, + eventMapping: EventMapping {[weak self] event, _, params, _ in + switch event { + case .autofillActiveUser: + Pixel.fire(pixel: .autofillActiveUser) + case .autofillEnabledUser: + Pixel.fire(pixel: .autofillEnabledUser) + case .autofillOnboardedUser: + Pixel.fire(pixel: .autofillOnboardedUser) + case .autofillToggledOn: + Pixel.fire(pixel: .autofillToggledOn, withAdditionalParameters: params ?? [:]) + if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } + case .autofillToggledOff: + Pixel.fire(pixel: .autofillToggledOff, withAdditionalParameters: params ?? [:]) + if let autofillExtensionToggled = self?.autofillUsageMonitor.autofillExtensionEnabled { + Pixel.fire(pixel: autofillExtensionToggled ? .autofillExtensionToggledOn : .autofillExtensionToggledOff, + withAdditionalParameters: params ?? [:]) + } + case .autofillLoginsStacked: + Pixel.fire(pixel: .autofillLoginsStacked, withAdditionalParameters: params ?? [:]) + default: + break + } + }, + installDate: StatisticsUserDefaults().installDate ?? Date()) + + _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, + object: nil, + queue: nil) { [weak self] _ in + self?.autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) + } + } + + @MainActor + func refreshShortcuts() async { + guard AppDependencyProvider.shared.vpnFeatureVisibility.shouldShowVPNShortcut() else { + UIApplication.shared.shortcutItems = nil + return + } + + if case .success(true) = await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .returnCacheDataDontLoad) { + let items = [ + UIApplicationShortcutItem(type: AppDelegate.ShortcutKey.openVPNSettings, + localizedTitle: UserText.netPOpenVPNQuickAction, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "VPN-16"), + userInfo: nil) + ] + + UIApplication.shared.shortcutItems = items + } else { + UIApplication.shared.shortcutItems = nil + } + } +} + + +extension OldAppDelegate: BlankSnapshotViewRecoveringDelegate { + + func recoverFromPresenting(controller: BlankSnapshotViewController) { + if overlayWindow == nil { + tryToObtainOverlayWindow() + } + + overlayWindow?.isHidden = true + overlayWindow = nil + window?.makeKeyAndVisible() + } + +} + +extension OldAppDelegate: UIScreenshotServiceDelegate { + func screenshotService(_ screenshotService: UIScreenshotService, + generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { + guard let webView = mainViewController?.currentTab?.webView else { + completionHandler(nil, 0, .zero) + return + } + + let zoomScale = webView.scrollView.zoomScale + + // The PDF's coordinate space has its origin at the bottom left, so the view's origin.y needs to be converted + let visibleBounds = CGRect( + x: webView.scrollView.contentOffset.x / zoomScale, + y: (webView.scrollView.contentSize.height - webView.scrollView.contentOffset.y - webView.bounds.height) / zoomScale, + width: webView.bounds.width / zoomScale, + height: webView.bounds.height / zoomScale + ) + + webView.createPDF { result in + let data = try? result.get() + completionHandler(data, 0, visibleBounds) + } + } +} + +extension OldAppDelegate: UNUserNotificationCenterDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler(.banner) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + if response.actionIdentifier == UNNotificationDefaultActionIdentifier { + let identifier = response.notification.request.identifier + + if NetworkProtectionNotificationIdentifier(rawValue: identifier) != nil { + presentNetworkProtectionStatusSettingsModal() + } + } + + completionHandler() + } + + func presentNetworkProtectionStatusSettingsModal() { + Task { + if case .success(let hasEntitlements) = await accountManager.hasEntitlement(forProductName: .networkProtection), hasEntitlements { + (window?.rootViewController as? MainViewController)?.segueToVPN() + } else { + (window?.rootViewController as? MainViewController)?.segueToPrivacyPro() + } + } + } + + private func presentSettings(with viewController: UIViewController) { + guard let window = window, let rootViewController = window.rootViewController as? MainViewController else { return } + + if let navigationController = rootViewController.presentedViewController as? UINavigationController { + if let lastViewController = navigationController.viewControllers.last, lastViewController.isKind(of: type(of: viewController)) { + // Avoid presenting dismissing and re-presenting the view controller if it's already visible: + return + } else { + // Otherwise, replace existing view controllers with the presented one: + navigationController.popToRootViewController(animated: false) + navigationController.pushViewController(viewController, animated: false) + return + } + } + + // If the previous checks failed, make sure the nav stack is reset and present the view controller from scratch: + rootViewController.clearNavigationStack() + + // Give the `clearNavigationStack` call time to complete. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { + rootViewController.segueToSettings() + let navigationController = rootViewController.presentedViewController as? UINavigationController + navigationController?.popToRootViewController(animated: false) + navigationController?.pushViewController(viewController, animated: false) + } + } +} From c11ea2184858c83938f705485a38279b6f5c6bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 17 Dec 2024 09:16:24 +0100 Subject: [PATCH 22/27] Fix syncCancellable not being reassigned --- DuckDuckGo/AppLifecycle/AppStates/Background.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 4e74e59c3e..74803da1a3 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -26,8 +26,7 @@ struct Background: AppState { private let lastBackgroundDate: Date = Date() private let application: UIApplication - private let appDependencies: AppDependencies - private var syncDidFinishCancellable: AnyCancellable? + private var appDependencies: AppDependencies var urlToOpen: URL? @@ -82,7 +81,7 @@ struct Background: AppState { UIApplication.shared.endBackgroundTask(taskID) } appDependencies.syncDidFinishCancellable?.cancel() - syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } + appDependencies.syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } .prefix(1) .receive(on: DispatchQueue.main) .sink { _ in From e5af3521084ae71cf428d882bd24193e487f0280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 17 Dec 2024 10:01:16 +0100 Subject: [PATCH 23/27] Fix running shorcuts when app is backgrounded --- DuckDuckGo/AppLifecycle/AppStateTransitions.swift | 5 ++++- DuckDuckGo/AppLifecycle/AppStates/Active.swift | 2 ++ DuckDuckGo/AppLifecycle/AppStates/Background.swift | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index e9e1f213ad..8d419ce7ce 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -103,7 +103,10 @@ extension Background { case .backgrounding: run() return self - case .launching, .suspending, .handleShortcutItem: + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + return self + case .launching, .suspending: return handleUnexpectedEvent(event) } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index a70361b217..bb95b92852 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -103,6 +103,8 @@ struct Active: AppState { if let url = stateContext.urlToOpen { openURL(url) + } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { + handleShortcutItem(shortcutItemToHandle, appIsLaunching: false) } activateApp() diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 74803da1a3..2bceee2389 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -29,6 +29,7 @@ struct Background: AppState { private var appDependencies: AppDependencies var urlToOpen: URL? + var shortcutItemToHandle: UIApplicationShortcutItem? init(stateContext: Inactive.StateContext) { application = stateContext.application @@ -106,6 +107,8 @@ extension Background { let application: UIApplication let lastBackgroundDate: Date let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? + let appDependencies: AppDependencies } @@ -114,6 +117,7 @@ extension Background { .init(application: application, lastBackgroundDate: lastBackgroundDate, urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, appDependencies: appDependencies) } From 8cca2ce0eda7918daf0a5c1b40303e8842c07764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 18 Dec 2024 09:03:14 +0100 Subject: [PATCH 24/27] Introduce a kill switch for the new feature --- DuckDuckGo/AppDelegate.swift | 28 +++++++++++-------- .../AppLifecycle/AppStates/Active.swift | 1 + .../AppLifecycle/AppStates/Background.swift | 10 +++++++ DuckDuckGo/NewAppDelegate.swift | 8 ++++-- DuckDuckGo/OldAppDelegate.swift | 4 +-- DuckDuckGoTests/AppSettingsMock.swift | 2 +- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index de85aa9d11..2f420be64f 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -22,12 +22,12 @@ import Core enum AppBehavior: String { - case existing - case stateMachine + case old + case new } -protocol DDGAppDelegate { +protocol DDGApp { var privacyProDataReporter: PrivacyProDataReporting? { get } @@ -51,18 +51,16 @@ protocol DDGAppDelegate { realDelegate.privacyProDataReporter } + func forceOldAppDelegate() { + BoolFileMarker(name: .forceOldAppDelegate)?.mark() + } + private let appBehavior: AppBehavior = { - guard !ProcessInfo().arguments.contains("testing") else { return .existing } - if let appBehavior = AppDependencyProvider.shared.appSettings.appBehavior { - return appBehavior - } - let appBehavior: AppBehavior = Double.random(in: 0..<1) < 0.2 ? .stateMachine : .existing // 20% of users will run through new flow - AppDependencyProvider.shared.appSettings.appBehavior = appBehavior - return appBehavior + BoolFileMarker(name: .forceOldAppDelegate)?.isPresent == true ? .old : .new }() - private lazy var realDelegate: UIApplicationDelegate & DDGAppDelegate = { - if appBehavior == .existing { + private lazy var realDelegate: UIApplicationDelegate & DDGApp = { + if appBehavior == .old { return OldAppDelegate(with: self) } else { return NewAppDelegate() @@ -161,3 +159,9 @@ extension Error { } } + +private extension BoolFileMarker.Name { + + static let forceOldAppDelegate = BoolFileMarker.Name(rawValue: "force-old-app-delegate") + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index bb95b92852..38d6d208e0 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -121,6 +121,7 @@ struct Active: AppState { private func activateApp(isTesting: Bool = false) { guard !isTesting else { return } // Leaving this as is for now to ensure this code is never executed, regardless of where it's called from. // In the future, we may consider creating separate states specifically for testing purposes. + StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) appDependencies.syncService.initializeIfNeeded() appDependencies.syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: appDependencies.syncService) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 2bceee2389..904eb3bb32 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -21,6 +21,7 @@ import Foundation import Combine import DDGSync import UIKit +import Core struct Background: AppState { @@ -70,6 +71,15 @@ struct Background: AppState { privacyProDataReporter.saveApplicationLastSessionEnded() resetAppStartTime() + + // Kill switch for the new app delegate: + // If the .forceOldAppDelegate flag is set in the config, we mark a file as present. + // This switches the app to the old mode and silently crashes it in the background. + // When reopened, the app will reliably run the old flow. + if ContentBlocking.shared.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .forceOldAppDelegate) { + (UIApplication.shared.delegate as? AppDelegate)?.forceOldAppDelegate() + fatalError() // we silently crash in the background + } } private mutating func suspendSync(syncService: DDGSync) { diff --git a/DuckDuckGo/NewAppDelegate.swift b/DuckDuckGo/NewAppDelegate.swift index 1f341b3389..00f9bf7fc7 100644 --- a/DuckDuckGo/NewAppDelegate.swift +++ b/DuckDuckGo/NewAppDelegate.swift @@ -19,12 +19,12 @@ import UIKit -final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGAppDelegate { +final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGApp { private let appStateMachine: AppStateMachine = AppStateMachine() var privacyProDataReporter: PrivacyProDataReporting? { - (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this antipattern + (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern } func initialize() { } // init code will happen inside AppStateMachine/Init state .init() @@ -35,7 +35,9 @@ final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGAppDelegate { } func applicationDidBecomeActive(_ application: UIApplication) { - appStateMachine.handle(.activating) + if !ProcessInfo().arguments.contains("testing") { + appStateMachine.handle(.activating) + } } func applicationWillResignActive(_ application: UIApplication) { diff --git a/DuckDuckGo/OldAppDelegate.swift b/DuckDuckGo/OldAppDelegate.swift index ee8413394a..a48ae41636 100644 --- a/DuckDuckGo/OldAppDelegate.swift +++ b/DuckDuckGo/OldAppDelegate.swift @@ -1,5 +1,5 @@ // -// AppDelegate.swift +// OldAppDelegate.swift // DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. @@ -42,7 +42,7 @@ import WebKit import os.log @MainActor -final class OldAppDelegate: NSObject, UIApplicationDelegate, DDGAppDelegate { +final class OldAppDelegate: NSObject, UIApplicationDelegate, DDGApp { private var testing = false var appIsLaunching = false diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index 6b73e06dc0..2aea19af93 100644 --- a/DuckDuckGoTests/AppSettingsMock.swift +++ b/DuckDuckGoTests/AppSettingsMock.swift @@ -22,7 +22,7 @@ import Foundation @testable import DuckDuckGo class AppSettingsMock: AppSettings { - var appBehavior: DuckDuckGo.AppBehavior? = .existing + var appBehavior: DuckDuckGo.AppBehavior? = .new var defaultTextZoomLevel: DuckDuckGo.TextZoomLevel = .percent100 From b22ce95e8690faf10f214c8de487374e2b7bb794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 18 Dec 2024 10:51:26 +0100 Subject: [PATCH 25/27] Fix issues --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/AppLifecycle/AppStates/Background.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2b9911e8cf..7450e30815 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11754,8 +11754,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 221.1.0; + branch = "jacek/refactor-app-delegate"; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 05e5067869..23175cdd4f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "e8654e1a51bd20fa8fa234a4155a9aec37411ed1", - "version" : "221.1.0" + "branch" : "jacek/refactor-app-delegate", + "revision" : "3712ccc0b6867e08b6235083f7e754d537f0b5cb" } }, { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index 904eb3bb32..f408b0919d 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -78,7 +78,7 @@ struct Background: AppState { // When reopened, the app will reliably run the old flow. if ContentBlocking.shared.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .forceOldAppDelegate) { (UIApplication.shared.delegate as? AppDelegate)?.forceOldAppDelegate() - fatalError() // we silently crash in the background + fatalError("crash to ensure the app restarts using the old app delegate next time") } } From 0e0412dbb206a5f79c42939fb8e81f5af0f63db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 18 Dec 2024 16:19:46 +0100 Subject: [PATCH 26/27] Add testing state --- Core/PixelEvent.swift | 2 +- DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/AppLifecycle/AppStateMachine.swift | 2 +- .../AppLifecycle/AppStateTransitions.swift | 11 ++- .../AppLifecycle/AppStates/Active.swift | 5 +- .../AppLifecycle/AppStates/Launched.swift | 90 +++++++------------ .../AppLifecycle/AppStates/Testing.swift | 47 ++++++++++ DuckDuckGo/NewAppDelegate.swift | 7 +- 8 files changed, 98 insertions(+), 70 deletions(-) create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Testing.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 29eadee0f7..526bfd9f9e 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -1835,7 +1835,7 @@ extension Pixel.Event { return "m_aichat_no_remote_settings_found-\(settings.lowercased())" // MARK: Lifecycle - case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state" + case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state-2" } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7450e30815..e3559d41ff 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1012,6 +1012,7 @@ CBD4F140279EBFB300B20FD7 /* SwiftUICollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1AEFB02799AA940031AE3D /* SwiftUICollectionViewCell.swift */; }; CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */; }; CBD79F4A2D1061E200DBB45A /* OldAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */; }; + CBD79F4D2D130F6500DBB45A /* Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD79F4C2D130F6300DBB45A /* Testing.swift */; }; CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */; }; CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE029A6741300832877 /* MockBundle.swift */; }; CBECDB6F2CD3DFBE005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */; }; @@ -2917,6 +2918,7 @@ CBD4F13B279EBF4A00B20FD7 /* HomeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMessage.swift; sourceTree = ""; }; CBD79F472D1061D500DBB45A /* NewAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAppDelegate.swift; sourceTree = ""; }; CBD79F492D1061DF00DBB45A /* OldAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldAppDelegate.swift; sourceTree = ""; }; + CBD79F4C2D130F6300DBB45A /* Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Testing.swift; sourceTree = ""; }; CBD7AE812AF6D5B6009052FD /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; CBDD5DDE29A6736A00832877 /* APIHeadersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIHeadersTests.swift; sourceTree = ""; }; CBDD5DE029A6741300832877 /* MockBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBundle.swift; sourceTree = ""; }; @@ -5612,6 +5614,7 @@ CBAD0EFC2CFE1D48006267B8 /* Active.swift */, CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, CBAD0F002CFE1D54006267B8 /* Background.swift */, + CBD79F4C2D130F6300DBB45A /* Testing.swift */, ); path = AppStates; sourceTree = ""; @@ -8089,6 +8092,7 @@ 98DA6ECA2181E41F00E65433 /* ThemeManager.swift in Sources */, F1D43AFC2B99C56000BAB743 /* RootDebugViewController+VanillaBrowser.swift in Sources */, C159DF072A430B60007834BB /* EmailSignupViewController.swift in Sources */, + CBD79F4D2D130F6500DBB45A /* Testing.swift in Sources */, 37A6A8FE2AFD0208008580A3 /* FaviconsFetcherOnboarding.swift in Sources */, F1CA3C3B1F045B65005FADB3 /* Authenticator.swift in Sources */, CBD4F13D279EBFA000B20FD7 /* HomeMessageCollectionViewCell.swift in Sources */, diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index a555782ae0..585f9bfb7c 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -21,7 +21,7 @@ import UIKit enum AppEvent { - case launching(UIApplication) + case launching(UIApplication, isTesting: Bool) case activating case backgrounding case suspending diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 8d419ce7ce..1d6fe1e89a 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -24,7 +24,10 @@ extension Init { func apply(event: AppEvent) -> any AppState { switch event { - case .launching(let application): + case .launching(let application, let isTesting): + if isTesting { + return Testing(application: application) + } return Launched(stateContext: makeStateContext(application: application)) default: return handleUnexpectedEvent(event) @@ -113,6 +116,12 @@ extension Background { } +extension Testing { + + func apply(event: AppEvent) -> any AppState { self } + +} + extension AppEvent { var rawValue: String { diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 38d6d208e0..539febfe95 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -81,7 +81,7 @@ struct Active: AppState { handleShortcutItem(shortcutItemToHandle, appIsLaunching: true) } - activateApp(isTesting: stateContext.isTesting) + activateApp() } // MARK: handle applicationWillEnterForeground(_:) logic here @@ -119,9 +119,6 @@ struct Active: AppState { // MARK: handle applicationDidBecomeActive(_:) logic here private func activateApp(isTesting: Bool = false) { - guard !isTesting else { return } // Leaving this as is for now to ensure this code is never executed, regardless of where it's called from. - // In the future, we may consider creating separate states specifically for testing purposes. - StorageInconsistencyMonitor().didBecomeActive(isProtectedDataAvailable: application.isProtectedDataAvailable) appDependencies.syncService.initializeIfNeeded() appDependencies.syncDataProviders.setUpDatabaseCleanersIfNeeded(syncService: appDependencies.syncService) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift index 15d5c01978..5bcbe14a26 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -38,9 +38,6 @@ import PixelExperimentKit @MainActor struct Launched: AppState { - @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) - private var privacyConfigCustomURL: String? - @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) private var didCrashDuringCrashHandlersSetUp: Bool @@ -66,17 +63,18 @@ struct Launched: AppState { private let crashReportUploaderOnboarding: CrashCollectionOnboarding // These should ideally be let properties instead of force-unwrapped. However, due to various initialization paths, such as database completion blocks, setting them up in advance is currently not feasible. Refactoring will be done once this code is streamlined. - private var uiService: UIService! - private var unService: UNService! - private var syncDataProviders: SyncDataProviders! - private var autoClear: AutoClear! - private var syncService: DDGSync! - private var isSyncInProgressCancellable: AnyCancellable! - private var remoteMessagingClient: RemoteMessagingClient! - private var subscriptionCookieManager: SubscriptionCookieManaging! - private var autofillPixelReporter: AutofillPixelReporter! - private var mainViewController: MainViewController! - private var window: UIWindow? + private let uiService: UIService + private let unService: UNService + private let syncDataProviders: SyncDataProviders + private let syncService: DDGSync + private let isSyncInProgressCancellable: AnyCancellable + private let remoteMessagingClient: RemoteMessagingClient + private let subscriptionCookieManager: SubscriptionCookieManaging + private let autofillPixelReporter: AutofillPixelReporter + private let window: UIWindow + + private var mainViewController: MainViewController? + private var autoClear: AutoClear? var urlToOpen: URL? var shortcutItemToHandle: UIApplicationShortcutItem? @@ -86,6 +84,9 @@ struct Launched: AppState { // swiftlint:disable:next cyclomatic_complexity init(stateContext: Init.StateContext) { + @UserDefaultsWrapper(key: .privacyConfigCustomURL, defaultValue: nil) + var privacyConfigCustomURL: String? + application = stateContext.application privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) vpnWorkaround = VPNRedditSessionWorkaround(accountManager: accountManager, tunnelController: tunnelController) @@ -177,29 +178,6 @@ struct Launched: AppState { } _ = DefaultUserAgentManager.shared - if isTesting { - Pixel.isDryRun = true - _ = DefaultUserAgentManager.shared - Database.shared.loadStore { _, _ in } - _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) - - window = UIWindow(frame: UIScreen.main.bounds) - window!.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() - - let blockingDelegate = BlockingNavigationDelegate() - let webView = blockingDelegate.prepareWebView() - window!.rootViewController?.view.addSubview(webView) - window!.rootViewController?.view.backgroundColor = .red - webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) - - application.setWindow(window!) - - let request = URLRequest(url: URL(string: "about:blank")!) - webView.load(request) - - return - } - removeEmailWaitlistState() func removeEmailWaitlistState() { @@ -405,18 +383,14 @@ struct Launched: AppState { privacyProDataReporter.injectTabsModel(tabsModel) if shouldPresentInsufficientDiskSpaceAlertAndCrash { - window = UIWindow(frame: UIScreen.main.bounds) - window!.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, + window.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, voiceSearchHelper: voiceSearchHelper) - window!.makeKeyAndVisible() + window.makeKeyAndVisible() application.setWindow(window) - presentInsufficientDiskSpaceAlert() - func presentInsufficientDiskSpaceAlert() { - let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() - window!.rootViewController?.present(alertController, animated: true, completion: nil) - } + let alertController = CriticalAlerts.makeInsufficientDiskSpaceAlert() + window.rootViewController?.present(alertController, animated: true, completion: nil) } else { let daxDialogsFactory = ExperimentContextualDaxDialogsFactory(contextualOnboardingLogic: daxDialogs, contextualOnboardingPixelReporter: onboardingPixelReporter) let contextualOnboardingPresenter = ContextualOnboardingPresenter(variantManager: variantManager, daxDialogsFactory: daxDialogsFactory) @@ -444,15 +418,15 @@ struct Launched: AppState { websiteDataManager: Self.makeWebsiteDataManager(fireproofing: fireproofing), appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) - mainViewController.loadViewIfNeeded() + mainViewController!.loadViewIfNeeded() syncErrorHandler.alertPresenter = mainViewController window = UIWindow(frame: UIScreen.main.bounds) - window!.rootViewController = mainViewController - window!.makeKeyAndVisible() - application.setWindow(window!) + window.rootViewController = mainViewController + window.makeKeyAndVisible() + application.setWindow(window) - let autoClear = AutoClear(worker: mainViewController) + let autoClear = AutoClear(worker: mainViewController!) self.autoClear = autoClear let applicationState = application.applicationState let vpnWorkaround = vpnWorkaround @@ -461,8 +435,8 @@ struct Launched: AppState { await vpnWorkaround.installRedditSessionWorkaround() } } - unService = UNService(window: window!, accountManager: accountManager) - uiService = UIService(window: window!) + unService = UNService(window: window, accountManager: accountManager) + uiService = UIService(window: window) voiceSearchHelper.migrateSettingsFlagIfNecessary() @@ -472,8 +446,8 @@ struct Launched: AppState { UNUserNotificationCenter.current().delegate = unService - window!.windowScene?.screenshotService?.delegate = uiService - ThemeManager.shared.updateUserInterfaceStyle(window: window!) + window.windowScene?.screenshotService?.delegate = uiService + ThemeManager.shared.updateUserInterfaceStyle(window: window) // Temporary logic for rollout of Autofill as on by default for new installs only if AppDependencyProvider.shared.appSettings.autofillIsNewInstallForOnByDefault == nil { @@ -521,7 +495,7 @@ struct Launched: AppState { _ = NotificationCenter.default.addObserver(forName: AppUserDefaults.Notifications.autofillEnabledChange, object: nil, queue: nil) { [autofillPixelReporter] _ in - autofillPixelReporter?.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) + autofillPixelReporter.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) } if stateContext.didCrashDuringCrashHandlersSetUp { @@ -540,9 +514,9 @@ struct Launched: AppState { appSettings: appSettings, privacyStore: privacyStore, uiService: uiService, - mainViewController: mainViewController, + mainViewController: mainViewController!, voiceSearchHelper: voiceSearchHelper, - autoClear: autoClear, + autoClear: autoClear!, autofillLoginSession: autofillLoginSession, marketplaceAdPostbackManager: marketplaceAdPostbackManager, syncService: syncService, @@ -647,7 +621,6 @@ extension Launched { struct StateContext { let application: UIApplication - let isTesting: Bool let didFinishLaunchingStartTime: CFAbsoluteTime let urlToOpen: URL? let shortcutItemToHandle: UIApplicationShortcutItem? @@ -657,7 +630,6 @@ extension Launched { func makeStateContext() -> StateContext { .init(application: application, - isTesting: isTesting, didFinishLaunchingStartTime: didFinishLaunchingStartTime, urlToOpen: urlToOpen, shortcutItemToHandle: shortcutItemToHandle, diff --git a/DuckDuckGo/AppLifecycle/AppStates/Testing.swift b/DuckDuckGo/AppLifecycle/AppStates/Testing.swift new file mode 100644 index 0000000000..2363721731 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Testing.swift @@ -0,0 +1,47 @@ +// +// Testing.swift +// DuckDuckGo +// +// 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 Core +import UIKit + +@MainActor +struct Testing: AppState { + + init(application: UIApplication) { + Pixel.isDryRun = true + _ = DefaultUserAgentManager.shared + Database.shared.loadStore { _, _ in } + _ = BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: BookmarksDatabase.make()) + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = UIStoryboard.init(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() + + let blockingDelegate = BlockingNavigationDelegate() + let webView = blockingDelegate.prepareWebView() + window.rootViewController?.view.addSubview(webView) + window.rootViewController?.view.backgroundColor = .red + webView.frame = CGRect(x: 10, y: 10, width: 300, height: 300) + + application.setWindow(window) + + let request = URLRequest(url: URL(string: "about:blank")!) + webView.load(request) + } + +} diff --git a/DuckDuckGo/NewAppDelegate.swift b/DuckDuckGo/NewAppDelegate.swift index 00f9bf7fc7..6d6adc6bc8 100644 --- a/DuckDuckGo/NewAppDelegate.swift +++ b/DuckDuckGo/NewAppDelegate.swift @@ -22,6 +22,7 @@ import UIKit final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGApp { private let appStateMachine: AppStateMachine = AppStateMachine() + private let isTesting: Bool = ProcessInfo().arguments.contains("testing") var privacyProDataReporter: PrivacyProDataReporting? { (appStateMachine.currentState as? Active)?.appDependencies.privacyProDataReporter // just for now, we have to get rid of this anti pattern @@ -30,14 +31,12 @@ final class NewAppDelegate: NSObject, UIApplicationDelegate, DDGApp { func initialize() { } // init code will happen inside AppStateMachine/Init state .init() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - appStateMachine.handle(.launching(application)) + appStateMachine.handle(.launching(application, isTesting: isTesting)) return true } func applicationDidBecomeActive(_ application: UIApplication) { - if !ProcessInfo().arguments.contains("testing") { - appStateMachine.handle(.activating) - } + appStateMachine.handle(.activating) } func applicationWillResignActive(_ application: UIApplication) { From 785e6da0566b34428abe96c0a05d1a78fe0a13c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Wed, 18 Dec 2024 16:25:41 +0100 Subject: [PATCH 27/27] Fix leftover comments --- DuckDuckGo/AppLifecycle/AppStates/Active.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift index 539febfe95..42afc268c4 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -68,7 +68,7 @@ struct Active: AppState { } // onApplicationLaunch code - Task { @MainActor [self] in // is capturing self here ok? + Task { @MainActor [self] in await beginAuthentication() initialiseBackgroundFetch(application) applyAppearanceChanges() @@ -94,7 +94,7 @@ struct Active: AppState { let uiService = appDependencies.uiService let syncService = appDependencies.syncService let autoClear = appDependencies.autoClear - Task { @MainActor [self] in // is capturing self here ok? + Task { @MainActor [self] in await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) uiService.showKeyboardIfSettingOn = true