diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index e9fcff8f830c..42126bc21f5d 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -1013,6 +1013,7 @@ DA3A4FE9254A781A00C4A9C9 /* TabTableViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3A4FE8254A781A00C4A9C9 /* TabTableViewHeader.swift */; }; DA52E1DA25F5961F0092204C /* TabTrayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52E1D925F5961F0092204C /* TabTrayViewController.swift */; }; DA9FD88424E213B500168D1E /* SmallQuickLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88224E213B400168D1E /* SmallQuickLink.swift */; }; + DA58976A26704308007F0784 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA58976926704308007F0784 /* SceneDelegate.swift */; }; DA9FD88624E213CD00168D1E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88524E213CC00168D1E /* Helpers.swift */; }; DA9FD88824E213DD00168D1E /* QuickLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9FD88724E213DC00168D1E /* QuickLink.swift */; }; DACDE996225E537900C8F37F /* VersionSettingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DACDE995225E537900C8F37F /* VersionSettingTests.swift */; }; @@ -4633,6 +4634,7 @@ DA3A4FE8254A781A00C4A9C9 /* TabTableViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTableViewHeader.swift; sourceTree = ""; }; DA4446B0870E847782CEFAF4 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/ErrorPages.strings; sourceTree = ""; }; DA52E1D925F5961F0092204C /* TabTrayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTrayViewController.swift; sourceTree = ""; }; + DA58976926704308007F0784 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; DA654F2582BF9863A719CB00 /* gu-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "gu-IN"; path = "gu-IN.lproj/Shared.strings"; sourceTree = ""; }; DA744D888927B768AD70ECF4 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/3DTouchActions.strings; sourceTree = ""; }; DA9FD88224E213B400168D1E /* SmallQuickLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmallQuickLink.swift; sourceTree = ""; }; @@ -7794,6 +7796,7 @@ EB9854FD2422686F0040F24B /* AppDelegate+PushNotifications.swift */, EB98550024226EF60040F24B /* AppDelegate+SyncSentTabs.swift */, D3BE7B451B054F8600641031 /* UITestAppDelegate.swift */, + DA58976926704308007F0784 /* SceneDelegate.swift */, ); name = Delegates; sourceTree = ""; @@ -10514,6 +10517,7 @@ 8AD40FC727BADC3400672675 /* ToolbarTextField.swift in Sources */, CA90753824929B22005B794D /* NoLoginsView.swift in Sources */, E4B423BE1AB9FE6A007E66C8 /* ReaderModeCache.swift in Sources */, + DA58976A26704308007F0784 /* SceneDelegate.swift in Sources */, 396CDB55203C5B870034A3A3 /* TabTrayController+KeyCommands.swift in Sources */, 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */, EBB8950C21939E4100EB91A0 /* FirefoxTabContentBlocker.swift in Sources */, diff --git a/Client/Application/AppDelegate.swift b/Client/Application/AppDelegate.swift index c66ea0dae688..e9a1e19f78da 100644 --- a/Client/Application/AppDelegate.swift +++ b/Client/Application/AppDelegate.swift @@ -14,8 +14,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // This is the easiest way to force a bootstrap that's guaranteed to happen on app launch private var appContainer: ServiceProvider = AppContainer.shared var window: UIWindow? - var browserViewController: BrowserViewController! - var rootViewController: UIViewController! var tabManager: TabManager! var receivedURLs = [URL]() var orientationLock = UIInterfaceOrientationMask.all @@ -35,8 +33,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { log.info("startApplication begin") - self.window = UIWindow(frame: UIScreen.main.bounds) - appLaunchUtil = AppLaunchUtil(profile: profile) appLaunchUtil?.setUpPreLaunchDependencies() @@ -44,13 +40,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { webServerUtil = WebServerUtil(profile: profile) webServerUtil?.setUpWebServer() - let imageStore = DiskImageStore(files: profile.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality) - self.tabManager = TabManager(profile: profile, imageStore: imageStore) - menuBuilderHelper = MenuBuilderHelper() - setupRootViewController() - log.info("startApplication end") return true @@ -62,15 +53,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Allow deinitializers to close our database connections. tabManager = nil - browserViewController = nil - rootViewController = nil } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window!.makeKeyAndVisible() pushNotificationSetup() appLaunchUtil?.setUpPostLaunchDependencies() backgroundSyncUtil = BackgroundSyncUtil(profile: profile, application: application) @@ -102,19 +90,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { webServerUtil?.setUpWebServer() /// When transitioning to scenes, each scene's BVC needs to resume its file download queue. - browserViewController.downloadQueue.resumeAll() + for session in application.openSessions { + (session.scene?.delegate as? SceneDelegate)?.browserViewController.downloadQueue.resumeAll() + } TelemetryWrapper.recordEvent(category: .action, method: .foreground, object: .app) - // Delay these operations until after UIKit/UIApp init is complete - // - loadQueuedTabs accesses the DB and shows up as a hot path in profiling - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // We could load these here, but then we have to futz with the tab counter - // and making NSURLRequests. - self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs) - self.receivedURLs.removeAll() - application.applicationIconBadgeNumber = 0 - } // Create fx favicon cache directory FaviconFetcher.createWebImageCacheDirectory() // update top sites widget @@ -123,7 +104,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Cleanup can be a heavy operation, take it out of the startup path. Instead check after a few seconds. DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { self.profile.cleanupHistoryIfNeeded() - self.browserViewController.ratingPromptManager.updateData() + BrowserViewController.foregroundBVC().ratingPromptManager.updateData() } } @@ -134,8 +115,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidEnterBackground(_ application: UIApplication) { // Pause file downloads. - // TODO: iOS 13 needs to iterate all the BVCs. - browserViewController.downloadQueue.pauseAll() + for session in application.openSessions { + (session.scene?.delegate as? SceneDelegate)?.browserViewController.downloadQueue.pauseAll() + } TelemetryWrapper.recordEvent(category: .action, method: .background, object: .app) TabsQuantityTelemetry.trackTabsQuantity(tabManager: tabManager) @@ -166,19 +148,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { widgetManager?.writeWidgetKitTopSites() } } - - /// When a user presses and holds the app icon from the Home Screen, we present quick actions / shortcut items (see QuickActions). - /// - /// This method can handle a quick action from both app launch and when the app becomes active. However, the system calls launch methods first if the app `launches` - /// and gives you a chance to handle the shortcut there. If it's not handled there, this method is called in the activation process with the shortcut item. - /// - /// Quick actions / shortcut items are handled here as long as our two launch methods return `true`. If either of them return `false`, this method - /// won't be called to handle shortcut items. - func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController) - - completionHandler(handledShortCutItem) - } } // This functionality will need to be moved to the SceneDelegate when the time comes @@ -189,86 +158,13 @@ extension AppDelegate { supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return self.orientationLock } +} +extension AppDelegate { func application(_ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - if userActivity.activityType == SiriShortcuts.activityType.openURL.rawValue { - browserViewController.openBlankNewTab(focusLocationField: false) - return true - } - - // If the `NSUserActivity` has a `webpageURL`, it is either a deep link or an old history item - // reached via a "Spotlight" search before we began indexing visited pages via CoreSpotlight. - if let url = userActivity.webpageURL { - let query = url.getQuery() - - // Check for fxa sign-in code and launch the login screen directly - if query["signin"] != nil { - // bvc.launchFxAFromDeeplinkURL(url) // Was using Adjust. Consider hooking up again when replacement system in-place. - return true - } - - // Per Adjust documentation, https://docs.adjust.com/en/universal-links/#running-campaigns-through-universal-links, - // it is recommended that links contain the `deep_link` query parameter. This link will also - // be url encoded. - if let deepLink = query["deep_link"]?.removingPercentEncoding, let url = URL(string: deepLink) { - browserViewController.switchToTabForURLOrOpen(url) - return true - } - - browserViewController.switchToTabForURLOrOpen(url) - return true - } - - // Otherwise, check if the `NSUserActivity` is a CoreSpotlight item and switch to its tab or - // open a new one. - if userActivity.activityType == CSSearchableItemActionType { - if let userInfo = userActivity.userInfo, - let urlString = userInfo[CSSearchableItemActivityIdentifier] as? String, - let url = URL(string: urlString) { - browserViewController.switchToTabForURLOrOpen(url) - return true - } - } - - return false - } - - func application(_ application: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - guard let routerpath = NavigationPath(url: url) else { return false } - - if let _ = profile.prefs.boolForKey(PrefsKeys.AppExtensionTelemetryOpenUrl) { - profile.prefs.removeObjectForKey(PrefsKeys.AppExtensionTelemetryOpenUrl) - var object = TelemetryWrapper.EventObject.url - if case .text = routerpath { - object = .searchText - } - TelemetryWrapper.recordEvent(category: .appExtensionAction, method: .applicationOpenUrl, object: object) - } - - DispatchQueue.main.async { - NavigationPath.handle(nav: routerpath, with: self.browserViewController) - } - return true - } - - private func setupRootViewController() { - if !LegacyThemeManager.instance.systemThemeIsOn { - window?.overrideUserInterfaceStyle = LegacyThemeManager.instance.userInterfaceStyle - } - - browserViewController = BrowserViewController(profile: profile, tabManager: tabManager) - browserViewController.edgesForExtendedLayout = [] - - let navigationController = UINavigationController(rootViewController: browserViewController) - navigationController.isNavigationBarHidden = true - navigationController.edgesForExtendedLayout = UIRectEdge(rawValue: 0) - rootViewController = navigationController - - window!.rootViewController = rootViewController + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } } diff --git a/Client/Application/SceneDelegate.swift b/Client/Application/SceneDelegate.swift new file mode 100644 index 000000000000..83d52644b0c5 --- /dev/null +++ b/Client/Application/SceneDelegate.swift @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import UIKit +import Shared +import Storage +import CoreSpotlight +import WebKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + var profile: Profile? + var browserViewController: BrowserViewController! + var tabManager: TabManager! + var isForeground: Bool = false + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if let windowScene = scene as? UIWindowScene { + windowScene.screenshotService?.delegate = self + let window = UIWindow(windowScene: windowScene) + let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity + configure(window: window, with: userActivity) + self.window = window + window.makeKeyAndVisible() + } + } + + func configure(window: UIWindow, with activity: NSUserActivity?) { + setupRootViewController(in: window) + if let url = activity?.webpageURL { + browserViewController.openURLInNewTab(url) + } + } + + private func setupRootViewController(in window: UIWindow) { + NotificationCenter.default.addObserver(forName: .DisplayThemeChanged, object: nil, queue: .main) { (notification) -> Void in + if !LegacyThemeManager.instance.systemThemeIsOn { + self.window?.overrideUserInterfaceStyle = LegacyThemeManager.instance.userInterfaceStyle + } else { + self.window?.overrideUserInterfaceStyle = .unspecified + } + } + + if !LegacyThemeManager.instance.systemThemeIsOn { + window.overrideUserInterfaceStyle = LegacyThemeManager.instance.userInterfaceStyle + } + + let appDelegate = UIApplication.shared.delegate as! AppDelegate + profile = appDelegate.profile! + // TODO: Use a per scene TabManager(?) + let imageStore = DiskImageStore(files: profile!.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality) + tabManager = TabManager(profile: appDelegate.profile!, imageStore: imageStore) + appDelegate.tabManager = tabManager + + browserViewController = BrowserViewController(profile: profile!, tabManager: tabManager!) + browserViewController.edgesForExtendedLayout = [] + + let navigationController = UINavigationController(rootViewController: browserViewController) + navigationController.delegate = appDelegate + navigationController.isNavigationBarHidden = true + navigationController.edgesForExtendedLayout = [] + + window.rootViewController = navigationController + browserViewController.updateState = .coldStart + } + + func sceneWillEnterForeground(_ scene: UIScene) { + isForeground = true + + browserViewController.firefoxHomeViewController?.reloadAll() + + // Resume file downloads. + browserViewController.downloadQueue.resumeAll() + + _ = profile?.logins.reopenIfClosed() + _ = profile?.places.reopenIfClosed() + + // Delay these operations until after UIKit/UIApp init is complete + // - loadQueuedTabs accesses the DB and shows up as a hot path in profiling + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // We could load these here, but then we have to futz with the tab counter + // and making NSURLRequests. + let receivedURLs = (UIApplication.shared.delegate as! AppDelegate).receivedURLs + self.browserViewController.loadQueuedTabs(receivedURLs: receivedURLs) + (UIApplication.shared.delegate as! AppDelegate).receivedURLs.removeAll() + UIApplication.shared.applicationIconBadgeNumber = 0 + } + } + + func sceneDidEnterBackground(_ scene: UIScene) { + tabManager.preserveTabs() + isForeground = false + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + if userActivity.activityType == SiriShortcuts.activityType.openURL.rawValue { + browserViewController.openBlankNewTab(focusLocationField: false) + return + } + + // If the `NSUserActivity` has a `webpageURL`, it is either a deep link or an old history item + // reached via a "Spotlight" search before we began indexing visited pages via CoreSpotlight. + if let url = userActivity.webpageURL { + let query = url.getQuery() + + // Check for fxa sign-in code and launch the login screen directly + if query["signin"] != nil { + // bvc.launchFxAFromDeeplinkURL(url) // Was using Adjust. Consider hooking up again when replacement system in-place. + return + } + + // Per Adjust documenation, https://docs.adjust.com/en/universal-links/#running-campaigns-through-universal-links, + // it is recommended that links contain the `deep_link` query parameter. This link will also + // be url encoded. + if let deepLink = query["deep_link"]?.removingPercentEncoding, let url = URL(string: deepLink) { + browserViewController.switchToTabForURLOrOpen(url) + return + } + + browserViewController.switchToTabForURLOrOpen(url) + return + } + + // Otherwise, check if the `NSUserActivity` is a CoreSpotlight item and switch to its tab or + // open a new one. + if userActivity.activityType == CSSearchableItemActionType { + if let userInfo = userActivity.userInfo, + let urlString = userInfo[CSSearchableItemActivityIdentifier] as? String, + let url = URL(string: urlString) { + browserViewController.switchToTabForURLOrOpen(url) + return + } + } + + return + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url, + let routerpath = NavigationPath(url: url) else { + return + } + + if let profile = (UIApplication.shared.delegate as? AppDelegate)?.profile, + let _ = profile.prefs.boolForKey(PrefsKeys.AppExtensionTelemetryOpenUrl) { + profile.prefs.removeObjectForKey(PrefsKeys.AppExtensionTelemetryOpenUrl) + var object = TelemetryWrapper.EventObject.url + if case .text(_) = routerpath { + object = .searchText + } + TelemetryWrapper.recordEvent(category: .appExtensionAction, method: .applicationOpenUrl, object: object) + } + + DispatchQueue.main.async { + NavigationPath.handle(nav: routerpath, with: self.browserViewController) + } + } + + func sceneDidBecomeActive(_ scene: UIScene) { + browserViewController.firefoxHomeViewController?.reloadAll() + browserViewController.downloadQueue.resumeAll() + browserViewController.updateViewConstraints() + } +} + +extension SceneDelegate: UIScreenshotServiceDelegate { + func screenshotService(_ screenshotService: UIScreenshotService, generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) { + guard let webView = browserViewController.tabManager.selectedTab?.currentWebView() else { + completionHandler(nil, 0, .zero) + return + } + + var rect = webView.scrollView.frame + rect.origin.x = webView.scrollView.contentOffset.x + rect.origin.y = webView.scrollView.contentSize.height - rect.height - webView.scrollView.contentOffset.y + + webView.createPDF { result in + switch result { + case .success(let data): + completionHandler(data, 0, rect) + case .failure(_): + completionHandler(nil, 0, .zero) + } + } + } +} diff --git a/Client/Frontend/Browser/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController.swift index ec7b1d25ce46..6c64a29e493f 100644 --- a/Client/Frontend/Browser/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController.swift @@ -2678,8 +2678,9 @@ extension BrowserViewController: FeatureFlaggable { extension BrowserViewController { public static func foregroundBVC() -> BrowserViewController { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, - let browserViewController = appDelegate.browserViewController else { + guard let sceneDelegate = UIWindow.keyWindow?.windowScene?.delegate as? SceneDelegate, + sceneDelegate.isForeground, + let browserViewController = sceneDelegate.browserViewController else { fatalError("Unable unwrap BrowserViewController") } diff --git a/Client/Info.plist b/Client/Info.plist index d2bfca3dadcb..825d90d18edf 100644 --- a/Client/Info.plist +++ b/Client/Info.plist @@ -123,6 +123,23 @@ FiraSans-SemiBold.ttf FiraSans-UltraLight.ttf + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + Client.SceneDelegate + + + + UIApplicationShortcutItems diff --git a/Client/RatingPromptManager.swift b/Client/RatingPromptManager.swift index d1b63935376c..a4bce91b7185 100644 --- a/Client/RatingPromptManager.swift +++ b/Client/RatingPromptManager.swift @@ -127,7 +127,12 @@ final class RatingPromptManager { lastRequestDate = date requestCount += 1 - SKStoreReviewController.requestReview() + guard #available(iOS 14.0, *), let windowScene = BrowserViewController.foregroundBVC().view.window?.windowScene else { + SKStoreReviewController.requestReview() + return + } + + SKStoreReviewController.requestReview(in: windowScene) } private func updateBookmarksCount(group: DispatchGroup) {