diff --git a/Core/Pixel.swift b/Core/Pixel.swift index 3f95d46454..ded3c22a2c 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -165,16 +165,6 @@ public struct PixelParameters { public static let appState = "state" public static let appEvent = "event" - - public static let firstBackgroundTimestamp = "firstBackgroundTimestamp" - public static let secondBackgroundTimestamp = "secondBackgroundTimestamp" - public static let didReceiveMemoryWarningTimestamp = "didReceiveMemoryWarningTimestamp" - public static let didReceiveMXPayloadTimestamp = "didReceiveMXPayloadTimestamp" - public static let didReceiveUNNotification = "didReceiveUNNotification" - public static let didStartRemoteMessagingClientBackgroundTask = "didStartRemoteMessagingClientBackgroundTask" - public static let didStartAppConfigurationFetchBackgroundTask = "didStartAppConfigurationFetchBackgroundTask" - public static let didPerformFetchTimestamp = "didPerformFetchTimestamp" - public static let numberOfBackgrounds = "numberOfBackgrounds" } public struct PixelValues { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 16091cf0c2..4a924e8c0f 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -947,7 +947,6 @@ extension Pixel { // MARK: Lifecycle case appDidTransitionToUnexpectedState - case appDidConsecutivelyBackground } } @@ -1891,8 +1890,7 @@ extension Pixel.Event { case .openAIChatFromAddressBar: return "m_aichat_addressbar_icon" // MARK: Lifecycle - case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state" - case .appDidConsecutivelyBackground: return "m_debug_app-did-consecutively-background" + 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 91b6c160d0..101fbe8b6d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -974,7 +974,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 */; }; - CB3C78912D08484800A7E4ED /* InactiveBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3C78902D08483F00A7E4ED /* InactiveBackground.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 */; }; @@ -990,13 +996,11 @@ 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 */; }; + 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 */; }; @@ -1006,6 +1010,9 @@ 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 */; }; + 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 */; }; @@ -2871,7 +2878,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 = ""; }; @@ -2899,12 +2905,20 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; + 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 = ""; }; @@ -5600,7 +5614,7 @@ CBAD0EFC2CFE1D48006267B8 /* Active.swift */, CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, CBAD0F002CFE1D54006267B8 /* Background.swift */, - CB3C78902D08483F00A7E4ED /* InactiveBackground.swift */, + CBD79F4C2D130F6300DBB45A /* Testing.swift */, ); path = AppStates; sourceTree = ""; @@ -5615,6 +5629,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 = ( @@ -6506,10 +6530,15 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F0E2D006291006267B8 /* AppServices */, + CBAD0F0B2CFF4EDD006267B8 /* AppDependencies.swift */, + CBAD0F092CFF4185006267B8 /* AppShortcuts.swift */, CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, 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 */, @@ -7684,7 +7713,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 */, @@ -7704,6 +7732,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 */, @@ -7721,7 +7750,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 */, @@ -7758,7 +7786,15 @@ 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 */, + CBD79F482D1061DA00DBB45A /* NewAppDelegate.swift in Sources */, 854A01332A558B3A00FCC628 /* UIView+Constraints.swift in Sources */, 9FB0271B2C2927D0009EA190 /* OnboardingView.swift in Sources */, C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */, @@ -7788,7 +7824,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 */, @@ -7828,7 +7863,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 */, @@ -7849,6 +7883,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 */, @@ -7924,7 +7959,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,6 +8051,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 */, @@ -8025,6 +8060,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 */, @@ -8036,6 +8072,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 */, @@ -8055,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 */, @@ -8118,7 +8156,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 */, @@ -8202,11 +8239,11 @@ 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 */, 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 */, @@ -8218,7 +8255,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/AppConfigurationFetch.swift b/DuckDuckGo/AppConfigurationFetch.swift index 38d012b81e..fcf1664d94 100644 --- a/DuckDuckGo/AppConfigurationFetch.swift +++ b/DuckDuckGo/AppConfigurationFetch.swift @@ -85,8 +85,6 @@ class AppConfigurationFetch { return Date().timeIntervalSince(Self.lastConfigurationRefreshDate) > Constants.minimumConfigurationRefreshInterval } - static var didStartBackgroundTaskTimestamp: Date? - enum BackgroundRefreshCompletionStatus { case expired @@ -134,7 +132,6 @@ class AppConfigurationFetch { static func registerBackgroundRefreshTaskHandler() { BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundProcessingTaskIdentifier, using: nil) { (task) in - didStartBackgroundTaskTimestamp = Date() guard shouldRefresh else { task.setTaskCompleted(success: true) scheduleBackgroundRefreshTask() 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 e13e49f8f3..2f420be64f 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -18,909 +18,94 @@ // 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 -@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - private static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) - private 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" - } - - private var testing = false - var appIsLaunching = false - var overlayWindow: UIWindow? - var window: UIWindow? - - 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(crashReportSender: CrashReportSender(platform: .iOS, - pixelEvents: CrashReportSender.pixelEvents), - crashCollectionStorage: UserDefaults()) - 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 let appStateMachine = AppStateMachine() - - override init() { - super.init() - - 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, launchOptions: launchOptions)) - 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 { [weak self] pixelParameters, payloads, sendReport in - self?.didReceiveMXPayloadTimestamp = Date() - 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) +enum AppBehavior: String { - let previewsSource = TabPreviewsSource() - let historyManager = makeHistoryManager() - let tabsModel = prepareTabsModel(previewsSource: previewsSource) + case old + case new - privacyProDataReporter.injectTabsModel(tabsModel) - - if shouldPresentInsufficientDiskSpaceAlertAndCrash { - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = BlankSnapshotViewController(appSettings: AppDependencyProvider.shared.appSettings, - 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() +protocol DDGApp { - if didCrashDuringCrashHandlersSetUp { - Pixel.fire(pixel: .crashOnCrashHandlersSetUp) - didCrashDuringCrashHandlersSetUp = false - } + var privacyProDataReporter: PrivacyProDataReporting? { get } + + func initialize() + func refreshRemoteMessages() - tipKitAppEventsHandler.appDidFinishLaunching() +} - return true - } +@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - private func makeWebsiteDataManager(fireproofing: Fireproofing, - dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { - return WebCacheManager(cookieStorage: MigratableCookieStorage(), - fireproofing: fireproofing, - dataStoreIDManager: dataStoreIDManager) + static let ShowKeyboardOnLaunchThreshold = TimeInterval(20) + 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" } - private func makeTextZoomCoordinator() -> TextZoomCoordinator { - let provider = AppDependencyProvider.shared - let storage = TextZoomStorage() + var window: UIWindow? - return TextZoomCoordinator(appSettings: provider.appSettings, - storage: storage, - featureFlagger: provider.featureFlagger) + var privacyProDataReporter: PrivacyProDataReporting? { + realDelegate.privacyProDataReporter } - 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 + func forceOldAppDelegate() { + BoolFileMarker(name: .forceOldAppDelegate)?.mark() } - 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 let appBehavior: AppBehavior = { + BoolFileMarker(name: .forceOldAppDelegate)?.isPresent == true ? .old : .new + }() - 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() + private lazy var realDelegate: UIApplicationDelegate & DDGApp = { + if appBehavior == .old { + 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 } func applicationDidBecomeActive(_ application: UIApplication) { - guard !testing else { return } - - appStateMachine.handle(.activating(application)) - - 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) { - appStateMachine.handle(.suspending(application)) - 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) { - 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) { - appStateMachine.handle(.backgrounding(application)) - 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) { - handleShortCutItem(shortcutItem) + realDelegate.application?(application, performActionFor: shortcutItem, completionHandler: completionHandler) } 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)) - - // 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) { Logger.lifecycle.debug(#function) - didPerformFetchTimestamp = Date() + AppConfigurationFetch().start(isBackgroundFetch: true) { result in switch result { case .noData: @@ -935,353 +120,11 @@ import os.log 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(appSettings: AppDependencyProvider.shared.appSettings, 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 - 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 - } - } - - var didReceiveMemoryWarningTimestamp: Date? - func applicationDidReceiveMemoryWarning(_ application: UIApplication) { - didReceiveMemoryWarningTimestamp = Date() - } - var didReceiveMXPayloadTimestamp: Date? - var didReceiveUNNotificationTimestamp: Date? - var didStartRemoteMessagingClientBackgroundTaskTimestamp: Date? { - remoteMessagingClient.didStartBackgroundTaskTimestamp - } - var didStartAppConfigurationFetchBackgroundTaskTimestamp: Date? { - AppConfigurationFetch.didStartBackgroundTaskTimestamp - } - - var didPerformFetchTimestamp: Date? -} - -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) { - didReceiveUNNotificationTimestamp = Date() - 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 { @@ -1300,7 +143,7 @@ extension DataStoreWarmup.ApplicationState { } } -private extension Error { +extension Error { var isDiskFull: Bool { let nsError = self as NSError @@ -1316,3 +159,9 @@ private extension Error { } } + +private extension BoolFileMarker.Name { + + static let forceOldAppDelegate = BoolFileMarker.Name(rawValue: "force-old-app-delegate") + +} diff --git a/DuckDuckGo/AppDependencies.swift b/DuckDuckGo/AppDependencies.swift new file mode 100644 index 0000000000..4c3cb5df0e --- /dev/null +++ b/DuckDuckGo/AppDependencies.swift @@ -0,0 +1,58 @@ +// +// 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 +import UIKit +import Core +import DDGSync +import Combine +import BrowserServicesKit + +struct AppDependencies { + + let accountManager: AccountManager + let vpnWorkaround: VPNRedditSessionWorkaround + let vpnFeatureVisibility: DefaultNetworkProtectionVisibility + + let appSettings: AppSettings + let privacyStore: PrivacyUserDefaults + + let uiService: UIService + let mainViewController: MainViewController + + let voiceSearchHelper: VoiceSearchHelper + let autoClear: AutoClear + let autofillLoginSession: AutofillLoginSession + let marketplaceAdPostbackManager: MarketplaceAdPostbackManaging + let syncService: DDGSync + let syncDataProviders: SyncDataProviders + let isSyncInProgressCancellable: AnyCancellable + let privacyProDataReporter: PrivacyProDataReporting + let remoteMessagingClient: RemoteMessagingClient + + let subscriptionService: SubscriptionService + + let onboardingPixelReporter: OnboardingPixelReporter + let widgetRefreshModel: NetworkProtectionWidgetRefreshModel + let autofillPixelReporter: AutofillPixelReporter + let crashReportUploaderOnboarding: CrashCollectionOnboarding + + var syncDidFinishCancellable: AnyCancellable? + +} diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift index 53fb3b1bf0..585f9bfb7c 100644 --- a/DuckDuckGo/AppLifecycle/AppStateMachine.swift +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -21,27 +21,30 @@ import UIKit enum AppEvent { - case launching(UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) - case activating(UIApplication) - case backgrounding(UIApplication) - case suspending(UIApplication) + case launching(UIApplication, isTesting: Bool) + case activating + case backgrounding + case suspending case openURL(URL) + case handleShortcutItem(UIApplicationShortcutItem) } 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() diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift index 637e7b0a09..1d6fe1e89a 100644 --- a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -24,8 +24,11 @@ extension Init { func apply(event: AppEvent) -> any AppState { switch event { - case .launching(let application, let launchOptions): - return Launched(application: application, launchOptions: launchOptions) + case .launching(let application, let isTesting): + if isTesting { + return Testing(application: application) + } + return Launched(stateContext: makeStateContext(application: application)) default: return handleUnexpectedEvent(event) } @@ -35,14 +38,18 @@ extension Init { extension Launched { - func apply(event: AppEvent) -> any AppState { + mutating func apply(event: AppEvent) -> any AppState { switch event { - case .activating(let application): - return Active(application: application) - case .openURL: + case .activating: + return Active(stateContext: makeStateContext()) + case .openURL(let url): + urlToOpen = url + return self + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem return self case .backgrounding: - return InactiveBackground() + return Background(stateContext: makeStateContext()) case .launching, .suspending: return handleUnexpectedEvent(event) } @@ -54,9 +61,13 @@ extension Active { func apply(event: AppEvent) -> any AppState { switch event { - case .suspending(let application): - return Inactive(application: application) - case .openURL: + case .suspending: + return Inactive(stateContext: makeStateContext()) + case .openURL(let url): + openURL(url) + return self + case .handleShortcutItem(let shortcutItem): + handleShortcutItem(shortcutItem) return self case .launching, .activating, .backgrounding: return handleUnexpectedEvent(event) @@ -67,15 +78,16 @@ extension Active { extension Inactive { - func apply(event: AppEvent) -> any AppState { + mutating 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 .openURL: + case .backgrounding: + return Background(stateContext: makeStateContext()) + case .activating: + return Active(stateContext: makeStateContext()) + case .openURL(let url): + urlToOpen = url return self - case .launching, .suspending: + case .launching, .suspending, .handleShortcutItem: return handleUnexpectedEvent(event) } } @@ -84,14 +96,19 @@ extension Inactive { extension Background { - func apply(event: AppEvent) -> any AppState { + mutating func apply(event: AppEvent) -> any AppState { switch event { - case .activating(let application): - return Active(application: application) - case .openURL: + case .activating: + return Active(stateContext: makeStateContext()) + case .openURL(let url): + urlToOpen = url return self case .backgrounding: - return DoubleBackground(previousDidEnterBackgroundTimestamp: timestamp, counter: 0) + run() + return self + case .handleShortcutItem(let shortcutItem): + shortcutItemToHandle = shortcutItem + return self case .launching, .suspending: return handleUnexpectedEvent(event) } @@ -99,36 +116,9 @@ extension Background { } -extension DoubleBackground { - - func apply(event: AppEvent) -> any AppState { - switch event { - case .activating(let application): - return Active(application: application) - case .suspending(let application): - return Inactive(application: application) - case .backgrounding(let application): - return DoubleBackground(previousDidEnterBackgroundTimestamp: currentDidEnterBackgroundTimestamp, counter: counter) - case .launching, .openURL: - return self - } - - } - -} - -extension InactiveBackground { +extension Testing { - func apply(event: AppEvent) -> any AppState { - switch event { - case .activating(let application): - return Active(application: application) - case .suspending(let application): - return Inactive(application: application) - case .launching, .backgrounding, .openURL: - return self - } - } + func apply(event: AppEvent) -> any AppState { self } } @@ -141,6 +131,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 df99c36d50..42afc268c4 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Active.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -17,12 +17,500 @@ // limitations under the License. // +import Foundation import UIKit +import BrowserServicesKit +import Core +import WidgetKit +import BackgroundTasks +import Subscription +import NetworkProtection struct Active: AppState { - init(application: UIApplication) { + let application: UIApplication + let appDependencies: AppDependencies + private let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + private let tunnelDefaults = UserDefaults.networkProtectionGroupDefaults + + 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)]) + } + + // 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 + await beginAuthentication() + initialiseBackgroundFetch(application) + applyAppearanceChanges() + refreshRemoteMessages(remoteMessagingClient: appDependencies.remoteMessagingClient) + } + + if let url = stateContext.urlToOpen { + openURL(url) + } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { + handleShortcutItem(shortcutItemToHandle, appIsLaunching: true) + } + + activateApp() + } + + // MARK: handle applicationWillEnterForeground(_:) logic here + init(stateContext: Background.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + ThemeManager.shared.updateUserInterfaceStyle() + + let uiService = appDependencies.uiService + let syncService = appDependencies.syncService + let autoClear = appDependencies.autoClear + Task { @MainActor [self] in + await beginAuthentication(lastBackgroundDate: stateContext.lastBackgroundDate) + await autoClear.clearDataIfEnabledAndTimeExpired(applicationState: .active) + uiService.showKeyboardIfSettingOn = true + syncService.scheduler.resumeSyncQueue() + } + + if let url = stateContext.urlToOpen { + openURL(url) + } else if let shortcutItemToHandle = stateContext.shortcutItemToHandle { + handleShortcutItem(shortcutItemToHandle, appIsLaunching: false) + } + + activateApp() + } + + init(stateContext: Inactive.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + activateApp() + } + + // MARK: handle applicationDidBecomeActive(_:) logic here + private func activateApp(isTesting: Bool = false) { + 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) { + Logger.sync.debug("App launched with url \(url.absoluteString)") + // If showing the onboarding intro ignore deeplinks + guard mainViewController.needsToShowOnboardingIntro() == false else { + return + } + + if handleEmailSignUpDeepLink(url) { + return + } + + 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 + 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) + } + } + + 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 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 + 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() + } + } + } + + func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem, appIsLaunching: Bool = false) { + Logger.general.debug("Handling shortcut item: \(shortcutItem.type)") + 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 { + 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) { [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 { + + struct StateContext { + + let application: UIApplication + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init( + application: application, + appDependencies: appDependencies + ) } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift index d3014c417e..f408b0919d 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Background.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -17,74 +17,118 @@ // limitations under the License. // +import Foundation +import Combine +import DDGSync import UIKit import Core struct Background: AppState { - let timestamp = Date() + private let lastBackgroundDate: Date = Date() + private let application: UIApplication + private var appDependencies: AppDependencies - init(application: UIApplication) { + var urlToOpen: URL? + var shortcutItemToHandle: UIApplicationShortcutItem? + init(stateContext: Inactive.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + urlToOpen = stateContext.urlToOpen + + run() } -} + init(stateContext: Launched.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + urlToOpen = stateContext.urlToOpen -struct DoubleBackground: AppState { + run() + } - private let dateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter - }() + mutating func run() { + 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 - let currentDidEnterBackgroundTimestamp: Date - var counter: Int + if autoClear.isClearingEnabled || privacyStore.authenticationEnabled { + uiService.displayBlankSnapshotWindow(voiceSearchHelper: voiceSearchHelper, + addressBarPosition: appSettings.currentAddressBarPosition) + } + autoClear.startClearingTimer() + autofillLoginSession.endSession() - init(previousDidEnterBackgroundTimestamp: Date, counter: Int) { - self.currentDidEnterBackgroundTimestamp = Date() - self.counter = counter + 1 + suspendSync(syncService: syncService) + syncDataProviders.bookmarksAdapter.cancelFaviconsFetching(application) + privacyProDataReporter.saveApplicationLastSessionEnded() - var parameters = [ - PixelParameters.firstBackgroundTimestamp: dateFormatter.string(from: previousDidEnterBackgroundTimestamp), - PixelParameters.secondBackgroundTimestamp: dateFormatter.string(from: currentDidEnterBackgroundTimestamp) - ] + resetAppStartTime() - if counter < 5 { - parameters[PixelParameters.numberOfBackgrounds] = String(counter) + // 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("crash to ensure the app restarts using the old app delegate next time") } + } - func isValid(timestamp: Date) -> Bool { - timestamp >= previousDidEnterBackgroundTimestamp && timestamp <= currentDidEnterBackgroundTimestamp - } + 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.") - if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - if let didReceiveMemoryWarningTimestamp = appDelegate.didReceiveMemoryWarningTimestamp, - isValid(timestamp: didReceiveMemoryWarningTimestamp) { - parameters[PixelParameters.didReceiveMemoryWarningTimestamp] = dateFormatter.string(from: didReceiveMemoryWarningTimestamp) - } - if let didReceiveMXPayloadTimestamp = appDelegate.didReceiveMXPayloadTimestamp, - isValid(timestamp: didReceiveMXPayloadTimestamp) { - parameters[PixelParameters.didReceiveMXPayloadTimestamp] = dateFormatter.string(from: didReceiveMXPayloadTimestamp) - } - if let didReceiveUNNotificationTimestamp = appDelegate.didReceiveUNNotificationTimestamp, - isValid(timestamp: didReceiveUNNotificationTimestamp) { - parameters[PixelParameters.didReceiveUNNotification] = dateFormatter.string(from: didReceiveUNNotificationTimestamp) - } - if let didStartRemoteMessagingClientBackgroundTaskTimestamp = appDelegate.didStartRemoteMessagingClientBackgroundTaskTimestamp, - isValid(timestamp: didStartRemoteMessagingClientBackgroundTaskTimestamp) { - parameters[PixelParameters.didStartRemoteMessagingClientBackgroundTask] = dateFormatter.string(from: didStartRemoteMessagingClientBackgroundTaskTimestamp) - } - if let didStartAppConfigurationFetchBackgroundTaskTimestamp = appDelegate.didStartAppConfigurationFetchBackgroundTaskTimestamp, - isValid(timestamp: didStartAppConfigurationFetchBackgroundTaskTimestamp) { - parameters[PixelParameters.didStartAppConfigurationFetchBackgroundTask] = dateFormatter.string(from: didStartAppConfigurationFetchBackgroundTaskTimestamp) - } - if let didPerformFetchTimestamp = appDelegate.didPerformFetchTimestamp, - isValid(timestamp: didPerformFetchTimestamp) { - parameters[PixelParameters.didPerformFetchTimestamp] = dateFormatter.string(from: didPerformFetchTimestamp) + var taskID: UIBackgroundTaskIdentifier! + taskID = UIApplication.shared.beginBackgroundTask(withName: "Cancelled Sync Completion Task") { + Logger.sync.debug("Forcing background task completion") + UIApplication.shared.endBackgroundTask(taskID) } + appDependencies.syncDidFinishCancellable?.cancel() + appDependencies.syncDidFinishCancellable = syncService.isSyncInProgressPublisher.filter { !$0 } + .prefix(1) + .receive(on: DispatchQueue.main) + .sink { _ in + Logger.sync.debug("Ending background task") + UIApplication.shared.endBackgroundTask(taskID) + } } - Pixel.fire(pixel: .appDidConsecutivelyBackground, withAdditionalParameters: parameters) + + syncService.scheduler.cancelSyncAndSuspendSyncQueue() + } + + private func resetAppStartTime() { + appDependencies.mainViewController.appDidFinishLaunchingStartTime = nil + } + +} + +extension Background { + + struct StateContext { + + let application: UIApplication + let lastBackgroundDate: Date + let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? + + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + lastBackgroundDate: lastBackgroundDate, + urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, + appDependencies: appDependencies) } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift index 888ef34e09..10837bd2c8 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -21,8 +21,41 @@ import UIKit struct Inactive: AppState { - init(application: UIApplication) { + private let application: UIApplication + private let appDependencies: AppDependencies + var urlToOpen: URL? + + init(stateContext: Active.StateContext) { + application = stateContext.application + appDependencies = stateContext.appDependencies + + 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 urlToOpen: URL? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + urlToOpen: urlToOpen, + appDependencies: appDependencies) } } diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift index d68d714ea5..5054d8f27e 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Init.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -17,6 +17,38 @@ // limitations under the License. // +import Core +import Crashes +import UIKit + +@MainActor struct Init: AppState { + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + var didCrashDuringCrashHandlersSetUp: Bool + + init() { + if !didCrashDuringCrashHandlersSetUp { + didCrashDuringCrashHandlersSetUp = true + CrashLogMessageExtractor.setUp(swapCxaThrow: false) + didCrashDuringCrashHandlersSetUp = false + } + } + +} + +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 674bcf0b91..981e1bb5c8 100644 --- a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -17,12 +17,637 @@ // 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 +import PixelKit +import PixelExperimentKit +@MainActor struct Launched: AppState { - init(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + @UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false) + private var didCrashDuringCrashHandlersSetUp: Bool + private let crashCollection = CrashCollection(crashReportSender: CrashReportSender(platform: .iOS, + pixelEvents: CrashReportSender.pixelEvents), + crashCollectionStorage: UserDefaults()) + 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 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 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 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? + + private let application: UIApplication + + // 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) + crashReportUploaderOnboarding = CrashCollectionOnboarding(appSettings: AppDependencyProvider.shared.appSettings) + + defer { + 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 = { [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 + + // Can be removed after a couple of versions + 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) { + Configuration.setURLProvider(CustomConfigurationURLProvider(customPrivacyConfigurationURL: url)) + } else { + Configuration.setURLProvider(AppConfigurationURLProvider()) + } + + 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) + } else { + DailyPixel.fireDaily(.dbCrashDetectedDaily) + } + } + + // Async dispatch because rootViewController may otherwise be nil here + DispatchQueue.main.async { + guard let viewController = application.window?.rootViewController else { return } + crashReportUploaderOnboarding.presentOnboardingIfNeeded(for: payloads, from: viewController, sendReport: sendReport) + } + } + + 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 + 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 { + + let parameters = [PixelParameters.applicationState: "\(stateContext.application.applicationState.rawValue)", + PixelParameters.dataAvailability: "\(stateContext.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, + 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 + 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 + ) + + 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 = Self.makeSubscriptionCookieManager(application: application) + + let homePageConfiguration = HomePageConfiguration(variantManager: AppDependencyProvider.shared.variantManager, + remoteMessagingClient: remoteMessagingClient, + privacyProDataReporter: privacyProDataReporter) + + + let previewsSource = TabPreviewsSource() + let historyManager = Self.makeHistoryManager() + let tabsModel = Self.prepareTabsModel(previewsSource: previewsSource) + + privacyProDataReporter.injectTabsModel(tabsModel) + + if shouldPresentInsufficientDiskSpaceAlertAndCrash { + window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = BlankSnapshotViewController(addressBarPosition: appSettings.currentAddressBarPosition, + voiceSearchHelper: voiceSearchHelper) + window.makeKeyAndVisible() + application.setWindow(window) + + 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) + 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: Self.makeTextZoomCoordinator(), + websiteDataManager: Self.makeWebsiteDataManager(fireproofing: fireproofing), + appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) + + mainViewController!.loadViewIfNeeded() + syncErrorHandler.alertPresenter = mainViewController + + window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = mainViewController + window.makeKeyAndVisible() + application.setWindow(window) + + let autoClear = AutoClear(worker: mainViewController!) + self.autoClear = autoClear + let applicationState = application.applicationState + let vpnWorkaround = 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. + // 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 + + 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 { + AppDependencyProvider.shared.appSettings.setAutofillIsNewInstallForOnByDefault() + } + + NewTabPageIntroMessageSetup().perform() + + widgetRefreshModel.beginObservingVPNStatus() + + AppDependencyProvider.shared.subscriptionManager.loadInitialData() + + let autofillUsageMonitor = AutofillUsageMonitor() + 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 ?? [:]) + 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 { + 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) { [autofillPixelReporter] _ in + autofillPixelReporter.updateAutofillEnabledStatus(AppDependencyProvider.shared.appSettings.autofillCredentialsEnabled) + } + + if stateContext.didCrashDuringCrashHandlersSetUp { + Pixel.fire(pixel: .crashOnCrashHandlersSetUp) + didCrashDuringCrashHandlersSetUp = false + } + + tipKitAppEventsHandler.appDidFinishLaunching() + } + + private var appDependencies: AppDependencies { + AppDependencies( + accountManager: accountManager, + vpnWorkaround: vpnWorkaround, + vpnFeatureVisibility: vpnFeatureVisibility, + 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, + privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager), + onboardingPixelReporter: onboardingPixelReporter, + widgetRefreshModel: widgetRefreshModel, + autofillPixelReporter: autofillPixelReporter, + crashReportUploaderOnboarding: crashReportUploaderOnboarding + ) + } + + private static func makeSubscriptionCookieManager(application: UIApplication) -> SubscriptionCookieManaging { + let subscriptionCookieManager = SubscriptionCookieManager(subscriptionManager: AppDependencyProvider.shared.subscriptionManager, + 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()) + + let privacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + + // Enable subscriptionCookieManager if feature flag is present + if privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(PrivacyProSubfeature.setAccessTokenCookieForSubscriptionDomains) { + subscriptionCookieManager.enableSettingSubscriptionCookie() + } + + return subscriptionCookieManager + } + + private static 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) +// 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): + return historyManager + } + } + + 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 { + 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 static func makeTextZoomCoordinator() -> TextZoomCoordinator { + let provider = AppDependencyProvider.shared + let storage = TextZoomStorage() + + return TextZoomCoordinator(appSettings: provider.appSettings, + storage: storage, + featureFlagger: provider.featureFlagger) + } + + private static func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + return WebCacheManager(cookieStorage: MigratableCookieStorage(), + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager) + } + +} + +extension Launched { + + struct StateContext { + + let application: UIApplication + let didFinishLaunchingStartTime: CFAbsoluteTime + let urlToOpen: URL? + let shortcutItemToHandle: UIApplicationShortcutItem? + let appDependencies: AppDependencies + + } + + func makeStateContext() -> StateContext { + .init(application: application, + didFinishLaunchingStartTime: didFinishLaunchingStartTime, + urlToOpen: urlToOpen, + shortcutItemToHandle: shortcutItemToHandle, + appDependencies: appDependencies) + } + +} + +extension UIApplication { + + func setWindow(_ window: UIWindow?) { + (delegate as? AppDelegate)?.window = window + } + + var window: UIWindow? { + delegate?.window ?? nil } } 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/AppServices/SubscriptionService.swift b/DuckDuckGo/AppServices/SubscriptionService.swift new file mode 100644 index 0000000000..fb5c9680d8 --- /dev/null +++ b/DuckDuckGo/AppServices/SubscriptionService.swift @@ -0,0 +1,42 @@ +// +// 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 +import BrowserServicesKit + +final class SubscriptionService { + + 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) + } + +} diff --git a/DuckDuckGo/AppServices/UIService.swift b/DuckDuckGo/AppServices/UIService.swift new file mode 100644 index 0000000000..25fd00983e --- /dev/null +++ b/DuckDuckGo/AppServices/UIService.swift @@ -0,0 +1,119 @@ +// +// 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 { + + var overlayWindow: UIWindow? + let window: UIWindow + + var showKeyboardIfSettingOn = true // temporary + + init(window: UIWindow) { + self.window = window + } + + func displayBlankSnapshotWindow(voiceSearchHelper: VoiceSearchHelper, + addressBarPosition: AddressBarPosition) { + guard overlayWindow == nil else { return } + + overlayWindow = UIWindow(frame: window.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 + } + } + + 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 { + + 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, // todo, will it be needed? + 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..f7874d03f7 --- /dev/null +++ b/DuckDuckGo/AppServices/UNService.swift @@ -0,0 +1,70 @@ +// +// 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 + let accountManager: AccountManager + + init(window: 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() + } + } + } + +} 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/AppShortcuts.swift b/DuckDuckGo/AppShortcuts.swift new file mode 100644 index 0000000000..aff44e84cf --- /dev/null +++ b/DuckDuckGo/AppShortcuts.swift @@ -0,0 +1,43 @@ +// +// 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 +import UIKit + +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) + ] + } + +} diff --git a/DuckDuckGo/AppUserDefaults.swift b/DuckDuckGo/AppUserDefaults.swift index df880d42d6..6c4d9ad876 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 { 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 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 { diff --git a/DuckDuckGo/NewAppDelegate.swift b/DuckDuckGo/NewAppDelegate.swift new file mode 100644 index 0000000000..6d6adc6bc8 --- /dev/null +++ b/DuckDuckGo/NewAppDelegate.swift @@ -0,0 +1,66 @@ +// +// 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, 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 + } + + 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, isTesting: isTesting)) + 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..0b142a6907 --- /dev/null +++ b/DuckDuckGo/OldAppDelegate.swift @@ -0,0 +1,1265 @@ +// +// OldAppDelegate.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, DDGApp { + + 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(crashReportSender: CrashReportSender(platform: .iOS, + pixelEvents: CrashReportSender.pixelEvents), + crashCollectionStorage: UserDefaults()) + 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) + } + } +} diff --git a/DuckDuckGo/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessagingClient.swift index 3bc72438ea..212475726a 100644 --- a/DuckDuckGo/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessagingClient.swift @@ -48,8 +48,6 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { let store: RemoteMessagingStoring let remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding - var didStartBackgroundTaskTimestamp: Date? - convenience init( bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, @@ -116,7 +114,6 @@ extension RemoteMessagingClient { let store = store BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.backgroundRefreshTaskIdentifier, using: nil) { [weak self] task in - self?.didStartBackgroundTaskTimestamp = Date() guard Self.shouldRefresh else { task.setTaskCompleted(success: true) Self.scheduleBackgroundRefreshTask() diff --git a/DuckDuckGoTests/AppSettingsMock.swift b/DuckDuckGoTests/AppSettingsMock.swift index bfd5fff474..b81eba6371 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? = .new + var defaultTextZoomLevel: DuckDuckGo.TextZoomLevel = .percent100 var recentlyVisitedSites: Bool = false