diff --git a/Core/Logging.swift b/Core/Logging.swift index fd6bdac037..9be20c9153 100644 --- a/Core/Logging.swift +++ b/Core/Logging.swift @@ -31,6 +31,7 @@ public extension OSLog { case autoconsentLog = "DDG Autoconsent" case configurationLog = "DDG Configuration" case syncLog = "DDG Sync" + case duckPlayerLog = "Duck Player" } @OSLogWrapper(.generalLog) static var generalLog @@ -40,6 +41,7 @@ public extension OSLog { @OSLogWrapper(.autoconsentLog) static var autoconsentLog @OSLogWrapper(.configurationLog) static var configurationLog @OSLogWrapper(.syncLog) static var syncLog + @OSLogWrapper(.duckPlayerLog) static var duckPlayerLog // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // To activate Logging Categories add categories here: @@ -50,7 +52,8 @@ public extension OSLog { .adAttributionLog, .lifecycleLog, .configurationLog, - .syncLog + .syncLog, + .duckPlayerLog ] #endif diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index dd3d93e456..94c8cdc6f6 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -150,9 +150,10 @@ public struct UserDefaultsWrapper { case duckPlayerMode = "com.duckduckgo.ios.duckPlayerMode" case duckPlayerAskModeOverlayHidden = "com.duckduckgo.ios.duckPlayerAskModeOverlayHidden" - + case vpnRedditWorkaroundInstalled = "com.duckduckgo.ios.vpn.workaroundInstalled" + // Debug keys case debugNewTabPageSectionsEnabledKey = "com.duckduckgo.ios.debug.newTabPageSectionsEnabled" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4a7b3516b..cc96638f15 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -830,7 +830,7 @@ D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64648AE2B5993890033090B /* SubscriptionEmailViewModel.swift */; }; D652498E2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D652498D2B515A6A0056B0DE /* SubscriptionSettingsViewModel.swift */; }; D65625902C22D307006EF297 /* DuckPlayerURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */; }; - D65625922C22D340006EF297 /* YouTubePlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */; }; + D65625922C22D340006EF297 /* DuckPlayerNavigationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */; }; D65625952C22D382006EF297 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F159BDA31F0BDB5A00B4A01D /* TabViewController.swift */; }; D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */; }; D65CEA702B6AC6C9008A759B /* Subscription.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D65CEA6F2B6AC6C9008A759B /* Subscription.xcassets */; }; @@ -2512,7 +2512,7 @@ D62EC3C12C248AF800FC9D04 /* DuckNavigationHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckNavigationHandling.swift; sourceTree = ""; }; D63657182A7BAE7C001AF19D /* EmailManagerRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailManagerRequestDelegate.swift; sourceTree = ""; }; D63677F42BBDB1C300605BA5 /* DaxLogoNavbarTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxLogoNavbarTitle.swift; sourceTree = ""; }; - D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubePlayerNavigationHandler.swift; sourceTree = ""; }; + D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerNavigationHandler.swift; sourceTree = ""; }; D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerURLExtension.swift; sourceTree = ""; }; D63FF8932C1B67E8006DE24D /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = ""; }; D63FF8942C1B67E8006DE24D /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = ""; }; @@ -4730,7 +4730,7 @@ D63FF8972C1B6A45006DE24D /* DuckPlayer.swift */, D6037E682C32F2E7009AAEC0 /* DuckPlayerSettings.swift */, D62EC3C12C248AF800FC9D04 /* DuckNavigationHandling.swift */, - D63FF8892C1B21C2006DE24D /* YouTubePlayerNavigationHandler.swift */, + D63FF8892C1B21C2006DE24D /* DuckPlayerNavigationHandler.swift */, D63FF88B2C1B21ED006DE24D /* DuckPlayerURLExtension.swift */, D63FF8942C1B67E8006DE24D /* YoutubeOverlayUserScript.swift */, D63FF8932C1B67E8006DE24D /* YoutubePlayerUserScript.swift */, @@ -6972,7 +6972,7 @@ 1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */, 980891A92238504B00313A70 /* UILabelExtension.swift in Sources */, 984D035A24ACCC7D0066CFB8 /* TabViewCell.swift in Sources */, - D65625922C22D340006EF297 /* YouTubePlayerNavigationHandler.swift in Sources */, + D65625922C22D340006EF297 /* DuckPlayerNavigationHandler.swift in Sources */, 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */, F194FAED1F14E2B3009B4DF8 /* UIFontExtension.swift in Sources */, 98F0FC2021FF18E700CE77AB /* AutoClearSettingsViewController.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 518224f0cc..a8a4f9f886 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", + "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift index c2e2178472..33ad44fb25 100644 --- a/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift +++ b/DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift @@ -21,13 +21,10 @@ import WebKit protocol DuckNavigationHandling { var referrer: DuckPlayerReferrer { get set } - func handleNavigation(_ navigationAction: WKNavigationAction, - webView: WKWebView, - completion: @escaping (WKNavigationActionPolicy) -> Void) + var duckPlayer: DuckPlayerProtocol { get } + func handleNavigation(_ navigationAction: WKNavigationAction, webView: WKWebView) func handleURLChange(url: URL?, webView: WKWebView) - func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, - completion: @escaping (WKNavigationActionPolicy) -> Void, - webView: WKWebView) + func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, webView: WKWebView) func handleGoBack(webView: WKWebView) func handleReload(webView: WKWebView) } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index fb54fd00b9..df7e5ecf29 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -26,7 +26,7 @@ import UserScript import Core /// Values that the Frontend can use to determine the current state. -struct InitialSetupSettings: Codable { +struct InitialPlayerSettings: Codable { struct PlayerSettings: Codable { let pip: PIP } @@ -34,16 +34,36 @@ struct InitialSetupSettings: Codable { struct PIP: Codable { let status: Status } + + struct Platform: Codable { + let name: String + } enum Status: String, Codable { case enabled case disabled } + + enum Environment: String, Codable { + case development + case production + } + + enum Locale: String, Codable { + case en + } let userValues: UserValues let settings: PlayerSettings + let platform: Platform + let locale: Locale } +struct InitialOverlaySettings: Codable { + let userValues: UserValues +} + + /// Values that the Frontend can use to determine user settings public struct UserValues: Codable { enum CodingKeys: String, CodingKey { @@ -67,7 +87,9 @@ protocol DuckPlayerProtocol { func setUserValues(params: Any, message: WKScriptMessage) -> Encodable? func getUserValues(params: Any, message: WKScriptMessage) -> Encodable? func openVideoInDuckPlayer(url: URL, webView: WKWebView) - func initialSetup(params: Any, message: WKScriptMessage) async -> Encodable? + + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? + func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? } final class DuckPlayer: DuckPlayerProtocol { @@ -103,9 +125,15 @@ final class DuckPlayer: DuckPlayerProtocol { } @MainActor - public func initialSetup(params: Any, message: WKScriptMessage) async -> Encodable? { + public func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? { let webView = message.webView - return await self.encodedSettings(with: webView) + return await self.encodedPlayerSettings(with: webView) + } + + @MainActor + public func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? { + let webView = message.webView + return await self.encodedPlayerSettings(with: webView) } private func encodeUserValues() -> UserValues { @@ -116,14 +144,21 @@ final class DuckPlayer: DuckPlayerProtocol { } @MainActor - private func encodedSettings(with webView: WKWebView?) async -> InitialSetupSettings { + private func encodedPlayerSettings(with webView: WKWebView?) async -> InitialPlayerSettings { let isPiPEnabled = webView?.configuration.allowsPictureInPictureMediaPlayback == true - let pip = InitialSetupSettings.PIP(status: isPiPEnabled ? .enabled : .disabled) - - let playerSettings = InitialSetupSettings.PlayerSettings(pip: pip) + let pip = InitialPlayerSettings.PIP(status: isPiPEnabled ? .enabled : .disabled) + let platform = InitialPlayerSettings.Platform(name: "ios") + let environment = InitialPlayerSettings.Environment.development + let locale = InitialPlayerSettings.Locale.en + let playerSettings = InitialPlayerSettings.PlayerSettings(pip: pip) let userValues = encodeUserValues() - - return InitialSetupSettings(userValues: userValues, settings: playerSettings) + return InitialPlayerSettings(userValues: userValues, settings: playerSettings, platform: platform, locale: locale) + } + + @MainActor + private func encodedOverlaySettings(with webView: WKWebView?) async -> InitialOverlaySettings { + let userValues = encodeUserValues() + return InitialOverlaySettings(userValues: userValues) } } diff --git a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift similarity index 64% rename from DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift rename to DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index d5288d97cd..bf93b4dd01 100644 --- a/DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -1,5 +1,5 @@ // -// YouTubePlayerNavigationHandler.swift +// DuckPlayerNavigationHandler.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -21,11 +21,14 @@ import Foundation import ContentScopeScripts import WebKit import Core +import Common -final class YoutubePlayerNavigationHandler { +final class DuckPlayerNavigationHandler { var duckPlayer: DuckPlayerProtocol var referrer: DuckPlayerReferrer = .other + var lastHandledVideoID: String? + var isDuckPlayerTemporarilyDisabled = false private struct Constants { static let SERPURL = "https://duckduckgo.com/" @@ -38,10 +41,13 @@ final class YoutubePlayerNavigationHandler { static let duckPlayerDefaultString = "default" static let settingsKey = "settings" static let httpMethod = "GET" + static let watchInYoutubePath = "openInYoutube" + static let watchInYoutubeVideoParameter = "v" } init(duckPlayer: DuckPlayerProtocol) { self.duckPlayer = duckPlayer + os_log("DP: Trying to load the same video while in DuckPlayer, use Youtube:", log: .duckPlayerLog, type: .debug) } static var htmlTemplatePath: String { @@ -92,14 +98,33 @@ final class YoutubePlayerNavigationHandler { } -extension YoutubePlayerNavigationHandler: DuckNavigationHandling { +extension DuckPlayerNavigationHandler: DuckNavigationHandling { // Handle rendering the simulated request if the URL is duck:// // and DuckPlayer is either enabled or alwaysAsk @MainActor - func handleNavigation(_ navigationAction: WKNavigationAction, - webView: WKWebView, - completion: @escaping (WKNavigationActionPolicy) -> Void) { + func handleNavigation(_ navigationAction: WKNavigationAction, webView: WKWebView) { + + os_log("DP: Handling DuckPlayer Player Navigation for %s", log: .duckPlayerLog, type: .debug, navigationAction.request.url?.absoluteString ?? "") + + // Handle Open in Youtube Links + // duck://player/openInYoutube?v=12345 + if let url = navigationAction.request.url, + url.scheme == "duck" { + let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if urlComponents?.path == "/\(Constants.watchInYoutubePath)", + let queryItems = urlComponents?.queryItems { + + if let videoParameterItem = queryItems.first(where: { $0.name == Constants.watchInYoutubeVideoParameter }), + let id = videoParameterItem.value { + // Disable DP temporarily + isDuckPlayerTemporarilyDisabled = true + handleURLChange(url: URL.youtube(id, timestamp: nil), webView: webView) + return + } + } + } // Daily Unique View Pixel if let url = navigationAction.request.url, @@ -110,8 +135,7 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { } // Pixel for Views From Youtube - if let url = navigationAction.request.url, - referrer == .youtube, + if referrer == .youtube, duckPlayer.settings.mode == .enabled { Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromYoutubeAutomatic, debounce: 2) } @@ -119,12 +143,12 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { // If DuckPlayer is Enabled or in ask mode, render the video if let url = navigationAction.request.url, url.isDuckURLScheme, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - let html = Self.makeHTMLFromTemplate() + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk, + !isDuckPlayerTemporarilyDisabled { let newRequest = Self.makeDuckPlayerRequest(from: URLRequest(url: url)) if #available(iOS 15.0, *) { - webView.loadSimulatedRequest(newRequest, responseHTML: html) - completion(.allow) + os_log("DP: Loading Simulated Request for %s", log: .duckPlayerLog, type: .debug, navigationAction.request.url?.absoluteString ?? "") + performRequest(request: newRequest, webView: webView) return } } @@ -133,13 +157,10 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { if let url = navigationAction.request.url, let (videoID, timestamp) = url.youtubeVideoParams, duckPlayer.settings.mode == .disabled { - webView.load(URLRequest(url: URL.youtube(videoID, timestamp: timestamp))) - completion(.allow) + os_log("DP: is Disabled. We should load original video for %s", log: .duckPlayerLog, type: .debug) + handleURLChange(url: URL.youtube(videoID, timestamp: timestamp), webView: webView) return } - - completion(.allow) - } // Handle URL changes not triggered via Omnibar @@ -147,24 +168,59 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { @MainActor func handleURLChange(url: URL?, webView: WKWebView) { + // Do not handle the URL if the video was just handled + if let url = url, + url.isYoutubeVideo || url.isDuckPlayer, + let (videoID, _) = url.youtubeVideoParams, + lastHandledVideoID == videoID, + !isDuckPlayerTemporarilyDisabled { + return + } + if let url = url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - webView.stopLoading() - let newURL = URL.duckPlayer(videoID, timestamp: timestamp) + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + + os_log("DP: Handling URL change: %s", log: .duckPlayerLog, type: .debug, url.absoluteString) + var newURL = URL.duckPlayer(videoID, timestamp: timestamp) + + // IF DP is temporarily disabled, load Youtube website + // Then reset the setting + if isDuckPlayerTemporarilyDisabled { + os_log("DP: Duckplayer is temporarily disabled. Opening Youtube", log: .duckPlayerLog, type: .debug) + newURL = URL.youtube(videoID, timestamp: timestamp) + } else { + os_log("DP: Duckplayer is NOT disabled. Opening DuckPlayer", log: .duckPlayerLog, type: .debug) + } + + // Load the URL webView.load(URLRequest(url: newURL)) + + // Add a short delay to let the webview start the navigation + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.lastHandledVideoID = videoID + self.isDuckPlayerTemporarilyDisabled = false + } } - } // DecidePolicyFor handler to redirect relevant requests // to duck://player @MainActor func handleDecidePolicyFor(_ navigationAction: WKNavigationAction, - completion: @escaping (WKNavigationActionPolicy) -> Void, webView: WKWebView) { + // Do not handle the URL if the video was just handled + if let url = navigationAction.request.url, + url.isYoutubeVideo || url.isDuckPlayer, + let (videoID, _) = url.youtubeVideoParams, + lastHandledVideoID == videoID, + !isDuckPlayerTemporarilyDisabled { + return + } + + // Pixel for Views From SERP if let url = navigationAction.request.url, navigationAction.request.allHTTPHeaderFields?[Constants.refererHeader] == Constants.SERPURL, @@ -179,21 +235,20 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { Pixel.fire(pixel: Pixel.Event.duckPlayerViewFromOther, debounce: 2) } - if let url = navigationAction.request.url, - url.isYoutubeVideo, - !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { - webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp))) - completion(.allow) + url.isYoutubeVideo, + !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams, + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + os_log("DP: Handling decidePolicy for Duck Player with %s", log: .duckPlayerLog, type: .debug, url.absoluteString) + handleURLChange(url: URL.duckPlayer(videoID, timestamp: timestamp), webView: webView) return } - completion(.allow) } // Handle Webview BackButton on DuckPlayer videos @MainActor func handleGoBack(webView: WKWebView) { + guard let backURL = webView.backForwardList.backItem?.url, backURL.isYoutubeVideo, backURL.youtubeVideoParams?.videoID == webView.url?.youtubeVideoParams?.videoID, @@ -204,14 +259,15 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling { webView.goBack(skippingHistoryItems: 2) } - // Handle Reload for DuckPlayer Videos @MainActor func handleReload(webView: WKWebView) { + if let url = webView.url, url.isDuckPlayer, !url.isDuckURLScheme, let (videoID, timestamp) = url.youtubeVideoParams, - duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + duckPlayer.settings.mode == .enabled || duckPlayer.settings.mode == .alwaysAsk { + os_log("DP: Handling DuckPlayer Reload for %s", log: .duckPlayerLog, type: .debug, url.absoluteString) webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp))) } else { webView.reload() diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift index d17a9b3780..5ae878341a 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerURLExtension.swift @@ -42,8 +42,14 @@ extension URL { } static func youtube(_ videoID: String, timestamp: String? = nil) -> URL { - let url = "https://www.youtube.com/watch?v=\(videoID)".url! - return url.addingTimestamp(timestamp) + #if os(iOS) + let baseUrl = "https://m.youtube.com/watch?v=\(videoID)" + #else + let baseUrl = "https://www.youtube.com/watch?v=\(videoID)" + #endif + + let url = URL(string: baseUrl)! + return url.addingTimestamp(timestamp) } var isDuckURLScheme: Bool { diff --git a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift index 091afb731b..261e1d912b 100644 --- a/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift @@ -107,7 +107,7 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature { case Handlers.sendDuckPlayerPixel: return handleSendJSPixel case Handlers.initialSetup: - return duckPlayer.initialSetup + return duckPlayer.initialSetupOverlay default: assertionFailure("YoutubeOverlayUserScript: Failed to parse User Script message: \(methodName)") // TODO: Send pixel here diff --git a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift index 2536de5f83..af283c3615 100644 --- a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift @@ -72,7 +72,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { case Handlers.setUserValues: return duckPlayer.setUserValues case Handlers.initialSetup: - return duckPlayer.initialSetup + return duckPlayer.initialSetupPlayer default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index d0a73362c6..12ed099455 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -35,6 +35,7 @@ class TabManager { private let historyManager: HistoryManaging private let syncService: DDGSyncing private var previewsSource: TabPreviewsSource + private var duckPlayerNavigationHandler: DuckNavigationHandling weak var delegate: TabDelegate? @@ -52,6 +53,9 @@ class TabManager { self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager self.syncService = syncService + + // Init Duck Player Handler + self.duckPlayerNavigationHandler = DuckPlayerNavigationHandler(duckPlayer: DuckPlayer()) registerForNotifications() } @@ -68,7 +72,8 @@ class TabManager { let controller = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -140,7 +145,8 @@ class TabManager { let controller = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index 43d494d4e4..5f3aa279e8 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -294,7 +294,8 @@ class TabViewController: UIViewController { appSettings: AppSettings = AppDependencyProvider.shared.appSettings, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, - syncService: DDGSyncing) -> TabViewController { + syncService: DDGSyncing, + duckPlayerNavigationHandler: DuckNavigationHandling) -> TabViewController { let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in TabViewController(coder: coder, @@ -302,7 +303,8 @@ class TabViewController: UIViewController { appSettings: appSettings, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) }) return controller } @@ -313,22 +315,22 @@ class TabViewController: UIViewController { let historyManager: HistoryManaging let historyCapture: HistoryCapture - - var duckPlayer: DuckPlayerProtocol = DuckPlayer() - var youtubeNavigationHandler: DuckNavigationHandling? + var duckPlayerNavigationHandler: DuckNavigationHandling required init?(coder aDecoder: NSCoder, tabModel: Tab, appSettings: AppSettings, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, - syncService: DDGSyncing) { + syncService: DDGSyncing, + duckPlayerNavigationHandler: DuckNavigationHandling) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager self.historyCapture = HistoryCapture(historyManager: historyManager) self.syncService = syncService + self.duckPlayerNavigationHandler = duckPlayerNavigationHandler super.init(coder: aDecoder) } @@ -346,9 +348,6 @@ class TabViewController: UIViewController { subscribeToEmailProtectionSignOutNotification() registerForDownloadsNotifications() registerForAddressBarLocationNotifications() - - // Setup DuckPlayer navigation handler - self.youtubeNavigationHandler = YoutubePlayerNavigationHandler(duckPlayer: duckPlayer) if #available(iOS 16.4, *) { registerForInspectableWebViewNotifications() @@ -681,18 +680,15 @@ class TabViewController: UIViewController { } else if let currentHost = url?.host, let newHost = webView.url?.host, currentHost == newHost { url = webView.url - if let handler = youtubeNavigationHandler, - let url, + if let url, url.isYoutubeVideo, - duckPlayer.settings.mode == .enabled { - handler.handleURLChange(url: url, webView: webView) + duckPlayerNavigationHandler.duckPlayer.settings.mode == .enabled { + duckPlayerNavigationHandler.handleURLChange(url: url, webView: webView) } } - if var handler = youtubeNavigationHandler, - let url { - handler.referrer = url.isYoutube ? .youtube : .other - + if let url { + duckPlayerNavigationHandler.referrer = url.isYoutube ? .youtube : .other } } @@ -744,8 +740,8 @@ class TabViewController: UIViewController { public func reload() { updateContentMode() cachedRuntimeConfigurationForDomain = [:] - if let url = webView.url, url.isDuckPlayer, let handler = youtubeNavigationHandler { - handler.handleReload(webView: webView) + if let url = webView.url, url.isDuckPlayer { + duckPlayerNavigationHandler.handleReload(webView: webView) } else { webView.reload() } @@ -759,8 +755,8 @@ class TabViewController: UIViewController { func goBack() { dismissJSAlertIfNeeded() - if let url = url, url.isDuckPlayer, let handler = youtubeNavigationHandler { - handler.handleGoBack(webView: webView) + if let url = url, url.isDuckPlayer { + duckPlayerNavigationHandler.handleGoBack(webView: webView) chromeDelegate?.omniBar.resignFirstResponder() return } @@ -1676,10 +1672,10 @@ extension TabViewController: WKNavigationDelegate { } if navigationAction.isTargetingMainFrame(), - let handler = youtubeNavigationHandler, url.isYoutubeVideo, - duckPlayer.settings.mode == .enabled { - handler.handleDecidePolicyFor(navigationAction, completion: completion, webView: webView) + duckPlayerNavigationHandler.duckPlayer.settings.mode == .enabled { + duckPlayerNavigationHandler.handleDecidePolicyFor(navigationAction, webView: webView) + completion(.allow) return } @@ -1700,11 +1696,9 @@ extension TabViewController: WKNavigationDelegate { performBlobNavigation(navigationAction, completion: completion) case .duck: - if let handler = youtubeNavigationHandler { - handler.handleNavigation(navigationAction, webView: webView, completion: completion) - return - } + duckPlayerNavigationHandler.handleNavigation(navigationAction, webView: webView) completion(.cancel) + return case .unknown: if navigationAction.navigationType == .linkActivated { @@ -2353,7 +2347,7 @@ extension TabViewController: UserContentControllerDelegate { userScripts.autoconsentUserScript.delegate = self // Setup DuckPlayer - userScripts.duckPlayer = duckPlayer + userScripts.duckPlayer = duckPlayerNavigationHandler.duckPlayer userScripts.youtubeOverlayScript?.webView = webView userScripts.youtubePlayerUserScript?.webView = webView diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index e6a48170cd..b25fb38e35 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -105,7 +105,8 @@ extension TabViewController { let tabController = TabViewController.loadFromStoryboard(model: tab, bookmarksDatabase: bookmarksDatabase, historyManager: historyManager, - syncService: syncService) + syncService: syncService, + duckPlayerNavigationHandler: duckPlayerNavigationHandler) tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() tabController.attachWebView(configuration: configuration, andLoadRequest: URLRequest.userInitiated(url), consumeCookies: false) diff --git a/DuckDuckGoTests/DuckPlayerMocks.swift b/DuckDuckGoTests/DuckPlayerMocks.swift index aea76a748a..ed84be6f1d 100644 --- a/DuckDuckGoTests/DuckPlayerMocks.swift +++ b/DuckDuckGoTests/DuckPlayerMocks.swift @@ -110,6 +110,14 @@ final class MockDuckPlayerSettings: DuckPlayerSettingsProtocol { } final class MockDuckPlayer: DuckPlayerProtocol { + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + + func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + var settings: any DuckPlayerSettingsProtocol init(settings: DuckPlayerSettingsProtocol) { diff --git a/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift b/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift index d5df8b9603..d45e6ca8f7 100644 --- a/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift +++ b/DuckDuckGoTests/DuckPlayerURLExtensionTests.swift @@ -22,6 +22,12 @@ import os.log @testable import DuckDuckGo final class DuckPlayerURLExtensionTests: XCTestCase { + + #if os(iOS) + let baseUrl = "https://m.youtube.com" + #else + let baseUrl = "https://www.youtube.com" + #endif func testIsDuckPlayerScheme() { XCTAssertTrue("duck:player/abcdef12345".url!.isDuckURLScheme) @@ -29,7 +35,7 @@ final class DuckPlayerURLExtensionTests: XCTestCase { XCTAssertTrue("duck://player/abcdef".url!.isDuckURLScheme) XCTAssertTrue("duck://player/12345".url!.isDuckURLScheme) XCTAssertFalse("http://duckplayer/abcdef12345".url!.isDuckURLScheme) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345".url!.isDuckURLScheme) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345".url!.isDuckURLScheme) XCTAssertFalse("https://www.youtube-nocookie.com/embed/abcdef12345".url!.isDuckURLScheme) } @@ -41,25 +47,25 @@ final class DuckPlayerURLExtensionTests: XCTestCase { XCTAssertFalse("https://www.youtube-nocookie.com/embed?t=23s".url!.isDuckPlayer) XCTAssertTrue("duck://player/abcdef12345".url!.isDuckPlayer) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345".url!.isDuckPlayer) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345".url!.isDuckPlayer) XCTAssertFalse("https://duckduckgo.com".url!.isDuckPlayer) } func testIsYoutubePlaylist() { - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertTrue("https://www.youtube.com/watch?list=abcdefgh12345678&v=abcdef12345".url!.isYoutubePlaylist) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) + XCTAssertTrue("\(baseUrl)/watch?list=abcdefgh12345678&v=abcdef12345".url!.isYoutubePlaylist) XCTAssertFalse("https://duckduckgo.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertFalse("https://www.youtube.com/watch?list=abcdefgh12345678".url!.isYoutubePlaylist) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubePlaylist) + XCTAssertFalse("\(baseUrl)/watch?list=abcdefgh12345678".url!.isYoutubePlaylist) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubePlaylist) } func testIsYoutubeVideo() { - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345".url!.isYoutubeVideo) - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubeVideo) - XCTAssertTrue("https://www.youtube.com/watch?v=abcdef12345&t=5m".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678&index=1".url!.isYoutubeVideo) + XCTAssertTrue("\(baseUrl)/watch?v=abcdef12345&t=5m".url!.isYoutubeVideo) - XCTAssertFalse("https://www.youtube.com/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubeVideo) + XCTAssertFalse("\(baseUrl)/watch?v=abcdef12345&list=abcdefgh12345678".url!.isYoutubeVideo) XCTAssertFalse("https://duckduckgo.com/watch?v=abcdef12345".url!.isYoutubeVideo) } @@ -74,15 +80,15 @@ final class DuckPlayerURLExtensionTests: XCTestCase { } func testYoutubeVideoParamsFromYoutubeURL() { - let params = "https://www.youtube.com/watch?v=abcdef12345".url!.youtubeVideoParams + let params = "\(baseUrl)/watch?v=abcdef12345".url!.youtubeVideoParams XCTAssertEqual(params?.videoID, "abcdef12345") XCTAssertEqual(params?.timestamp, nil) - let paramsWithTimestamp = "https://www.youtube.com/watch?v=abcdef12345&t=23s".url!.youtubeVideoParams + let paramsWithTimestamp = "\(baseUrl)/watch?v=abcdef12345&t=23s".url!.youtubeVideoParams XCTAssertEqual(paramsWithTimestamp?.videoID, "abcdef12345") XCTAssertEqual(paramsWithTimestamp?.timestamp, "23s") - let paramsWithTimestampWithoutUnits = "https://www.youtube.com/watch?t=102&v=abcdef12345&feature=youtu.be".url!.youtubeVideoParams + let paramsWithTimestampWithoutUnits = "\(baseUrl)/watch?t=102&v=abcdef12345&feature=youtu.be".url!.youtubeVideoParams XCTAssertEqual(paramsWithTimestampWithoutUnits?.videoID, "abcdef12345") XCTAssertEqual(paramsWithTimestampWithoutUnits?.timestamp, "102") } @@ -110,15 +116,15 @@ final class DuckPlayerURLExtensionTests: XCTestCase { } func testYoutubeURLTimestampValidation() { - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: nil).absoluteString, "https://www.youtube.com/watch?v=abcdef12345") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "23s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=23s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5m5s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h400m100s").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=12h400m100s") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h2s2h").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=12h2s2h") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5m5m").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5m5m5m") - - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5").absoluteString, "https://www.youtube.com/watch?v=abcdef12345&t=5") - XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "10d").absoluteString, "https://www.youtube.com/watch?v=abcdef12345") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: nil).absoluteString, "\(baseUrl)/watch?v=abcdef12345") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "23s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=23s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5m5s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h400m100s").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=12h400m100s") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "12h2s2h").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=12h2s2h") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5m5m5m").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5m5m5m") + + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "5").absoluteString, "\(baseUrl)/watch?v=abcdef12345&t=5") + XCTAssertEqual(URL.youtube("abcdef12345", timestamp: "10d").absoluteString, "\(baseUrl)/watch?v=abcdef12345") } func testYoutubeNoCookieURLTimestampValidation() { diff --git a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift index fe4f76eb09..877980dc8e 100644 --- a/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift +++ b/DuckDuckGoTests/YoutublePlayerNavigationHandlerTests.swift @@ -25,14 +25,14 @@ import BrowserServicesKit @testable import DuckDuckGo -class YoutubePlayerNavigationHandlerTests: XCTestCase { +class DuckPlayerNavigationHandlerTests: XCTestCase { var webView: WKWebView! var mockWebView: MockWebView! var mockNavigationDelegate: MockWKNavigationDelegate! var mockAppSettings: AppSettingsMock! var mockPrivacyConfig: PrivacyConfigurationManagerMock! - + override func setUp() { super.setUp() webView = WKWebView() @@ -52,7 +52,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { // Test for htmlTemplatePath existence func testHtmlTemplatePathExists() { - let templatePath = YoutubePlayerNavigationHandler.htmlTemplatePath + let templatePath = DuckPlayerNavigationHandler.htmlTemplatePath let fileExists = FileManager.default.fileExists(atPath: templatePath) XCTAssertFalse(templatePath.isEmpty, "The template path should not be empty") XCTAssertTrue(fileExists, "The template file should exist at the specified path") @@ -62,7 +62,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { func testMakeDuckPlayerRequestFromOriginalRequest() { let originalRequest = URLRequest(url: URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")!) - let duckPlayerRequest = YoutubePlayerNavigationHandler.makeDuckPlayerRequest(from: originalRequest) + let duckPlayerRequest = DuckPlayerNavigationHandler.makeDuckPlayerRequest(from: originalRequest) XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") @@ -76,7 +76,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let videoID = "abc123" let timestamp = "10s" - let duckPlayerRequest = YoutubePlayerNavigationHandler.makeDuckPlayerRequest(for: videoID, timestamp: timestamp) + let duckPlayerRequest = DuckPlayerNavigationHandler.makeDuckPlayerRequest(for: videoID, timestamp: timestamp) XCTAssertEqual(duckPlayerRequest.url?.host, "www.youtube-nocookie.com") XCTAssertEqual(duckPlayerRequest.url?.path, "/embed/abc123") @@ -87,24 +87,23 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { // Test for makeHTMLFromTemplate func testMakeHTMLFromTemplate() { - let expectedHtml = try? String(contentsOfFile: YoutubePlayerNavigationHandler.htmlTemplatePath) - let html = YoutubePlayerNavigationHandler.makeHTMLFromTemplate() + let expectedHtml = try? String(contentsOfFile: DuckPlayerNavigationHandler.htmlTemplatePath) + let html = DuckPlayerNavigationHandler.makeHTMLFromTemplate() XCTAssertEqual(html, expectedHtml) } - // Test for handleURLChange + // MARK: handleURLChange tests @MainActor func testHandleURLChangeDuckPlayerEnabled() { let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: youtubeURL, webView: mockWebView) - XCTAssertTrue(mockWebView.didStopLoadingCalled, "Expected stopLoading to be called") XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") if let loadedRequest = mockWebView.lastLoadedRequest { @@ -113,6 +112,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { XCTAssertEqual(loadedRequest.url?.path, "/abc123") XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) } + } @MainActor @@ -122,157 +122,147 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: youtubeURL, webView: mockWebView) - XCTAssertFalse(mockWebView.didStopLoadingCalled, "Expected stopLoading Not to be called") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request NOT to be loaded") } @MainActor - func testHandleURLChangeForNonYouTubeVideo() { + func testHandleURLChangeDuckPlayerTemporarilyDisabled() { + let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! + + let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) + playerSettings.setMode(.enabled) + let player = MockDuckPlayer(settings: playerSettings) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + handler.isDuckPlayerTemporarilyDisabled = true + + handler.handleURLChange(url: youtubeURL, webView: mockWebView) + + XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + + if let loadedRequest = mockWebView.lastLoadedRequest { + XCTAssertEqual(loadedRequest.url?.scheme, "https") + XCTAssertEqual(loadedRequest.url?.host, "m.youtube.com") + XCTAssertEqual(loadedRequest.url?.path, "/watch") + XCTAssertEqual(loadedRequest.url?.query?.contains("v=abc123"), true) + XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) + } + } + + @MainActor + func testHandleURLChangeNonYouTubeURL() { let nonYouTubeURL = URL(string: "https://www.google.com")! let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleURLChange(url: nonYouTubeURL, webView: mockWebView) - XCTAssertFalse(mockWebView.didStopLoadingCalled, "Expected stopLoading not to be called") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request NOT to be loaded") } - // Test for handleDecidePolicyFor @MainActor - func testHandleDecidePolicyForWithDuckPlayerEnabled() { - let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! - let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationOpenInYoutubeLink() { + let duckURL = URL(string: "duck://player/openInYoutube?v=12345")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) - waitForExpectations(timeout: 1, handler: nil) + handler.handleNavigation(navigationAction, webView: mockWebView) - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") + XCTAssertTrue(handler.isDuckPlayerTemporarilyDisabled, "Expected DuckPlayer to be temporarily disabled") XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") if let loadedRequest = mockWebView.lastLoadedRequest { - XCTAssertEqual(loadedRequest.url?.scheme, "duck") - XCTAssertEqual(loadedRequest.url?.host, "player") - XCTAssertEqual(loadedRequest.url?.path, "/abc123") - XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) + XCTAssertEqual(loadedRequest.url?.scheme, "https") + XCTAssertEqual(loadedRequest.url?.host, "m.youtube.com") + XCTAssertEqual(loadedRequest.url?.path, "/watch") + XCTAssertEqual(loadedRequest.url?.query?.contains("v=12345"), true) } - } @MainActor - func testHandleDecidePolicyForWithDuckPlayerDisabled() { - let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! - let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationDuckPlayerEnabledAlreadyInDuckPlayer() { + let duckPlayerURL = URL(string: "duck://player/CYTASDSD")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckPlayerURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + player.settings.setMode(.enabled) - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + handler.handleNavigation(navigationAction, webView: mockWebView) - waitForExpectations(timeout: 1, handler: nil) - - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") - + } + @MainActor - func testHandleDecidePolicyForNonYouTubeVideoWithDuckPlayerEnabled() { - let nonYouTubeURL = URL(string: "https://www.google.com")! - let navigationAction = MockNavigationAction(request: URLRequest(url: nonYouTubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? + func testHandleNavigationDuckPlayerDisabled() { + let duckPlayerURL = URL(string: "duck://player/CUIUIIUI")! + let navigationAction = MockNavigationAction(request: URLRequest(url: duckPlayerURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy - expectation.fulfill() - }, webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + handler.handleNavigation(navigationAction, webView: mockWebView) - waitForExpectations(timeout: 1, handler: nil) + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } @MainActor - func testHandleDecidePolicyForNonYouTubeVideoWithDuckPlayerDisabled() { - let nonYouTubeURL = URL(string: "https://www.google.com")! - let navigationAction = MockNavigationAction(request: URLRequest(url: nonYouTubeURL)) - let expectation = self.expectation(description: "Completion handler called") - - var navigationPolicy: WKNavigationActionPolicy? - + func testHandleDecidePolicyForVideoJustHandled() { + let youtubeURL = URL(string: "https://www.youtube.com/watch?v=abc123&t=10s")! + let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.disabled) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleDecidePolicyFor(navigationAction, completion: { policy in - navigationPolicy = policy + let handler = DuckPlayerNavigationHandler(duckPlayer: player) + + // Call handleDecidePolicyFor twice with the same URL to simulate handling the same video twice + handler.handleDecidePolicyFor(navigationAction, webView: mockWebView) + + // Wait for 0.8 seconds to simulate the time delay + let expectation = self.expectation(description: "Wait for 0.8 seconds") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + handler.handleDecidePolicyFor(navigationAction, webView: self.mockWebView) expectation.fulfill() - }, webView: mockWebView) + } - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 1.0, handler: nil) - XCTAssertEqual(navigationPolicy, .allow, "Expected navigation policy to be .allow") - XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") + // Verify that the second call did not load a new request + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected no new request to be loaded since video was just handled") } @MainActor - func testHandleReloadForDuckPlayerVideoWithDuckPlayerEnabled() { - let duckPlayerURL = URL(string: "https://www.youtube-nocookie.com/embed/abc123?t=10s")! - - mockWebView.setCurrentURL(duckPlayerURL) + func testHandleDecidePolicyForTransformYoutubeURL() { + let youtubeURL = URL(string: "https://m.youtube.com/watch?v=abc123&t=10s")! + let navigationAction = MockNavigationAction(request: URLRequest(url: youtubeURL)) let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) - playerSettings.setMode(.alwaysAsk) + playerSettings.setMode(.enabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) - - handler.handleReload(webView: mockWebView) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) - XCTAssertNotNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + handler.handleDecidePolicyFor(navigationAction, webView: mockWebView) - if let loadedRequest = mockWebView.lastLoadedRequest { - XCTAssertEqual(loadedRequest.url?.scheme, "duck") - XCTAssertEqual(loadedRequest.url?.host, "player") - XCTAssertEqual(loadedRequest.url?.path, "/abc123") - XCTAssertEqual(loadedRequest.url?.query?.contains("t=10s"), true) - } + XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request to be loaded") + } @MainActor @@ -284,14 +274,14 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } - + @MainActor func testHandleReloadForNonDuckPlayerVideoWithDuckPlayerEnabled() { let nonDuckPlayerURL = URL(string: "https://www.google.com")! @@ -302,7 +292,7 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.alwaysAsk) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") @@ -318,10 +308,10 @@ class YoutubePlayerNavigationHandlerTests: XCTestCase { let playerSettings = MockDuckPlayerSettings(appSettings: mockAppSettings, privacyConfigManager: mockPrivacyConfig) playerSettings.setMode(.disabled) let player = MockDuckPlayer(settings: playerSettings) - let handler = YoutubePlayerNavigationHandler(duckPlayer: player) + let handler = DuckPlayerNavigationHandler(duckPlayer: player) handler.handleReload(webView: mockWebView) XCTAssertNil(mockWebView.lastLoadedRequest, "Expected a new request not to be loaded") } - + } diff --git a/submodules/privacy-reference-tests b/submodules/privacy-reference-tests index afb4f6128a..a603ff9af2 160000 --- a/submodules/privacy-reference-tests +++ b/submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1 +Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8