diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 8bf183dafd..272e4ea794 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -47,6 +47,7 @@ public enum FeatureFlag: String { case autcompleteTabs case textZoom case adAttributionReporting + case handoff /// https://app.asana.com/0/72649045549333/1208231259093710/f case networkProtectionUserTips @@ -127,6 +128,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.feature(.autofillSurveys)) case .autcompleteTabs: return .remoteReleasable(.feature(.autocompleteTabs)) + case .handoff: + return .internalOnly() case .networkProtectionUserTips: return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips)) case .textZoom: diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 0ec3c93bf1..da568f09af 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -721,6 +721,8 @@ import os.log } AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } + + mainViewController?.currentTab?.becomeCurrentActivity() } private func stopAndRemoveVPNIfNotAuthenticated() async { @@ -921,6 +923,17 @@ import os.log return true } + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { + guard userActivity.activityType == "com.duckduckgo.mobile.ios.web-browsing", + let mainViewController else { + return false + } + + restorationHandler([mainViewController]) + + return true + } + // MARK: private private func sendAppLaunchPostback() { diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 6d3941b0ca..3df47d72d9 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -192,6 +192,7 @@ NSUserActivityTypes ConfigurationIntent + com.duckduckgo.mobile.ios.web-browsing UIApplicationShortcutItems diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index e5b54dca00..094b4eb5c7 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -1055,6 +1055,7 @@ class MainViewController: UIViewController { hideNotificationBarIfBrokenSitePromptShown() if tab.link == nil { attachHomeScreen() + invalidateCurrentActivity() } else { attachTab(tab: tab) refreshControls() @@ -1072,7 +1073,7 @@ class MainViewController: UIViewController { hideNotificationBarIfBrokenSitePromptShown() currentTab?.progressWorker.progressBar = nil currentTab?.chromeDelegate = nil - + addToContentContainer(controller: tab) viewCoordinator.logoContainer.isHidden = true @@ -1080,6 +1081,7 @@ class MainViewController: UIViewController { tab.progressWorker.progressBar = viewCoordinator.progress chromeManager.attach(to: tab.webView.scrollView) tab.chromeDelegate = self + tab.becomeCurrentActivity() } private func addToContentContainer(controller: UIViewController) { @@ -1464,6 +1466,8 @@ class MainViewController: UIViewController { tabsBarController?.refresh(tabsModel: tabManager.model) swipeTabsCoordinator?.refresh(tabsModel: tabManager.model, scrollToSelected: true) newTabPageViewController?.openedAsNewTab(allowingKeyboard: allowingKeyboard) + + invalidateCurrentActivity() } func updateFindInPage() { @@ -2962,3 +2966,27 @@ extension MainViewController: AIChatViewControllerDelegate { loadUrlInNewTab(url, inheritedAttribution: nil) } } + +// NSUserActivity-related +extension MainViewController { + private func invalidateCurrentActivity() { + guard supportsHandoff() else { return } + + userActivity?.invalidate() + userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + userActivity?.webpageURL = nil + userActivity?.becomeCurrent() + } + + override func restoreUserActivityState(_ activity: NSUserActivity) { + guard supportsHandoff(), activity.activityType == "com.duckduckgo.mobile.ios.web-browsing", let url = activity.webpageURL else { + return + } + + loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: nil) + } + + private func supportsHandoff() -> Bool { + featureFlagger.isFeatureOn(.handoff) + } +} diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 24dddd1628..8d59725d76 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -211,6 +211,7 @@ class TabViewController: UIViewController { updateTabModel() delegate?.tabLoadingStateDidChange(tab: self) checkLoginDetectionAfterNavigation() + updateCurrentActivity(url: url) } } @@ -274,7 +275,7 @@ class TabViewController: UIViewController { manager.delegate = self return manager }() - + private static let debugEvents = EventMapping { event, _, _, onComplete in let domainEvent: Pixel.Event switch event { @@ -3182,3 +3183,42 @@ extension TabViewController: DuckPlayerTabNavigationHandling { } } + +// NSUserActivity-related +extension TabViewController { + func becomeCurrentActivity() { + guard supportsHandoff() else { return } + + if userActivity?.webpageURL == nil { + userActivity?.invalidate() + userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + userActivity?.webpageURL = nil + } + + userActivity?.becomeCurrent() + } + + private func updateCurrentActivity(url: URL?) { + guard supportsHandoff() else { return } + + let newURL: URL? = { + guard let url, let scheme = url.scheme, ["http", "https"].contains(scheme) else { return nil } + return url.isDuckDuckGo ? url.removingInternalSearchParameters() : url + }() + guard newURL != userActivity?.webpageURL else { return } + + userActivity?.invalidate() + if newURL != nil { + userActivity = NSUserActivity(activityType: "com.duckduckgo.mobile.ios.web-browsing") + } else { + userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + } + userActivity?.webpageURL = newURL + + userActivity?.becomeCurrent() + } + + private func supportsHandoff() -> Bool { + featureFlagger.isFeatureOn(.handoff) + } +}