diff --git a/App/Client.xcodeproj/project.pbxproj b/App/Client.xcodeproj/project.pbxproj index 38fb6f7f048..94bde10100b 100644 --- a/App/Client.xcodeproj/project.pbxproj +++ b/App/Client.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ CA986FB4298C1254000C6DD8 /* BraveWidgetsModels in Frameworks */ = {isa = PBXBuildFile; productRef = CA986FB3298C1254000C6DD8 /* BraveWidgetsModels */; }; CAADEFE026E2707F0020DC4C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAADEFDF26E2707F0020DC4C /* SceneDelegate.swift */; }; CAAE653D287C9FCF00FA44A3 /* CPTemplateApplicationSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAAE653C287C9FCF00FA44A3 /* CPTemplateApplicationSceneDelegate.swift */; }; + CABDE77F2A55DD1C00A388A4 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABDE77E2A55DD1C00A388A4 /* AppState.swift */; }; E6231C011B90A44F005ABB0D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; E6231C051B90A472005ABB0D /* libxml2.2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C041B90A472005ABB0D /* libxml2.2.tbd */; }; F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84B21E51A0910F600AAB793 /* AppDelegate.swift */; }; @@ -315,6 +316,7 @@ CA0391F7271E143F000EB13C /* SingleStatWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleStatWidget.swift; sourceTree = ""; }; CAADEFDF26E2707F0020DC4C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; CAAE653C287C9FCF00FA44A3 /* CPTemplateApplicationSceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPTemplateApplicationSceneDelegate.swift; sourceTree = ""; }; + CABDE77E2A55DD1C00A388A4 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; E6231C001B90A44F005ABB0D /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; E6231C041B90A472005ABB0D /* libxml2.2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.2.tbd; path = usr/lib/libxml2.2.tbd; sourceTree = SDKROOT; }; E62AC15F1E956AFC00843532 /* Dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Dev.entitlements; sourceTree = ""; }; @@ -606,6 +608,7 @@ children = ( CAAE653C287C9FCF00FA44A3 /* CPTemplateApplicationSceneDelegate.swift */, F84B21E51A0910F600AAB793 /* AppDelegate.swift */, + CABDE77E2A55DD1C00A388A4 /* AppState.swift */, CAADEFDF26E2707F0020DC4C /* SceneDelegate.swift */, ); path = Delegates; @@ -1040,6 +1043,7 @@ 2F4A572429E608C8003454F8 /* BrowserIntents.intentdefinition in Sources */, F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */, 27BEDCCC28AD37CE0073425E /* BraveWidgets.intentdefinition in Sources */, + CABDE77F2A55DD1C00A388A4 /* AppState.swift in Sources */, CAADEFE026E2707F0020DC4C /* SceneDelegate.swift in Sources */, CAAE653D287C9FCF00FA44A3 /* CPTemplateApplicationSceneDelegate.swift in Sources */, ); diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index a1aebc47f2c..0e71961dcb6 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -32,56 +32,23 @@ import Preferences import BraveShields import PrivateCDN -extension AppDelegate { - // A model that is passed used in every scene - struct SceneInfoModel { - let profile: Profile - let diskImageStore: DiskImageStore? - let migration: Migration? - let rewards: Brave.BraveRewards - } -} - @main class AppDelegate: UIResponder, UIApplicationDelegate { private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-delegate") var window: UIWindow? - lazy var braveCore: BraveCoreMain = { - var switches: [BraveCoreSwitch] = [] - if !AppConstants.buildChannel.isPublic { - // Check prefs for additional switches - let activeSwitches = Set(Preferences.BraveCore.activeSwitches.value) - let switchValues = Preferences.BraveCore.switchValues.value - for activeSwitch in activeSwitches { - let key = BraveCoreSwitchKey(rawValue: activeSwitch) - if key.isValueless { - switches.append(.init(key: key)) - } else if let value = switchValues[activeSwitch], !value.isEmpty { - switches.append(.init(key: key, value: value)) - } - } - } - switches.append(.init(key: .rewardsFlags, value: BraveRewards.Configuration.current().flags)) - return BraveCoreMain(userAgent: UserAgent.mobile, additionalSwitches: switches) - }() - - var migration: Migration? - private weak var application: UIApplication? - var launchOptions: [AnyHashable: Any]? - let appVersion = Bundle.main.infoDictionaryString(forKey: "CFBundleShortVersionString") - var receivedURLs: [URL]? - - /// Object used to handle server pings - private(set) lazy var dau = DAU(braveCoreStats: braveCore.braveStats) - - private var cancellables: Set = [] - private var sceneInfo: SceneInfoModel? - override init() { + private var cancellables: Set = [] + + @discardableResult + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Hold references to willFinishLaunching parameters for delayed app launch + self.application = application + + // Application Constants must be initialized first #if MOZ_CHANNEL_RELEASE AppConstants.buildChannel = .release #elseif MOZ_CHANNEL_BETA @@ -93,62 +60,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #elseif MOZ_CHANNEL_DEBUG AppConstants.buildChannel = .debug #endif - super.init() - } - - @discardableResult - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Hold references to willFinishLaunching parameters for delayed app launch - self.application = application - self.launchOptions = launchOptions - - // Brave Core Initialization - BraveCoreMain.setLogHandler { severity, file, line, messageStartIndex, message in - let message = String(message.dropFirst(messageStartIndex).dropLast()) - .trimmingCharacters(in: .whitespacesAndNewlines) - if message.isEmpty { - // Nothing to print - return true - } - - if severity == .fatal { - let filename = URL(fileURLWithPath: file).lastPathComponent -#if DEBUG - // Prints a special runtime warning instead of crashing. - os_log( - .fault, - dso: os_rw.dso, - log: os_rw.log(category: "BraveCore"), - "[%@:%ld] > %@", filename, line, message - ) - return true -#else - fatalError("Fatal BraveCore Error at \(filename):\(line).\n\(message)") -#endif - } - - let level: OSLogType = { - switch severity { - case .fatal: return .fault - case .error: return .error - // No `.warning` level exists for OSLogType. os_Log.warning is an alias for `.error`. - case .warning: return .error - case .info: return .info - default: return .debug - } - }() - - let braveCoreLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "brave-core") - if AppConstants.buildChannel.isPublic { - braveCoreLogger.log(level: level, "\(message, privacy: .private)") - } else { - braveCoreLogger.log(level: level, "\(message, privacy: .public)") - } - - return true - } - - migration = Migration(braveCore: braveCore) + + AppState.shared.state = .launching(options: launchOptions ?? [:], active: false) // Passcode checking, must happen on immediate launch if !DataController.shared.storeExists() { @@ -157,14 +70,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // upon reinstall. KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(nil) } - - return startApplication(application, withLaunchOptions: launchOptions) - } - - @discardableResult - fileprivate func startApplication(_ application: UIApplication, withLaunchOptions launchOptions: [AnyHashable: Any]?) -> Bool { - log.info("startApplication begin") - + // Set the Safari UA for browsing. setUserAgent() @@ -185,41 +91,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SDImageCodersManager.shared.addCoder(PrivateCDNImageCoder()) SDImageCodersManager.shared.addCoder(SDImageSVGNativeCoder.shared) - // Setup Profile - let profile = BrowserProfile(localName: "profile") - - // Setup DiskImageStore for Screenshots - let diskImageStore = { () -> DiskImageStore? in - do { - return try DiskImageStore( - files: profile.files, - namespace: "TabManagerScreenshots", - quality: UIConstants.screenshotQuality) - } catch { - log.error("Failed to create an image store for files: \(profile.files.rootPath) and namespace: \"TabManagerScreenshots\": \(error.localizedDescription)") - assertionFailure() - } - return nil - }() - - // Setup ads - let rewardsConfiguration = BraveRewards.Configuration.current() - Migration.migrateAdsConfirmations(for: rewardsConfiguration) - - // Setup Scene Info - sceneInfo = SceneInfoModel( - profile: profile, - diskImageStore: diskImageStore, - migration: migration, - rewards: BraveRewards(configuration: rewardsConfiguration)) - - // Perform migrations - let profilePrefix = profile.prefs.getBranchPrefix() - migration?.launchMigrations(keyPrefix: profilePrefix, profile: profile) - - // Setup Custom WKWebView Scheme Handlers - setupCustomSchemeHandlers(profile) - // Temporary fix for Bug 1390871 - NSInvalidArgumentException: -[WKContentView menuHelperFindInPage]: unrecognized selector if let clazz = NSClassFromString("WKCont" + "ent" + "View"), let swizzledMethod = class_getInstanceMethod(TabWebViewMenuHelper.self, #selector(TabWebViewMenuHelper.swizzledMenuHelperFindInPage)) { class_addMethod(clazz, MenuHelper.selectorFindInPage, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) @@ -240,15 +111,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } SystemUtils.onFirstRun() - - // Schedule Brave Core Priority Tasks - braveCore.scheduleLowPriorityStartupTasks() - - log.info("startApplication end") return true } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + AppState.shared.state = .launching(options: launchOptions ?? [:], active: true) + // IAPs can trigger on the app as soon as it launches, // for example when a previous transaction was not finished and is in pending state. SKPaymentQueue.default().add(BraveVPN.iapObserver) @@ -316,12 +184,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // There was a bug that when you skipped onboarding, default search engine preference // was not set. if Preferences.Search.defaultEngineName.value == nil { - sceneInfo?.profile.searchEngines.searchEngineSetup() + AppState.shared.profile.searchEngines.searchEngineSetup() } // Migration of Yahoo Search Engines if !Preferences.Search.yahooEngineMigrationCompleted.value { - sceneInfo?.profile.searchEngines.migrateDefaultYahooSearchEngines() + AppState.shared.profile.searchEngines.migrateDefaultYahooSearchEngines() } if isFirstLaunch { @@ -359,7 +227,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let weekOfInstall = Preferences.DAU.weekOfInstallation.value ?? Preferences.DAU.installationDate.value?.mondayOfCurrentWeekFormatted, AppConstants.buildChannel != .debug { - braveCore.initializeP3AService( + AppState.shared.braveCore.initializeP3AService( forChannel: AppConstants.buildChannel.serverChannelParam, weekOfInstall: weekOfInstall ) @@ -369,6 +237,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { await self.cleanUpLargeTemporaryDirectory() } + Task(priority: .high) { + // Start preparing the ad-block services right away + // So it's ready a lot faster + await LaunchHelper.shared.prepareAdBlockServices( + adBlockService: AppState.shared.braveCore.adblockService + ) + } + + // Setup Playlist + // This restores the playlist incomplete downloads. So if a download was started + // and interrupted on application death, we restart it on next launch. + Task(priority: .low) { @MainActor in + PlaylistManager.shared.setupPlaylistFolder() + PlaylistManager.shared.restoreSession() + } + return shouldPerformAdditionalDelegateHandling } @@ -383,13 +267,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #endif func applicationWillTerminate(_ application: UIApplication) { - // We have only five seconds here, so let's hope this doesn't take too long. - sceneInfo?.profile.shutdown() - + AppState.shared.profile.shutdown() + SKPaymentQueue.default().remove(BraveVPN.iapObserver) // Clean up BraveCore - braveCore.syncAPI.removeAllObservers() + AppState.shared.braveCore.syncAPI.removeAllObservers() log.debug("Cleanly Terminated the Application") } @@ -402,45 +285,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - func syncOnDidEnterBackground(application: UIApplication) { - // BRAVE TODO: Decide whether or not we want to use this for our own sync down the road - - var taskId = UIBackgroundTaskIdentifier(rawValue: 0) - taskId = application.beginBackgroundTask { - print("Running out of background time, but we have a profile shutdown pending.") - self.shutdownProfileWhenNotActive(application) - application.endBackgroundTask(taskId) - } - - sceneInfo?.profile.shutdown() - application.endBackgroundTask(taskId) - } - - fileprivate func shutdownProfileWhenNotActive(_ application: UIApplication) { - // Only shutdown the profile if we are not in the foreground - guard application.applicationState != .active else { - return - } - - sceneInfo?.profile.shutdown() - } - - func setupCustomSchemeHandlers(_ profile: Profile) { - let responders: [(String, InternalSchemeResponse)] = [ - (AboutHomeHandler.path, AboutHomeHandler()), - (AboutLicenseHandler.path, AboutLicenseHandler()), - (SessionRestoreHandler.path, SessionRestoreHandler()), - (ErrorPageHandler.path, ErrorPageHandler()), - (ReaderModeHandler.path, ReaderModeHandler(profile: profile)), - (IPFSSchemeHandler.path, IPFSSchemeHandler()), - (Web3DomainHandler.path, Web3DomainHandler()) - ] - - responders.forEach { (path, responder) in - InternalSchemeHandler.responders[path] = responder - } - } - fileprivate func setUserAgent() { let userAgent = UserAgent.userAgentForDesktopMode @@ -455,10 +299,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Record the user agent for use by search suggestion clients. SearchViewController.userAgent = userAgent } - - func sceneInfo(for sceneSession: UISceneSession) -> SceneInfoModel? { - return sceneInfo - } /// Dumps the temporary directory if the total size of the directory exceeds a size threshold (in bytes) private nonisolated func cleanUpLargeTemporaryDirectory(thresholdInBytes: Int = 100_000_000) async { @@ -494,7 +334,6 @@ extension AppDelegate: MFMailComposeViewControllerDelegate { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { // Dismiss the view controller and start the app up controller.dismiss(animated: true, completion: nil) - startApplication(application!, withLaunchOptions: self.launchOptions) } } @@ -517,5 +356,11 @@ extension AppDelegate { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + + sceneSessions.forEach { session in + if let windowIdString = session.scene?.userActivity?.userInfo?["WindowID"] as? String, let windowId = UUID(uuidString: windowIdString) { + SessionWindow.delete(windowId: windowId) + } + } } } diff --git a/App/iOS/Delegates/AppState.swift b/App/iOS/Delegates/AppState.swift new file mode 100644 index 00000000000..ca17be7eb82 --- /dev/null +++ b/App/iOS/Delegates/AppState.swift @@ -0,0 +1,229 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// 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 Foundation +import UIKit +import BraveCore +import Brave +import Data +import RuntimeWarnings +import Shared +import Growth +import Preferences +import Storage +import BraveNews +import os.log + +private let adsRewardsLog = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ads-rewards") + +/// Class that does startup initialization +/// Everything in this class can only be execute ONCE +/// IE: BraveCore initialization, BuildChannel, Migrations, etc. +public class AppState { + private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-state") + + public static let shared = AppState() + + public let braveCore: BraveCoreMain + public let dau: DAU + public let migration: Migration + public let profile: Profile + public let rewards: Brave.BraveRewards + public let newsFeedDataSource: FeedDataSource + private var didBecomeActive = false + + public var state: State = .launching(options: [:], active: false) { + didSet { + switch state { + case .launching(_, let isActive): + if didBecomeActive { + assertionFailure("Cannot set launching state twice!") + } + + if isActive && !didBecomeActive { + // We have to wait until pre 1.12 migration is done until we proceed with database + // initialization. This is because Database container may change. See bugs #3416, #3377. + didBecomeActive = true + DataController.shared.initializeOnce() + Migration.postCoreDataInitMigrations() + Migration.migrateTabStateToWebkitState(diskImageStore: diskImageStore) + } + break + case .active: + break + case .backgrounded: + break + case .terminating: + break + } + } + } + + private(set) public lazy var diskImageStore = { () -> DiskImageStore? in + do { + return try DiskImageStore( + files: profile.files, + namespace: "TabManagerScreenshots", + quality: UIConstants.screenshotQuality) + } catch { + log.error("Failed to create an image store for files: \(self.profile.files.rootPath) and namespace: \"TabManagerScreenshots\": \(error.localizedDescription)") + } + return nil + }() + + private init() { + // Setup Constants + AppState.setupConstants() + + // Setup BraveCore + braveCore = AppState.setupBraveCore().then { + $0.scheduleLowPriorityStartupTasks() + } + + // Setup DAU + dau = DAU(braveCoreStats: braveCore.braveStats) + + // Setup Profile + profile = BrowserProfile(localName: "profile") + + // Setup Migrations + migration = Migration(braveCore: braveCore) + + // Perform Migrations + migration.launchMigrations(keyPrefix: profile.prefs.getBranchPrefix(), profile: profile) + + // Setup Rewards & Ads + let configuration = BraveRewards.Configuration.current() + Self.migrateAdsConfirmations(for: configuration) + rewards = BraveRewards(configuration: configuration) + newsFeedDataSource = FeedDataSource() + + // Setup Custom URL scheme handlers + setupCustomSchemeHandlers(profile: profile) + } + + public enum State { + case launching(options: [UIApplication.LaunchOptionsKey: Any], active: Bool) + case active + case backgrounded + case terminating + } + + private static func setupConstants() { + } + + private static func setupBraveCore() -> BraveCoreMain { + // BraveCore Log Handler + BraveCoreMain.setLogHandler { severity, file, line, messageStartIndex, message in + let message = String(message.dropFirst(messageStartIndex).dropLast()) + .trimmingCharacters(in: .whitespacesAndNewlines) + if message.isEmpty { + // Nothing to print + return true + } + + if severity == .fatal { + let filename = URL(fileURLWithPath: file).lastPathComponent + #if DEBUG + // Prints a special runtime warning instead of crashing. + os_log( + .fault, + dso: os_rw.dso, + log: os_rw.log(category: "BraveCore"), + "[%@:%ld] > %@", filename, line, message + ) + return true + #else + fatalError("Fatal BraveCore Error at \(filename):\(line).\n\(message)") + #endif + } + + let level: OSLogType = { + switch severity { + case .fatal: return .fault + case .error: return .error + // No `.warning` level exists for OSLogType. os_Log.warning is an alias for `.error`. + case .warning: return .error + case .info: return .info + default: return .debug + } + }() + + let braveCoreLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "brave-core") + if AppConstants.buildChannel.isPublic { + braveCoreLogger.log(level: level, "\(message, privacy: .private)") + } else { + braveCoreLogger.log(level: level, "\(message, privacy: .public)") + } + + return true + } + + // Initialize BraveCore Switches + var switches: [BraveCoreSwitch] = [] + if !AppConstants.buildChannel.isPublic { + // Check prefs for additional switches + let activeSwitches = Set(Preferences.BraveCore.activeSwitches.value) + let switchValues = Preferences.BraveCore.switchValues.value + for activeSwitch in activeSwitches { + let key = BraveCoreSwitchKey(rawValue: activeSwitch) + if key.isValueless { + switches.append(.init(key: key)) + } else if let value = switchValues[activeSwitch], !value.isEmpty { + switches.append(.init(key: key, value: value)) + } + } + } + switches.append(.init(key: .rewardsFlags, value: BraveRewards.Configuration.current().flags)) + + // Initialize BraveCore + return BraveCoreMain(userAgent: UserAgent.mobile, additionalSwitches: switches) + } + + private func setupCustomSchemeHandlers(profile: Profile) { + let responders: [(String, InternalSchemeResponse)] = [ + (AboutHomeHandler.path, AboutHomeHandler()), + (AboutLicenseHandler.path, AboutLicenseHandler()), + (SessionRestoreHandler.path, SessionRestoreHandler()), + (ErrorPageHandler.path, ErrorPageHandler()), + (ReaderModeHandler.path, ReaderModeHandler(profile: profile)), + (IPFSSchemeHandler.path, IPFSSchemeHandler()), + (Web3DomainHandler.path, Web3DomainHandler()) + ] + + responders.forEach { (path, responder) in + InternalSchemeHandler.responders[path] = responder + } + } + + private static func migrateAdsConfirmations(for configuruation: Brave.BraveRewards.Configuration) { + // To ensure after a user launches 1.21 that their ads confirmations, viewed count and + // estimated payout remain correct. + // + // This hack is unfortunately neccessary due to a missed migration path when moving + // confirmations from ledger to ads, we must extract `confirmations.json` out of ledger's + // state file and save it as a new file under the ads directory. + let base = configuruation.storageURL + let ledgerStateContainer = base.appendingPathComponent("ledger/random_state.plist") + let adsConfirmations = base.appendingPathComponent("ads/confirmations.json") + let fm = FileManager.default + + if !fm.fileExists(atPath: ledgerStateContainer.path) || fm.fileExists(atPath: adsConfirmations.path) { + // Nothing to migrate or already migrated + return + } + + do { + let contents = NSDictionary(contentsOfFile: ledgerStateContainer.path) + guard let confirmations = contents?["confirmations.json"] as? String else { + adsRewardsLog.debug("No confirmations found to migrate in ledger's state container") + return + } + try confirmations.write(toFile: adsConfirmations.path, atomically: true, encoding: .utf8) + } catch { + adsRewardsLog.error("Failed to migrate confirmations.json to ads folder: \(error.localizedDescription)") + } + } +} diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 4e0b1a92cdf..d8223ac24fc 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -17,6 +17,7 @@ import BraveVPN import Growth import os.log import BraveCore +import BraveNews import Preferences class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -25,72 +26,59 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Chromium force unwraps it and uses it. For this reason, we always set this window property to the scene's main window. internal var window: UIWindow? private var windowProtection: WindowProtection? - private var sceneInfo: AppDelegate.SceneInfoModel? static var shouldHandleUrpLookup = false private var cancellables: Set = [] - private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "scene-delegate") func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } - - // Create a browser instance - // There has to be an application delegate - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - fatalError("Failed to create browser instance") - } - - guard let sceneInfo = appDelegate.sceneInfo(for: session) else { - return - } - - self.sceneInfo = sceneInfo - // We have to wait until pre1.12 migration is done until we proceed with database - // initialization. This is because Database container may change. See bugs #3416, #3377. - DataController.shared.initializeOnce() - Migration.postCoreDataInitMigrations() - Migration.migrateTabStateToWebkitState(diskImageStore: sceneInfo.diskImageStore) + let browserViewController = createBrowserWindow( + scene: windowScene, + braveCore: AppState.shared.braveCore, + profile: AppState.shared.profile, + diskImageStore: AppState.shared.diskImageStore, + migration: AppState.shared.migration, + rewards: AppState.shared.rewards, + newsFeedDataSource: AppState.shared.newsFeedDataSource, + userActivity: connectionOptions.userActivities.first ?? session.stateRestorationActivity + ) - Task(priority: .high) { - // Start preparing the ad-block services right away - // So it's ready a lot faster - await LaunchHelper.shared.prepareAdBlockServices( - adBlockService: appDelegate.braveCore.adblockService - ) + let conditions = scene.activationConditions + conditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(value: true) + if let windowId = session.userInfo?["WindowID"] as? UUID { + let preferPredicate = NSPredicate(format: "self == %@", windowId.uuidString) + conditions.prefersToActivateForTargetContentIdentifierPredicate = preferPredicate } - + Preferences.General.themeNormalMode.objectWillChange .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateTheme() + .sink { [weak self, weak scene] _ in + guard let self = self, + let scene = scene as? UIWindowScene else { return } + self.updateTheme(for: scene) } .store(in: &cancellables) Preferences.General.nightModeEnabled.objectWillChange .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateTheme() + .sink { [weak self, weak scene] _ in + guard let self = self, + let scene = scene as? UIWindowScene else { return } + self.updateTheme(for: scene) } .store(in: &cancellables) - PrivateBrowsingManager.shared.$isPrivateBrowsing + browserViewController.privateBrowsingManager.$isPrivateBrowsing .removeDuplicates() .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateTheme() + .sink { [weak self, weak scene] _ in + guard let self = self, + let scene = scene as? UIWindowScene else { return } + self.updateTheme(for: scene) } .store(in: &cancellables) - - let browserViewController = createBrowserWindow( - scene: windowScene, - braveCore: appDelegate.braveCore, - profile: sceneInfo.profile, - diskImageStore: sceneInfo.diskImageStore, - migration: sceneInfo.migration, - rewards: sceneInfo.rewards - ) if SceneDelegate.shouldHandleUrpLookup { // TODO: Find a better way to do this when multiple windows are involved. @@ -101,12 +89,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - // Setup Playlist - // This restores the playlist incomplete downloads. So if a download was started - // and interrupted on application death, we restart it on next launch. - PlaylistManager.shared.setupPlaylistFolder() - PlaylistManager.shared.restoreSession() - // Setup Playlist Car-Play // TODO: Decide what to do if we have multiple windows // as it is only possible to have a single car-play instance. @@ -124,7 +106,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { PrivacyReportsManager.scheduleNotification(debugMode: !AppConstants.buildChannel.isPublic) PrivacyReportsManager.consolidateData() - PrivacyReportsManager.scheduleProcessingBlockedRequests() + PrivacyReportsManager.scheduleProcessingBlockedRequests(isPrivateBrowsing: browserViewController.privateBrowsingManager.isPrivateBrowsing) PrivacyReportsManager.scheduleVPNAlertsTask() } @@ -138,7 +120,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Assign each browser a window of its own let window = UIWindow(windowScene: windowScene).then { $0.backgroundColor = .black - $0.overrideUserInterfaceStyle = expectedThemeOverride + $0.overrideUserInterfaceStyle = expectedThemeOverride(for: windowScene) $0.tintColor = .braveBlurpleTint $0.rootViewController = navigationController @@ -175,21 +157,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidDisconnect(_ scene: UIScene) { - + log.debug("SCENE DISCONNECTED") } func sceneDidBecomeActive(_ scene: UIScene) { + scene.userActivity?.becomeCurrent() + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, - let scene = scene as? UIWindowScene, - let profile = sceneInfo?.profile + let scene = scene as? UIWindowScene else { return } - + + if let windowId = (scene.userActivity?.userInfo?["WindowID"] ?? + scene.session.userInfo?["WindowID"]) as? String, + let windowUUID = UUID(uuidString: windowId) { + SessionWindow.setSelected(windowId: windowUUID) + } + Preferences.AppState.backgroundedCleanly.value = false - - profile.reopen() - appDelegate.setupCustomSchemeHandlers(profile) + AppState.shared.profile.reopen() appDelegate.receivedURLs = nil UIApplication.shared.applicationIconBadgeNumber = 0 @@ -211,14 +198,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // (offline, bad connection etc.). // Also send the ping only after the URP lookup has processed. if Preferences.URP.referralLookupOutstanding.value == false { - appDelegate.dau.sendPingToServer() + AppState.shared.dau.sendPingToServer() } - BraveSkusManager.refreshSKUCredential(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + BraveSkusManager.refreshSKUCredential(isPrivate: scene.browserViewController?.privateBrowsingManager.isPrivateBrowsing == true) } func sceneWillResignActive(_ scene: UIScene) { Preferences.AppState.backgroundedCleanly.value = true + scene.userActivity?.resignCurrent() } func sceneWillEnterForeground(_ scene: UIScene) { @@ -228,11 +216,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidEnterBackground(_ scene: UIScene) { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return - } - - appDelegate.syncOnDidEnterBackground(application: UIApplication.shared) + AppState.shared.profile.shutdown() BraveVPN.sendVPNWorksInBackgroundNotification() } @@ -243,7 +227,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } URLContexts.forEach({ - guard let routerpath = NavigationPath(url: $0.url) else { + guard let routerpath = NavigationPath(url: $0.url, isPrivateBrowsing: scene.browserViewController?.privateBrowsingManager.isPrivateBrowsing == true) else { log.debug("Invalid Navigation Path: \($0.url)") return } @@ -251,9 +235,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { scene.browserViewController?.handleNavigationPath(path: routerpath) }) } + + func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) { + log.debug("Updated User Activity for Scene") + } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { - guard let scene = scene as? UIWindowScene else { return } @@ -361,7 +348,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - if let browserViewController = windowScene.browserViewController { QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController) completionHandler(true) @@ -371,59 +357,165 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { - return nil + return scene.userActivity } } extension SceneDelegate { - private var expectedThemeOverride: UIUserInterfaceStyle { + private func expectedThemeOverride(for scene: UIWindowScene?) -> UIUserInterfaceStyle { // The expected appearance theme should be dark mode when night mode is enabled for websites let themeValue = Preferences.General.nightModeEnabled.value ? DefaultTheme.dark.rawValue : Preferences.General.themeNormalMode.value let themeOverride = DefaultTheme(rawValue: themeValue)?.userInterfaceStyleOverride ?? .unspecified - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = scene?.browserViewController?.privateBrowsingManager.isPrivateBrowsing == true return isPrivateBrowsing ? .dark : themeOverride } - private func updateTheme() { - guard let window = UIApplication.shared.windows.first(where: { (window) -> Bool in window.isKeyWindow }) else { return } - UIView.transition( - with: window, duration: 0.15, options: [.transitionCrossDissolve], - animations: { - window.overrideUserInterfaceStyle = self.expectedThemeOverride - }, completion: nil) + private func updateTheme(for scene: UIWindowScene) { + scene.windows.forEach { window in + UIView.transition( + with: window, duration: 0.15, options: [.transitionCrossDissolve], + animations: { + window.overrideUserInterfaceStyle = self.expectedThemeOverride(for: scene) + }, completion: nil) + } } } extension SceneDelegate { - private func createBrowserWindow(scene: UIWindowScene, braveCore: BraveCoreMain, profile: Profile, diskImageStore: DiskImageStore?, migration: Migration?, rewards: Brave.BraveRewards) -> BrowserViewController { - // Make sure current private browsing flag respects the private browsing only user preference - PrivateBrowsingManager.shared.isPrivateBrowsing = Preferences.Privacy.privateBrowsingOnly.value + private func createBrowserWindow(scene: UIWindowScene, + braveCore: BraveCoreMain, + profile: Profile, + diskImageStore: DiskImageStore?, + migration: Migration?, + rewards: Brave.BraveRewards, + newsFeedDataSource: BraveNews.FeedDataSource, + userActivity: NSUserActivity?) -> BrowserViewController { + + let privateBrowsingManager = PrivateBrowsingManager() // Don't track crashes if we're building the development environment due to the fact that terminating/stopping // the simulator via Xcode will count as a "crash" and lead to restore popups in the subsequent launch let crashedLastSession = !Preferences.AppState.backgroundedCleanly.value && AppConstants.buildChannel != .debug + + // Store the scene's activities + let windowId: UUID + let isPrivate: Bool + let urlToOpen: URL? + + if let userActivity = userActivity { + // Restore the scene with the WindowID from the User-Activity + + let windowIdString = userActivity.userInfo?["WindowID"] as? String ?? "" + windowId = UUID(uuidString: windowIdString) ?? UUID() + isPrivate = userActivity.userInfo?["isPrivate"] as? Bool == true + privateBrowsingManager.isPrivateBrowsing = isPrivate + urlToOpen = userActivity.userInfo?["OpenURL"] as? URL + + // Create a new session window + SessionWindow.createWindow(isPrivate: isPrivate, isSelected: true, uuid: windowId) + + scene.userActivity = userActivity + scene.session.userInfo?["WindowID"] = windowId + scene.session.userInfo?["isPrivate"] = isPrivate + } else if let sceneWindowId = scene.session.userInfo?["WindowID"] as? String, + let sceneIsPrivate = scene.session.userInfo?["isPrivate"] as? Bool, + let windowUUID = UUID(uuidString: sceneWindowId) { + + // Restore the scene from the Session's User-Info WindowID + + windowId = windowUUID + isPrivate = sceneIsPrivate + privateBrowsingManager.isPrivateBrowsing = sceneIsPrivate + urlToOpen = scene.session.userInfo?["OpenURL"] as? URL + + scene.userActivity = BrowserState.userActivity(for: windowId, isPrivate: isPrivate) + } else { + // Should NOT be possible to get here. + // However, if a controller is NOT active, and tapping the app-icon opens a New-Window + // Then we need to make sure not to restore that "New" Window + // So we iterate all the windows and if there is no active window, then we need to "Restore" one. + // If a window is already active, we need to create a new blank window. + + if let activeWindowId = SessionWindow.getActiveWindow(context: DataController.swiftUIContext)?.windowId { + let activeSession = UIApplication.shared.openSessions + .compactMap({ $0.userInfo?["WindowID"] as? String }) + .first(where: { $0 == activeWindowId.uuidString }) + + if activeSession != nil { + // An existing window is already active on screen + // So create a new window + let newWindowId = UUID() + SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: newWindowId) + windowId = newWindowId + } else { + // Restore the active window since none is active on screen + windowId = activeWindowId + } + } else { + // Should be impossible to get here. There must always be an active window. + // However, if for some reason there is none, then we should create one. + let newWindowId = UUID() + SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: newWindowId) + windowId = newWindowId + } + + isPrivate = false + privateBrowsingManager.isPrivateBrowsing = false + urlToOpen = nil + + scene.userActivity = BrowserState.userActivity(for: windowId, isPrivate: false) + scene.session.userInfo = ["WindowID": windowId.uuidString, + "isPrivate": false] + } // Create a browser instance let browserViewController = BrowserViewController( + windowId: windowId, profile: profile, diskImageStore: diskImageStore, braveCore: braveCore, rewards: rewards, migration: migration, - crashedLastSession: crashedLastSession) + crashedLastSession: crashedLastSession, + newsFeedDataSource: newsFeedDataSource, + privateBrowsingManager: privateBrowsingManager + ) browserViewController.do { $0.edgesForExtendedLayout = [] // Add restoration class, the factory that will return the ViewController we will restore with. - $0.restorationIdentifier = NSStringFromClass(BrowserViewController.self) + $0.restorationIdentifier = BrowserState.sceneId $0.restorationClass = SceneDelegate.self // Remove Ad-Grant Reminders $0.removeScheduledAdGrantReminders() } + + if let tabIdString = userActivity?.userInfo?["TabID"] as? String, + let tabWindowId = userActivity?.userInfo?["TabWindowID"] as? String, + let tabId = UUID(uuidString: tabIdString) { + + let currentTabScene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).filter({ + guard let sceneWindowId = $0.session.userInfo?["WindowID"] as? String else { + return false + } + + return sceneWindowId == tabWindowId + }).first + + if let currentTabScene = currentTabScene, let currentTabSceneBrowser = currentTabScene.browserViewController { + browserViewController.loadViewIfNeeded() + currentTabSceneBrowser.moveTab(tabId: tabId, to: browserViewController) + } + } + + if let urlToOpen = urlToOpen { + browserViewController.loadViewIfNeeded() + browserViewController.switchToTabForURLOrOpen(urlToOpen, isPrivileged: false) + } return browserViewController } @@ -442,7 +534,7 @@ extension BrowserViewController { urp.referralLookup() { referralCode, offerUrl in // Attempting to send ping after first urp lookup. // This way we can grab the referral code if it exists, see issue #2586. - (UIApplication.shared.delegate as? AppDelegate)?.dau.sendPingToServer() + AppState.shared.dau.sendPingToServer() if let code = referralCode { let retryTime = AppConstants.buildChannel.isPublic ? 1.days : 10.minutes let retryDeadline = Date() + retryTime diff --git a/App/iOS/Supporting Files/Info.plist b/App/iOS/Supporting Files/Info.plist index f6c92dfeb7b..cde218b12b9 100644 --- a/App/iOS/Supporting Files/Info.plist +++ b/App/iOS/Supporting Files/Info.plist @@ -89,13 +89,14 @@ ShortcutsConfigurationIntent StatsConfigurationIntent com.brave.ios.browsing + com.brave.ios.browser-scene SERVICES_KEY $(BRAVE_SERVICES_KEY) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - + UISceneConfigurations CPTemplateApplicationSceneSessionRoleApplication diff --git a/Sources/Brave/Frontend/Brave Rewards/Panel/BraveRewardsViewController.swift b/Sources/Brave/Frontend/Brave Rewards/Panel/BraveRewardsViewController.swift index 9ab7127205a..8935f3c5ccb 100644 --- a/Sources/Brave/Frontend/Brave Rewards/Panel/BraveRewardsViewController.swift +++ b/Sources/Brave/Frontend/Brave Rewards/Panel/BraveRewardsViewController.swift @@ -34,7 +34,7 @@ class BraveRewardsViewController: UIViewController, PopoverContentComponent { } else { if let url = tab.url { rewardsView.publisherView.faviconImageView.contentMode = .scaleAspectFit - rewardsView.publisherView.faviconImageView.loadFavicon(for: url) + rewardsView.publisherView.faviconImageView.loadFavicon(for: url, isPrivateBrowsing: tab.isPrivate) } else { rewardsView.publisherView.faviconImageView.isHidden = true } diff --git a/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift b/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift index fce79c67441..554f6ccad6a 100644 --- a/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift +++ b/Sources/Brave/Frontend/Browser/BackForwardListViewController.swift @@ -216,6 +216,7 @@ class BackForwardListViewController: UIViewController, UITableViewDataSource, UI return extracted.absoluteString }() + cell.isPrivateBrowsing = tabManager.privateBrowsingManager.isPrivateBrowsing cell.isCurrentTab = listData[indexPath.item] == self.currentItem cell.connectingBackwards = indexPath.item != listData.count - 1 cell.connectingForwards = indexPath.item != 0 diff --git a/Sources/Brave/Frontend/Browser/BackForwardTableViewCell.swift b/Sources/Brave/Frontend/Browser/BackForwardTableViewCell.swift index cf24846125c..99ffb39fd52 100644 --- a/Sources/Brave/Frontend/Browser/BackForwardTableViewCell.swift +++ b/Sources/Brave/Frontend/Browser/BackForwardTableViewCell.swift @@ -43,6 +43,8 @@ class BackForwardTableViewCell: UITableViewCell { label.textColor = BackForwardViewCellUX.textColor return label }() + + var isPrivateBrowsing: Bool = false var connectingForwards = true { didSet { @@ -70,7 +72,7 @@ class BackForwardTableViewCell: UITableViewCell { faviconView.backgroundColor = .white faviconView.image = Favicon.defaultImage } else { - faviconView.loadFavicon(for: s.tileURL) + faviconView.loadFavicon(for: s.tileURL, isPrivateBrowsing: isPrivateBrowsing) } var title = s.title if title.isEmpty { diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController.swift index 1f875757984..6c2c84b9982 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController.swift @@ -52,7 +52,7 @@ public class BrowserViewController: UIViewController { private(set) lazy var topToolbar: TopToolbarView = { // Setup the URL bar, wrapped in a view to get transparency effect - let topToolbar = TopToolbarView(voiceSearchSupported: speechRecognizer.isVoiceSearchAvailable) + let topToolbar = TopToolbarView(voiceSearchSupported: speechRecognizer.isVoiceSearchAvailable, privateBrowsingManager: privateBrowsingManager) topToolbar.translatesAutoresizingMaskIntoConstraints = false topToolbar.delegate = self topToolbar.tabToolbarDelegate = self @@ -69,7 +69,7 @@ public class BrowserViewController: UIViewController { }() // These views wrap the top and bottom toolbars to provide background effects on them - let header = HeaderContainerView() + private(set) lazy var header = HeaderContainerView(privateBrowsingManager: privateBrowsingManager) private let headerHeightLayoutGuide = UILayoutGuide() let footer: UIView = { @@ -133,7 +133,7 @@ public class BrowserViewController: UIViewController { // Single data source used for all favorites vcs public let backgroundDataSource: NTPDataSource - let feedDataSource = FeedDataSource() + let feedDataSource: FeedDataSource private var postSetupTasks: [() -> Void] = [] private var setupTasksCompleted: Bool = false @@ -163,11 +163,13 @@ public class BrowserViewController: UIViewController { var displayedPopoverController: UIViewController? var updateDisplayedPopoverProperties: (() -> Void)? + public let windowId: UUID let profile: Profile let braveCore: BraveCoreMain let tabManager: TabManager let migration: Migration? let bookmarkManager: BookmarkManager + public let privateBrowsingManager: PrivateBrowsingManager /// Whether last session was a crash or not private let crashedLastSession: Bool @@ -272,27 +274,37 @@ public class BrowserViewController: UIViewController { var topToolbarDidPressReloadTask: Task<(), Never>? public init( + windowId: UUID, profile: Profile, diskImageStore: DiskImageStore?, braveCore: BraveCoreMain, rewards: BraveRewards, migration: Migration?, - crashedLastSession: Bool + crashedLastSession: Bool, + newsFeedDataSource: FeedDataSource, + privateBrowsingManager: PrivateBrowsingManager ) { + self.windowId = windowId self.profile = profile self.braveCore = braveCore self.bookmarkManager = BookmarkManager(bookmarksAPI: braveCore.bookmarksAPI) self.rewards = rewards self.migration = migration self.crashedLastSession = crashedLastSession + self.privateBrowsingManager = privateBrowsingManager + self.feedDataSource = newsFeedDataSource feedDataSource.historyAPI = braveCore.historyAPI - backgroundDataSource = .init(service: braveCore.backgroundImagesService) + backgroundDataSource = .init(service: braveCore.backgroundImagesService, + privateBrowsingManager: privateBrowsingManager) // Initialize TabManager self.tabManager = TabManager( + windowId: windowId, prefs: profile.prefs, rewards: rewards, - tabGeneratorAPI: braveCore.tabGeneratorAPI) + tabGeneratorAPI: braveCore.tabGeneratorAPI, + privateBrowsingManager: privateBrowsingManager + ) // Add Regular tabs to Sync Chain if Preferences.Chromium.syncOpenTabsEnabled.value { @@ -339,7 +351,7 @@ public class BrowserViewController: UIViewController { } } - feedDataSource.ads = rewards.ads + self.feedDataSource.ads = rewards.ads // Observer watching tab information is sent by another device openTabsModelStateListener = braveCore.sendTabAPI.add( @@ -443,7 +455,7 @@ public class BrowserViewController: UIViewController { pageZoomListener = NotificationCenter.default.addObserver(forName: PageZoomView.notificationName, object: nil, queue: .main) { [weak self] _ in self?.tabManager.allTabs.forEach({ guard let url = $0.webView?.url else { return } - let zoomLevel = PrivateBrowsingManager.shared.isPrivateBrowsing ? 1.0 : Domain.getPersistedDomain(for: url)?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value + let zoomLevel = self?.privateBrowsingManager.isPrivateBrowsing == true ? 1.0 : Domain.getPersistedDomain(for: url)?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value $0.webView?.setValue(zoomLevel, forKey: PageZoomView.propertyName) }) } @@ -550,7 +562,7 @@ public class BrowserViewController: UIViewController { notificationsPresenter: notificationsPresenter) notificationsHandler?.canShowNotifications = { [weak self] in guard let self = self else { return false } - return !PrivateBrowsingManager.shared.isPrivateBrowsing && !self.topToolbar.inOverlayMode + return !self.privateBrowsingManager.isPrivateBrowsing && !self.topToolbar.inOverlayMode } notificationsHandler?.actionOccured = { [weak self] ad, action in guard let self = self, let ad = ad else { return } @@ -567,7 +579,7 @@ public class BrowserViewController: UIViewController { return } let request = URLRequest(url: targetURL) - self.tabManager.addTabAndSelect(request, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self.tabManager.addTabAndSelect(request, isPrivate: self.privateBrowsingManager.isPrivateBrowsing) } } } @@ -608,7 +620,7 @@ public class BrowserViewController: UIViewController { bottomTouchArea.isEnabled = showToolbar if showToolbar { - toolbar = BottomToolbarView() + toolbar = BottomToolbarView(privateBrowsingManager: privateBrowsingManager) toolbar?.setSearchButtonState(url: tabManager.selectedTab?.url) footer.addSubview(toolbar!) toolbar?.tabToolbarDelegate = self @@ -872,7 +884,7 @@ public class BrowserViewController: UIViewController { scheduleDefaultBrowserNotification() } - privateModeCancellable = PrivateBrowsingManager.shared + privateModeCancellable = privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -1081,7 +1093,7 @@ public class BrowserViewController: UIViewController { fileprivate func showRestoreTabsAlert() { guard canRestoreTabs() else { - self.tabManager.addTabAndSelect(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self.tabManager.addTabAndSelect(isPrivate: self.privateBrowsingManager.isPrivateBrowsing) return } let alert = UIAlertController.restoreTabsAlert( @@ -1090,7 +1102,7 @@ public class BrowserViewController: UIViewController { }, noCallback: { _ in SessionTab.deleteAll() - self.tabManager.addTabAndSelect(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self.tabManager.addTabAndSelect(isPrivate: self.privateBrowsingManager.isPrivateBrowsing) } ) self.present(alert, animated: true, completion: nil) @@ -1123,6 +1135,24 @@ public class BrowserViewController: UIViewController { show(toast: toast, afterWaiting: ButtonToastUX.toastDelay) } showQueuedAlertIfAvailable() + + let isPrivateBrowsing = SessionWindow.from(windowId: windowId)?.isPrivate == true + var userActivity = view.window?.windowScene?.userActivity + if userActivity == nil { + userActivity = BrowserState.userActivity(for: windowId, isPrivate: isPrivateBrowsing) + } else { + userActivity?.targetContentIdentifier = windowId.uuidString + userActivity?.addUserInfoEntries(from: ["WindowID": windowId.uuidString, + "isPrivate": isPrivateBrowsing]) + } + + view.window?.windowScene?.userActivity = userActivity + view.window?.windowScene?.session.userInfo = ["WindowID": windowId.uuidString, + "isPrivate": isPrivateBrowsing] + + for session in UIApplication.shared.openSessions { + UIApplication.shared.requestSceneSessionRefresh(session) + } } /// Whether or not to show the playlist onboarding callout this session @@ -1141,6 +1171,7 @@ public class BrowserViewController: UIViewController { super.viewWillDisappear(animated) rewards.ledger?.selectedTabId = 0 + view.window?.windowScene?.userActivity = nil } /// A layout guide defining where the favorites and NTP overlay are placed @@ -1299,7 +1330,9 @@ public class BrowserViewController: UIViewController { profile: profile, dataSource: backgroundDataSource, feedDataSource: feedDataSource, - rewards: rewards) + rewards: rewards, + privateBrowsingManager: privateBrowsingManager + ) // Donate NewTabPage Activity For Custom Suggestions let newTabPageActivity = ActivityShortcutManager.shared.createShortcutActivity(type: selectedTab.isPrivate ? .newPrivateTab : .newTab) @@ -1382,7 +1415,7 @@ public class BrowserViewController: UIViewController { public func presentCorrespondingVPNViewController() { if BraveSkusManager.keepShowingSessionExpiredState { let alert = BraveSkusManager.sessionExpiredStateAlert(loginCallback: { [unowned self] _ in - self.openURLInNewTab(.brave.account, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, + self.openURLInNewTab(.brave.account, isPrivate: self.privateBrowsingManager.isPrivateBrowsing, isPrivileged: false) }) @@ -1834,6 +1867,17 @@ public class BrowserViewController: UIViewController { navigationToolbar.updatePageStatus(isPage) updateWebViewPageZoom(tab: tab) } + + public func moveTab(tabId: UUID, to browser: BrowserViewController) { + guard let tab = tabManager.allTabs.filter({ $0.id == tabId }).first, + let url = tab.url else { + return + } + + let isPrivate = tab.isPrivate + tabManager.removeTab(tab) + browser.tabManager.addTabsForURLs([url], zombie: false, isPrivate: isPrivate) + } public func switchToTabForURLOrOpen(_ url: URL, isPrivate: Bool = false, isPrivileged: Bool, isExternal: Bool = false) { if !isExternal { @@ -1855,7 +1899,7 @@ public class BrowserViewController: UIViewController { } else if let tab = tabManager.getTabForURL(url) { tabManager.selectTab(tab) } else { - openURLInNewTab(url, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, isPrivileged: false) + openURLInNewTab(url, isPrivate: privateBrowsingManager.isPrivateBrowsing, isPrivileged: false) } } @@ -1907,6 +1951,25 @@ public class BrowserViewController: UIViewController { } } } + + func openInNewWindow(url: URL?, isPrivate: Bool) { + let activity = BrowserState.userActivity(for: UUID(), isPrivate: isPrivate) + + if let url = url { + activity.addUserInfoEntries(from: ["OpenURL": url]) + } + + let options = UIScene.ActivationRequestOptions().then { + $0.requestingScene = view.window?.windowScene + } + + UIApplication.shared.requestSceneSessionActivation(nil, + userActivity: activity, + options: options, + errorHandler: { error in + Logger.module.error("Error creating new window: \(error)") + }) + } func clearHistoryAndOpenNewTab() { // When PB Only mode is enabled @@ -1958,7 +2021,7 @@ public class BrowserViewController: UIViewController { var activities = [UIActivity]() // Adding SendTabToSelfActivity conditionally to show device selection screen - if !PrivateBrowsingManager.shared.isPrivateBrowsing, !url.isLocal, !InternalURL.isValid(url: url), !url.isReaderModeURL, + if !privateBrowsingManager.isPrivateBrowsing, !url.isLocal, !InternalURL.isValid(url: url), !url.isReaderModeURL, braveCore.syncAPI.isSendTabToSelfVisible { let sendTabToSelfActivity = SendTabToSelfActivity() { [weak self] in guard let self = self else { return } @@ -2176,7 +2239,7 @@ public class BrowserViewController: UIViewController { } guard let webView = tabManager.selectedTab?.webView else { return } - let pageZoomBar = UIHostingController(rootView: PageZoomView(webView: webView)) + let pageZoomBar = UIHostingController(rootView: PageZoomView(webView: webView, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)) pageZoomBar.rootView.dismiss = { [weak self] in guard let self = self else { return } @@ -2213,7 +2276,7 @@ public class BrowserViewController: UIViewController { if let currentURL = tab.url { let domain = Domain.getPersistedDomain(for: currentURL) - let zoomLevel = PrivateBrowsingManager.shared.isPrivateBrowsing ? 1.0 : domain?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value + let zoomLevel = privateBrowsingManager.isPrivateBrowsing ? 1.0 : domain?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value tab.webView?.setValue(zoomLevel, forKey: PageZoomView.propertyName) } } @@ -2230,7 +2293,7 @@ public class BrowserViewController: UIViewController { defer { setNeedsStatusBarAppearanceUpdate() } guard isUsingBottomBar, let tab = tabManager.selectedTab, tab.url.map(InternalURL.isValid) == false, let color = tab.webView?.sampledPageTopColor else { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if privateBrowsingManager.isPrivateBrowsing { statusBarOverlay.backgroundColor = .privateModeBackground } else { statusBarOverlay.backgroundColor = Preferences.General.nightModeEnabled.value ? .nightModeBackground : .urlBarBackground @@ -2260,21 +2323,21 @@ public class BrowserViewController: UIViewController { webView.evaluateSafeJavaScript(functionName: "\(ReaderModeNamespace).checkReadability", contentWorld: ReaderModeScriptHandler.scriptSandbox) // Only add history of a url which is not a localhost url - if !tab.isPrivate, !url.isReaderModeURL { + if !url.isReaderModeURL { // The visitType is checked If it is "typed" or not to determine the History object we are adding // should be synced or not. This limitation exists on browser side so we are aligning with this - if let visitType = typedNavigation.first(where: { - $0.key.typedDisplayString == url.typedDisplayString - })?.value, visitType == .typed { - braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date()) - } else { - braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date(), isURLTyped: false) - } - - // Saving Tab. Private Mode - not supported yet. if !tab.isPrivate { - tabManager.saveTab(tab) + if let visitType = typedNavigation.first(where: { + $0.key.typedDisplayString == url.typedDisplayString + })?.value, visitType == .typed { + braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date()) + } else { + braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date(), isURLTyped: false) + } } + + // Saving Tab. + tabManager.saveTab(tab) } } @@ -2414,7 +2477,7 @@ extension BrowserViewController { self.topToolbar.enterOverlayMode(overlayText, pasted: false, search: false) } - if !url.isBookmarklet && !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !url.isBookmarklet && !privateBrowsingManager.isPrivateBrowsing { RecentSearch.addItem(type: .qrCode, text: nil, websiteUrl: url.absoluteString) } } @@ -2423,7 +2486,7 @@ extension BrowserViewController { popToBVC() submitSearchText(text) - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { RecentSearch.addItem(type: .qrCode, text: text, websiteUrl: nil) } } @@ -2431,7 +2494,7 @@ extension BrowserViewController { extension BrowserViewController: SettingsDelegate { func settingsOpenURLInNewTab(_ url: URL) { - let forcedPrivate = PrivateBrowsingManager.shared.isPrivateBrowsing + let forcedPrivate = privateBrowsingManager.isPrivateBrowsing self.openURLInNewTab(url, isPrivate: forcedPrivate, isPrivileged: false) } @@ -2480,6 +2543,10 @@ extension BrowserViewController: TabsBarViewControllerDelegate { break } } + + func tabsBarDidSelectAddNewWindow(_ isPrivate: Bool) { + self.openInNewWindow(url: nil, isPrivate: isPrivate) + } } extension BrowserViewController: TabDelegate { @@ -2614,7 +2681,7 @@ extension BrowserViewController: TabDelegate { /// Triggered when "Search with Brave" is selected on selected web text func tab(_ tab: Tab, didSelectSearchWithBraveFor selectedText: String) { - let engine = profile.searchEngines.defaultEngine() + let engine = profile.searchEngines.defaultEngine(forType: tab.isPrivate ? .privateMode : .standard) guard let url = engine.searchURLForQuery(selectedText) else { assertionFailure("If this returns nil, investigate why and add proper handling or commenting") @@ -2627,7 +2694,7 @@ extension BrowserViewController: TabDelegate { isPrivate: tab.isPrivate ) - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { RecentSearch.addItem(type: .text, text: selectedText, websiteUrl: url.absoluteString) } } @@ -2666,7 +2733,7 @@ extension BrowserViewController: TabDelegate { vc.linkTapped = { [unowned self] request in tab.rewardsEnabledCallback?(false) self.tabManager - .addTabAndSelect(request, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + .addTabAndSelect(request, isPrivate: privateBrowsingManager.isPrivateBrowsing) } } @@ -2714,7 +2781,7 @@ extension BrowserViewController: TabDelegate { @MainActor private func isPendingRequestAvailable() async -> Bool { - let privateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let privateMode = privateBrowsingManager.isPrivateBrowsing // If we have an open `WalletStore`, use that so we can assign the pending request if the wallet is open, // which allows us to store the new `PendingRequest` triggering a modal presentation for that request. guard let cryptoStore = self.walletStore?.cryptoStore ?? CryptoStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: privateMode) else { @@ -2751,7 +2818,7 @@ extension BrowserViewController: SearchViewControllerDelegate { } func presentSearchSettingsController() { - let settingsNavigationController = SearchSettingsTableViewController(profile: profile) + let settingsNavigationController = SearchSettingsTableViewController(profile: profile, privateBrowsingManager: privateBrowsingManager) let navController = ModalSettingsNavigationController(rootViewController: settingsNavigationController) self.present(navController, animated: true, completion: nil) @@ -2860,7 +2927,7 @@ extension BrowserViewController: ToolbarUrlActionsDelegate { updateURLBarWalletButton() case .openInNewTab(let isPrivate): let tab = tabManager.addTab(PrivilegedRequest(url: url) as URLRequest, afterTab: tabManager.selectedTab, isPrivate: isPrivate) - if isPrivate && !PrivateBrowsingManager.shared.isPrivateBrowsing { + if isPrivate && !privateBrowsingManager.isPrivateBrowsing { tabManager.selectTab(tab) } else { // If we are showing toptabs a user can just use the top tab bar @@ -2905,7 +2972,7 @@ extension BrowserViewController: ToolbarUrlActionsDelegate { extension BrowserViewController: NewTabPageDelegate { func navigateToInput(_ input: String, inNewTab: Bool, switchingToPrivateMode: Bool) { - let isPrivate = PrivateBrowsingManager.shared.isPrivateBrowsing || switchingToPrivateMode + let isPrivate = privateBrowsingManager.isPrivateBrowsing || switchingToPrivateMode if inNewTab { tabManager.addTabAndSelect(isPrivate: isPrivate) } @@ -2983,7 +3050,7 @@ extension BrowserViewController: PreferencesObserver { case Preferences.General.tabBarVisibility.key: updateTabsBarVisibility() case Preferences.Privacy.privateBrowsingOnly.key: - PrivateBrowsingManager.shared.isPrivateBrowsing = Preferences.Privacy.privateBrowsingOnly.value + privateBrowsingManager.isPrivateBrowsing = Preferences.Privacy.privateBrowsingOnly.value setupTabs() updateTabsBarVisibility() updateApplicationShortcuts() @@ -3001,7 +3068,7 @@ extension BrowserViewController: PreferencesObserver { case Preferences.General.defaultPageZoomLevel.key: tabManager.allTabs.forEach({ guard let url = $0.webView?.url else { return } - let zoomLevel = PrivateBrowsingManager.shared.isPrivateBrowsing ? 1.0 : Domain.getPersistedDomain(for: url)?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value + let zoomLevel = $0.isPrivate ? 1.0 : Domain.getPersistedDomain(for: url)?.zoom_level?.doubleValue ?? Preferences.General.defaultPageZoomLevel.value $0.webView?.setValue(zoomLevel, forKey: PageZoomView.propertyName) }) case Preferences.Shields.httpsEverywhere.key: @@ -3051,7 +3118,7 @@ extension BrowserViewController: PreferencesObserver { state: selectedTab?.playlistItemState ?? .none, item: selectedTab?.playlistItem) case Preferences.PrivacyReports.captureShieldsData.key: - PrivacyReportsManager.scheduleProcessingBlockedRequests() + PrivacyReportsManager.scheduleProcessingBlockedRequests(isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) PrivacyReportsManager.scheduleNotification(debugMode: !AppConstants.buildChannel.isPublic) case Preferences.PrivacyReports.captureVPNAlerts.key: PrivacyReportsManager.scheduleVPNAlertsTask() @@ -3061,7 +3128,7 @@ extension BrowserViewController: PreferencesObserver { notificationsPresenter.removeNotification(with: WalletNotification.Constant.id) WalletProviderPermissionRequestsManager.shared.cancelAllPendingRequests(for: [.eth]) WalletProviderAccountCreationRequestManager.shared.cancelAllPendingRequests(coins: [.eth]) - let privateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let privateMode = privateBrowsingManager.isPrivateBrowsing if let cryptoStore = CryptoStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: privateMode) { cryptoStore.rejectAllPendingWebpageRequests() } @@ -3072,7 +3139,7 @@ extension BrowserViewController: PreferencesObserver { notificationsPresenter.removeNotification(with: WalletNotification.Constant.id) WalletProviderPermissionRequestsManager.shared.cancelAllPendingRequests(for: [.sol]) WalletProviderAccountCreationRequestManager.shared.cancelAllPendingRequests(coins: [.sol]) - let privateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let privateMode = privateBrowsingManager.isPrivateBrowsing if let cryptoStore = CryptoStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: privateMode) { cryptoStore.rejectAllPendingWebpageRequests() } @@ -3103,7 +3170,7 @@ extension BrowserViewController { extension BrowserViewController { func presentTabReceivedToast(url: URL) { // 'Tab Received' indicator will only be shown in normal browsing - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { let toast = ButtonToast( labelText: Strings.Callout.tabReceivedCalloutTitle, image: UIImage(braveSystemNamed: "leo.smartphone.tablet-portrait"), @@ -3173,11 +3240,11 @@ extension BrowserViewController: UIScreenshotServiceDelegate { // Privacy reports extension BrowserViewController { public func openPrivacyReport() { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if privateBrowsingManager.isPrivateBrowsing { return } - let host = UIHostingController(rootView: PrivacyReportsManager.prepareView()) + let host = UIHostingController(rootView: PrivacyReportsManager.prepareView(isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)) host.rootView.openPrivacyReportsUrl = { [weak self] in guard let self = self else { return } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift index 97ad2f85c4e..7aaf0e2c959 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+Rewards.swift @@ -25,7 +25,7 @@ extension BrowserViewController { self.topToolbar.locationView.rewardsButton.isHidden = true return } - self.topToolbar.locationView.rewardsButton.isHidden = Preferences.Rewards.hideRewardsIcon.value || PrivateBrowsingManager.shared.isPrivateBrowsing + self.topToolbar.locationView.rewardsButton.isHidden = Preferences.Rewards.hideRewardsIcon.value || privateBrowsingManager.isPrivateBrowsing self.topToolbar.locationView.rewardsButton.iconState = Preferences.Rewards.rewardsToggledOnce.value ? (rewards.isEnabled || rewards.isCreatingWallet ? .enabled : .disabled) : .initial } @@ -119,7 +119,7 @@ extension BrowserViewController { tabManager.selectTab(tab) } else { let request = URLRequest(url: url) - let isPrivate = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivate = privateBrowsingManager.isPrivateBrowsing tabManager.addTabAndSelect(request, isPrivate: isPrivate) } } @@ -185,7 +185,7 @@ extension BrowserViewController { extension Tab { func reportPageLoad(to rewards: BraveRewards, redirectionURLs urls: [URL]) { guard let webView = webView, let url = webView.url else { return } - if url.isLocal || PrivateBrowsingManager.shared.isPrivateBrowsing { return } + if url.isLocal || self.isPrivate { return } var htmlBlob: String? var classifierText: String? diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift index 9517232dbdd..41cae5be3fb 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift @@ -249,7 +249,7 @@ extension BrowserViewController { var linkReceiptView = VPNLinkReceiptView() linkReceiptView.linkReceiptAction = { - self.openURLInNewTab(.brave.braveVPNLinkReceiptProd, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, isPrivileged: false) + self.openURLInNewTab(.brave.braveVPNLinkReceiptProd, isPrivate: self.privateBrowsingManager.isPrivateBrowsing, isPrivileged: false) } let popup = PopupViewController(rootView: linkReceiptView, isDismissable: true) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift index 327f3ecde3c..37937a06d9e 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+KeyCommands.swift @@ -43,7 +43,7 @@ extension BrowserViewController { } @objc private func newTabKeyCommand() { - openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: privateBrowsingManager.isPrivateBrowsing) } @objc private func newPrivateTabKeyCommand() { @@ -211,7 +211,7 @@ extension BrowserViewController { UIKeyCommand(title: Strings.Hotkey.newPrivateTabTitle, action: #selector(newPrivateTabKeyCommand), input: "n", modifierFlags: [.command, .shift]), ] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { navigationCommands += [ UIKeyCommand(title: Strings.Hotkey.recentlyClosedTabTitle, action: #selector(reopenRecentlyClosedTabCommand), input: "t", modifierFlags: [.command, .shift]) ] diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift index 0d5b780ba4d..78bfd6c4deb 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Menu.swift @@ -31,7 +31,8 @@ extension BrowserViewController { }, displayAlert: { [unowned menuController] alert in menuController.present(alert, animated: true) }, openURL: { [weak self] url in - self?.openURLInNewTab(url, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, + guard let self = self else { return } + self.openURLInNewTab(url, isPrivate: self.privateBrowsingManager.isPrivateBrowsing, isPrivileged: false) }) @@ -69,7 +70,7 @@ extension BrowserViewController { self.present(alert, animated: true) }, openURL: { [unowned self] url in self.openURLInNewTab(url, - isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, + isPrivate: self.privateBrowsingManager.isPrivateBrowsing, isPrivileged: false) } ) @@ -89,7 +90,7 @@ extension BrowserViewController { } // Add Brave Talk and News options only in normal browsing - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { // Show Brave News if it is first launch and after first launch If the new is enabled if Preferences.General.isFirstLaunch.value || (!Preferences.General.isFirstLaunch.value && Preferences.BraveNews.isEnabled.value) { MenuItemFactory.button(for: .news) { [weak self] in @@ -124,13 +125,13 @@ extension BrowserViewController { let vc = BookmarksViewController( folder: bookmarkManager.lastVisitedFolder(), bookmarkManager: bookmarkManager, - isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing) + isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) vc.toolbarUrlActionsDelegate = self menuController.presentInnerMenu(vc) } MenuItemFactory.button(for: .history) { [unowned self, unowned menuController] in let vc = HistoryViewController( - isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing, + isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing, historyAPI: self.braveCore.historyAPI, tabManager: self.tabManager) vc.toolbarUrlActionsDelegate = self @@ -154,7 +155,7 @@ extension BrowserViewController { } } MenuItemFactory.button(for: .settings) { [unowned self, unowned menuController] in - let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateMode = privateBrowsingManager.isPrivateBrowsing let keyringService = BraveWallet.KeyringServiceFactory.get(privateMode: isPrivateMode) let walletService = BraveWallet.ServiceFactory.get(privateMode: isPrivateMode) let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode) @@ -200,11 +201,23 @@ extension BrowserViewController { } private func presentPlaylistController() { + if PlaylistCarplayManager.shared.isPlaylistControllerPresented { + let alert = UIAlertController(title: Strings.PlayList.playlistAlreadyShowingTitle, + message: Strings.PlayList.playlistAlreadyShowingBody, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.OKString, style: .default)) + dismiss(animated: true) { + self.present(alert, animated: true) + } + return + } + // Present existing playlist controller if let playlistController = PlaylistCarplayManager.shared.playlistController { PlaylistP3A.recordUsage() dismiss(animated: true) { + PlaylistCarplayManager.shared.isPlaylistControllerPresented = true self.present(playlistController, animated: true) } } else { @@ -217,6 +230,7 @@ extension BrowserViewController { PlaylistP3A.recordUsage() self.dismiss(animated: true) { + PlaylistCarplayManager.shared.isPlaylistControllerPresented = true self.present(playlistController, animated: true) } } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift index 163b3487dec..0ddd471629f 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Onboarding.swift @@ -49,7 +49,7 @@ extension BrowserViewController { tabManager.addTab( PrivilegedRequest(url: url) as URLRequest, afterTab: self.tabManager.selectedTab, - isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + isPrivate: privateBrowsingManager.isPrivateBrowsing) } } @@ -105,7 +105,7 @@ extension BrowserViewController { } private func showPrivacyReportsOnboardingIfNeeded() { - if Preferences.PrivacyReports.ntpOnboardingCompleted.value || PrivateBrowsingManager.shared.isPrivateBrowsing { + if Preferences.PrivacyReports.ntpOnboardingCompleted.value || privateBrowsingManager.isPrivateBrowsing { return } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift index 64d7dd132fa..94c84346d2a 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+TabManagerDelegate.swift @@ -167,7 +167,7 @@ extension BrowserViewController: TabManagerDelegate { topToolbar.leaveOverlayMode(didCancel: true) updateTabsBarVisibility() - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { rewards.reportTabClosed(tabId: Int(tab.rewardsId)) } } @@ -200,7 +200,7 @@ extension BrowserViewController: TabManagerDelegate { } func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { - guard let toast = toast, !PrivateBrowsingManager.shared.isPrivateBrowsing else { + guard let toast = toast, !privateBrowsingManager.isPrivateBrowsing else { return } show(toast: toast, afterWaiting: ButtonToastUX.toastDelay) @@ -216,7 +216,7 @@ extension BrowserViewController: TabManagerDelegate { var newTabMenuChildren: [UIAction] = [] var addTabMenuChildren: [UIAction] = [] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { let openNewPrivateTab = UIAction( title: Strings.Hotkey.newPrivateTabTitle, image: UIImage(systemName: "plus.square.fill.on.square.fill"), @@ -232,10 +232,10 @@ extension BrowserViewController: TabManagerDelegate { } let openNewTab = UIAction( - title: PrivateBrowsingManager.shared.isPrivateBrowsing ? Strings.Hotkey.newPrivateTabTitle : Strings.Hotkey.newTabTitle, - image: PrivateBrowsingManager.shared.isPrivateBrowsing ? UIImage(systemName: "plus.square.fill.on.square.fill") : UIImage(systemName: "plus.square.on.square"), + title: privateBrowsingManager.isPrivateBrowsing ? Strings.Hotkey.newPrivateTabTitle : Strings.Hotkey.newTabTitle, + image: privateBrowsingManager.isPrivateBrowsing ? UIImage(systemName: "plus.square.fill.on.square.fill") : UIImage(systemName: "plus.square.on.square"), handler: UIAction.deferredActionHandler { [unowned self] _ in - self.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: privateBrowsingManager.isPrivateBrowsing) }) if (UIDevice.current.userInterfaceIdiom == .pad && tabsBar.view.isHidden) || (UIDevice.current.userInterfaceIdiom == .phone && toolbar == nil) { @@ -264,7 +264,7 @@ extension BrowserViewController: TabManagerDelegate { image: UIImage(systemName: "book"), handler: UIAction.deferredActionHandler { [unowned self] _ in let mode = BookmarkEditMode.addFolderUsingTabs(title: Strings.savedTabsFolderTitle, tabList: tabManager.tabsForCurrentMode) - let addBookMarkController = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode) + let addBookMarkController = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) presentSettingsNavigation(with: addBookMarkController, cancelEnabled: true) }) @@ -294,14 +294,14 @@ extension BrowserViewController: TabManagerDelegate { var recentlyClosedMenuChildren: [UIAction] = [] // Recently Closed Actions are only in normal mode - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { let viewRecentlyClosedTabs = UIAction( title: Strings.RecentlyClosed.viewRecentlyClosedTab, image: UIImage(braveSystemNamed: "leo.browser.mobile-recent-tabs"), handler: UIAction.deferredActionHandler { [weak self] _ in guard let self = self else { return } - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if privateBrowsingManager.isPrivateBrowsing { return } @@ -326,7 +326,7 @@ extension BrowserViewController: TabManagerDelegate { handler: UIAction.deferredActionHandler { [weak self] _ in guard let self = self else { return } - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if privateBrowsingManager.isPrivateBrowsing { return } @@ -369,7 +369,7 @@ extension BrowserViewController: TabManagerDelegate { let cancelAction = UIAlertAction(title: Strings.CancelString, style: .cancel) let closedTabsTitle = String(format: Strings.closeAllTabsTitle, tabManager.tabsForCurrentMode.count) let closeAllAction = UIAlertAction(title: closedTabsTitle, style: .destructive) { _ in - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { // Add the tab information to recently closed before removing tabManager.addAllTabsToRecentlyClosed() } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift index 6752ff0f2f6..806c9c0ffbe 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift @@ -260,7 +260,8 @@ extension BrowserViewController: TopToolbarDelegate { if let url = searchURL, InternalURL.isValid(url: url) { searchURL = url } - if let query = profile.searchEngines.queryForSearchURL(searchURL as URL?) { + if let query = profile.searchEngines.queryForSearchURL(searchURL as URL?, + forType: privateBrowsingManager.isPrivateBrowsing ? .privateMode : .standard) { return (query, true) } else { return (topToolbar?.absoluteString, false) @@ -304,7 +305,7 @@ extension BrowserViewController: TopToolbarDelegate { // We couldn't build a URL, so pass it on to the search engine. submitSearchText(text, isBraveSearchPromotion: isBraveSearchPromotion) - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { RecentSearch.addItem(type: .text, text: text, websiteUrl: nil) } } @@ -313,7 +314,7 @@ extension BrowserViewController: TopToolbarDelegate { @discardableResult func handleIPFSSchemeURL(_ url: URL, visitType: VisitType) -> Bool { - guard !PrivateBrowsingManager.shared.isPrivateBrowsing else { + guard !privateBrowsingManager.isPrivateBrowsing else { topToolbar.leaveOverlayMode() if let errorPageHelper = tabManager.selectedTab?.getContentScript(name: ErrorPageHelper.scriptName) as? ErrorPageHelper, let webView = tabManager.selectedTab?.webView { errorPageHelper.loadPage(IPFSErrorPageHandler.privateModeError, forUrl: url, inWebView: webView) @@ -386,7 +387,7 @@ extension BrowserViewController: TopToolbarDelegate { } func submitSearchText(_ text: String, isBraveSearchPromotion: Bool = false) { - var engine = profile.searchEngines.defaultEngine() + var engine = profile.searchEngines.defaultEngine(forType: privateBrowsingManager.isPrivateBrowsing ? .privateMode : .standard) if isBraveSearchPromotion { let braveSearchEngine = profile.searchEngines.orderedEngines.first { @@ -448,12 +449,21 @@ extension BrowserViewController: TopToolbarDelegate { let shields = ShieldsViewController(tab: selectedTab) shields.shieldsSettingsChanged = { [unowned self] _, shield in - // Update the shields status immediately - self.topToolbar.refreshShieldsStatus() - - // Reload this tab. This will also trigger an update of the brave icon in `TabLocationView` if - // the setting changed is the global `.AllOff` shield - self.tabManager.selectedTab?.reload() + let currentDomain = self.tabManager.selectedTab?.url?.baseDomain + let browsers = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).compactMap({ $0.browserViewController }) + + browsers.forEach { browser in + // Update the shields status immediately + browser.topToolbar.refreshShieldsStatus() + + // Reload the tabs. This will also trigger an update of the brave icon in `TabLocationView` if + // the setting changed is the global `.AllOff` shield + browser.tabManager.allTabs.forEach { + if $0.url?.baseDomain == currentDomain { + $0.reload() + } + } + } // Record P3A shield changes DispatchQueue.main.asyncAfter(deadline: .now() + 1) { @@ -790,7 +800,7 @@ extension BrowserViewController: TopToolbarDelegate { let mode = BookmarkEditMode.addBookmark(title: selectedTab.displayTitle, url: bookmarkUrl.absoluteString) - let addBookMarkController = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode) + let addBookMarkController = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) presentSettingsNavigation(with: addBookMarkController, cancelEnabled: true) } @@ -881,7 +891,7 @@ extension BrowserViewController: ToolbarDelegate { } func tabToolbarDidPressAddTab(_ tabToolbar: ToolbarProtocol, button: UIButton) { - self.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: privateBrowsingManager.isPrivateBrowsing) } func tabToolbarDidLongPressForward(_ tabToolbar: ToolbarProtocol, button: UIButton) { diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift index f1abd6c6c8b..870e18a5972 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -241,7 +241,7 @@ extension BrowserViewController: WKNavigationDelegate { } } - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing // Website redirection logic if url.isWebPage(includeDataURIs: false), @@ -446,7 +446,7 @@ extension BrowserViewController: WKNavigationDelegate { @MainActor public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing let response = navigationResponse.response let responseURL = response.url let tab = tab(for: webView) @@ -693,7 +693,7 @@ extension BrowserViewController: WKNavigationDelegate { tab: tab, url: url, isSelected: tabManager.selectedTab == tab, - isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing + isPrivate: privateBrowsingManager.isPrivateBrowsing ) } @@ -1002,7 +1002,6 @@ extension BrowserViewController: WKUIDelegate { } openNewTabAction.accessibilityLabel = "linkContextMenu.openInNewTab" - actions.append(openNewTabAction) } @@ -1013,8 +1012,31 @@ extension BrowserViewController: WKUIDelegate { self.addTab(url: url, inPrivateMode: true, currentTab: currentTab) } openNewPrivateTabAction.accessibilityLabel = "linkContextMenu.openInNewPrivateTab" - actions.append(openNewPrivateTabAction) + + if UIApplication.shared.supportsMultipleScenes { + if !tabType.isPrivate { + let openNewWindowAction = UIAction( + title: Strings.openInNewWindowTitle, + image: UIImage(braveSystemNamed: "leo.window") + ) { _ in + self.openInNewWindow(url: url, isPrivate: false) + } + + openNewWindowAction.accessibilityLabel = "linkContextMenu.openInNewWindow" + actions.append(openNewWindowAction) + } + + let openNewPrivateWindowAction = UIAction( + title: Strings.openInNewPrivateWindowTitle, + image: UIImage(braveSystemNamed: "leo.window.tab-private") + ) { _ in + self.openInNewWindow(url: url, isPrivate: true) + } + + openNewPrivateWindowAction.accessibilityLabel = "linkContextMenu.openInNewPrivateWindow" + actions.append(openNewPrivateWindowAction) + } let copyAction = UIAction( title: Strings.copyLinkActionTitle, @@ -1091,7 +1113,7 @@ extension BrowserViewController: WKUIDelegate { fileprivate func addTab(url: URL, inPrivateMode: Bool, currentTab: Tab) { let tab = self.tabManager.addTab(URLRequest(url: url), afterTab: currentTab, isPrivate: inPrivateMode) - if inPrivateMode && !PrivateBrowsingManager.shared.isPrivateBrowsing { + if inPrivateMode && !privateBrowsingManager.isPrivateBrowsing { self.tabManager.selectTab(tab) } else { // We're not showing the top tabs; show a toast to quick switch to the fresh new tab. diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift index 2a908d7898a..cb953179257 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Wallet.swift @@ -80,7 +80,7 @@ extension BrowserViewController { /// Initializes a new WalletStore for displaying the wallet, setting up an observer to notify /// when the pending request is updated so we can update the wallet url bar button. func newWalletStore() -> WalletStore? { - let privateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let privateMode = privateBrowsingManager.isPrivateBrowsing guard let walletStore = WalletStore.from(ipfsApi: braveCore.ipfsAPI, privateMode: privateMode) else { Logger.module.error("Failed to load wallet. One or more services were unavailable") return nil @@ -135,7 +135,7 @@ extension BrowserViewController: BraveWalletDelegate { } else { _ = tabManager.addTabAndSelect( URLRequest(url: destinationURL), - isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing + isPrivate: privateBrowsingManager.isPrivateBrowsing ) } } @@ -194,7 +194,7 @@ extension Tab: BraveWalletProviderDelegate { return } - let isPrivate = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivate = self.isPrivate // Check if eth permissions already exist for this origin and if they don't, ensure the user allows // ethereum/solana provider access @@ -338,7 +338,7 @@ extension Tab: BraveWalletProviderDelegate { } func showAccountCreation(_ coin: BraveWallet.CoinType) { - let privateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let privateMode = self.isPrivate guard let keyringService = BraveWallet.KeyringServiceFactory.get(privateMode: privateMode) else { return } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift index e199d8266f5..ceaf7452087 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Web3NameService.swift @@ -11,7 +11,7 @@ import BraveCore extension BrowserViewController: Web3NameServiceScriptHandlerDelegate { /// Returns a `DecentralizedDNSHelper` for the given mode if supported and not in private mode. func decentralizedDNSHelperFor(url: URL?) -> DecentralizedDNSHelper? { - let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateMode = privateBrowsingManager.isPrivateBrowsing guard !isPrivateMode, let url, DecentralizedDNSHelper.isSupported(domain: url.domainURL.schemelessAbsoluteDisplayString), @@ -24,7 +24,7 @@ extension BrowserViewController: Web3NameServiceScriptHandlerDelegate { } func web3NameServiceDecisionHandler(_ proceed: Bool, web3Service: Web3Service, originalURL: URL, visitType: VisitType) { - let isPrivateMode = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateMode = privateBrowsingManager.isPrivateBrowsing guard let rpcService = BraveWallet.JsonRpcServiceFactory.get(privateMode: isPrivateMode), let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: originalURL) else { finishEditingAndSubmit(originalURL, visitType: visitType) diff --git a/Sources/Brave/Frontend/Browser/Favicons/LargeFaviconView.swift b/Sources/Brave/Frontend/Browser/Favicons/LargeFaviconView.swift index 07423220ee3..452f58685de 100644 --- a/Sources/Brave/Frontend/Browser/Favicons/LargeFaviconView.swift +++ b/Sources/Brave/Frontend/Browser/Favicons/LargeFaviconView.swift @@ -16,7 +16,7 @@ struct FaviconUX { /// Displays a large favicon given some favorite class LargeFaviconView: UIView { - func loadFavicon(siteURL: URL, monogramFallbackCharacter: Character? = nil) { + func loadFavicon(siteURL: URL, isPrivateBrowsing: Bool, monogramFallbackCharacter: Character? = nil) { faviconTask?.cancel() if let favicon = FaviconFetcher.getIconFromCache(for: siteURL) { faviconTask = nil @@ -34,7 +34,7 @@ class LargeFaviconView: UIView { } faviconTask = Task { @MainActor in - let isPersistent = !PrivateBrowsingManager.shared.isPrivateBrowsing + let isPersistent = !isPrivateBrowsing do { let favicon = try await FaviconFetcher.loadIcon(url: siteURL, kind: .largeIcon, diff --git a/Sources/Brave/Frontend/Browser/Favicons/UIImageView+Favicon.swift b/Sources/Brave/Frontend/Browser/Favicons/UIImageView+Favicon.swift index f960640a926..777340447de 100644 --- a/Sources/Brave/Frontend/Browser/Favicons/UIImageView+Favicon.swift +++ b/Sources/Brave/Frontend/Browser/Favicons/UIImageView+Favicon.swift @@ -28,7 +28,7 @@ extension UIImageView { /// Load the favicon from a site URL directly into a `UIImageView`. If no /// favicon is found, a monogram will be used where the letter is determined /// based on the site URL. - func loadFavicon(for siteURL: URL, monogramFallbackCharacter: Character? = nil, completion: ((Favicon?) -> Void)? = nil) { + func loadFavicon(for siteURL: URL, isPrivateBrowsing: Bool, monogramFallbackCharacter: Character? = nil, completion: ((Favicon?) -> Void)? = nil) { cancelFaviconLoad() if let icon = FaviconFetcher.getIconFromCache(for: siteURL) { @@ -39,7 +39,7 @@ extension UIImageView { self.image = Favicon.defaultImage faviconTask = Task { @MainActor in do { - let favicon = try await FaviconFetcher.loadIcon(url: siteURL, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let favicon = try await FaviconFetcher.loadIcon(url: siteURL, persistent: !isPrivateBrowsing) self.image = favicon.image ?? Favicon.defaultImage completion?(favicon) } catch { diff --git a/Sources/Brave/Frontend/Browser/Favorites/FavoritesViewController.swift b/Sources/Brave/Frontend/Browser/Favorites/FavoritesViewController.swift index af957c4525a..bae22c84e12 100644 --- a/Sources/Brave/Frontend/Browser/Favorites/FavoritesViewController.swift +++ b/Sources/Brave/Frontend/Browser/Favorites/FavoritesViewController.swift @@ -94,11 +94,13 @@ class FavoritesViewController: UIViewController { $0.contentView.backgroundColor = UIColor.braveBackground.withAlphaComponent(0.5) } private var hasPasteboardURL = false + private var isPrivateBrowsing: Bool init(tabType: TabType, action: @escaping (Favorite, BookmarksAction) -> Void, recentSearchAction: @escaping (RecentSearch?, Bool) -> Void) { self.tabType = tabType self.action = action self.recentSearchAction = recentSearchAction + self.isPrivateBrowsing = tabType.isPrivate collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) super.init(nibName: nil, bundle: nil) @@ -352,7 +354,7 @@ extension FavoritesViewController: UICollectionViewDelegateFlowLayout { }) var urlChildren: [UIAction] = [openInNewTab] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !self.isPrivateBrowsing { let openInNewPrivateTab = UIAction( title: Strings.openNewPrivateTabButtonTitle, handler: UIAction.deferredActionHandler { _ in @@ -668,7 +670,7 @@ extension FavoritesViewController: NSFetchedResultsControllerDelegate { cell.textLabel.text = favorite.displayTitle ?? favorite.url if let url = favorite.url?.asURL { - cell.imageView.loadFavicon(siteURL: url) + cell.imageView.loadFavicon(siteURL: url, isPrivateBrowsing: self.isPrivateBrowsing) } cell.accessibilityLabel = cell.textLabel.text diff --git a/Sources/Brave/Frontend/Browser/FrequencyQuery.swift b/Sources/Brave/Frontend/Browser/FrequencyQuery.swift index 66102bd4c41..024aa17e974 100644 --- a/Sources/Brave/Frontend/Browser/FrequencyQuery.swift +++ b/Sources/Brave/Frontend/Browser/FrequencyQuery.swift @@ -79,7 +79,7 @@ class FrequencyQuery { var tabList = [Site]() for tab in tabs { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if tab.isPrivate { if let url = tab.url, url.isWebPage(), !(InternalURL(url)?.isAboutHomeURL ?? false) { if let selectedTabID = tabManager.selectedTab?.id, selectedTabID == tab.id { continue diff --git a/Sources/Brave/Frontend/Browser/Helpers/BrowserNavigationHelper.swift b/Sources/Brave/Frontend/Browser/Helpers/BrowserNavigationHelper.swift index c4a49aca3cd..8d8e4e9c9ca 100644 --- a/Sources/Brave/Frontend/Browser/Helpers/BrowserNavigationHelper.swift +++ b/Sources/Brave/Frontend/Browser/Helpers/BrowserNavigationHelper.swift @@ -45,7 +45,7 @@ class BrowserNavigationHelper { let vc = BookmarksViewController( folder: bvc.bookmarkManager.lastVisitedFolder(), bookmarkManager: bvc.bookmarkManager, - isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing) + isPrivateBrowsing: bvc.privateBrowsingManager.isPrivateBrowsing) vc.toolbarUrlActionsDelegate = bvc open(vc, doneButton: DoneButton(style: .done, position: .right)) @@ -58,7 +58,7 @@ class BrowserNavigationHelper { func openHistory() { guard let bvc = bvc else { return } let vc = HistoryViewController( - isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing, + isPrivateBrowsing: bvc.privateBrowsingManager.isPrivateBrowsing, historyAPI: bvc.braveCore.historyAPI, tabManager: bvc.tabManager) vc.toolbarUrlActionsDelegate = bvc diff --git a/Sources/Brave/Frontend/Browser/Helpers/FaviconImage.swift b/Sources/Brave/Frontend/Browser/Helpers/FaviconImage.swift index 7b842d320a7..1ac8a31584d 100644 --- a/Sources/Brave/Frontend/Browser/Helpers/FaviconImage.swift +++ b/Sources/Brave/Frontend/Browser/Helpers/FaviconImage.swift @@ -14,10 +14,10 @@ private class FaviconHelper: ObservableObject { @Published var image: UIImage? private var faviconTask: Task? - func load(url: URL) { + func load(url: URL, isPrivateBrowsing: Bool) { faviconTask?.cancel() faviconTask = Task { @MainActor in - let favicon = try await FaviconFetcher.loadIcon(url: url, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let favicon = try await FaviconFetcher.loadIcon(url: url, persistent: !isPrivateBrowsing) self.image = favicon.image } } @@ -25,9 +25,12 @@ private class FaviconHelper: ObservableObject { struct FaviconImage: View { let url: URL? + private let isPrivateBrowsing: Bool @StateObject private var faviconLoader = FaviconHelper() - init(url: String?) { + init(url: String?, isPrivateBrowsing: Bool) { + self.isPrivateBrowsing = isPrivateBrowsing + if let url = url { self.url = URL(string: url) } else { @@ -43,7 +46,7 @@ struct FaviconImage: View { .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .onAppear { if let url = url { - faviconLoader.load(url: url) + faviconLoader.load(url: url, isPrivateBrowsing: isPrivateBrowsing) } } } diff --git a/Sources/Brave/Frontend/Browser/HomePanel/NTPDataSource.swift b/Sources/Brave/Frontend/Browser/HomePanel/NTPDataSource.swift index a0415b3d44a..c8cc2b2bee9 100644 --- a/Sources/Brave/Frontend/Browser/HomePanel/NTPDataSource.swift +++ b/Sources/Brave/Frontend/Browser/HomePanel/NTPDataSource.swift @@ -52,6 +52,8 @@ enum NTPWallpaper { } public class NTPDataSource { + + private(set) var privateBrowsingManager: PrivateBrowsingManager var initializeFavorites: ((_ sites: [NTPSponsoredImageTopSite]?) -> Void)? @@ -85,8 +87,9 @@ public class NTPDataSource { let service: NTPBackgroundImagesService - public init(service: NTPBackgroundImagesService) { + public init(service: NTPBackgroundImagesService, privateBrowsingManager: PrivateBrowsingManager) { self.service = service + self.privateBrowsingManager = privateBrowsingManager Preferences.NewTabPage.selectedCustomTheme.observe(from: self) @@ -131,7 +134,7 @@ public class NTPDataSource { let attemptSponsored = Preferences.NewTabPage.backgroundSponsoredImages.value && backgroundRotationCounter == NTPDataSource.sponsorshipShowValue - && !PrivateBrowsingManager.shared.isPrivateBrowsing + && !privateBrowsingManager.isPrivateBrowsing if attemptSponsored { // Pick the campaign randomly diff --git a/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift b/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift index 4292226662b..aaa1afb1798 100644 --- a/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift +++ b/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift @@ -45,7 +45,7 @@ class LinkPreviewViewController: UIViewController { } // Add rule lists for this page - let domain = Domain.getOrCreate(forUrl: url, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let domain = Domain.getOrCreate(forUrl: url, persistent: !currentTab.isPrivate) Task(priority: .userInitiated) { let ruleLists = await ContentBlockerManager.shared.ruleLists(for: domain) diff --git a/Sources/Brave/Frontend/Browser/NavigationRouter.swift b/Sources/Brave/Frontend/Browser/NavigationRouter.swift index 37e8834dbf9..4ea3edc0f1a 100644 --- a/Sources/Brave/Frontend/Browser/NavigationRouter.swift +++ b/Sources/Brave/Frontend/Browser/NavigationRouter.swift @@ -21,10 +21,10 @@ public enum NavigationPath: Equatable { case text(String) case widgetShortcutURL(WidgetShortcut) - public init?(url: URL) { + public init?(url: URL, isPrivateBrowsing: Bool) { let urlString = url.absoluteString if url.scheme == "http" || url.scheme == "https" || url.isIPFSScheme { - self = .url(webURL: url, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + self = .url(webURL: url, isPrivate: isPrivateBrowsing) return } @@ -48,7 +48,7 @@ public enum NavigationPath: Equatable { } else if urlString.starts(with: "\(scheme)://open-url") { let urlText = components.valueForQuery("url") let url = URIFixup.getURL(urlText ?? "") ?? urlText?.asURL - let forcedPrivate = Preferences.Privacy.privateBrowsingOnly.value || PrivateBrowsingManager.shared.isPrivateBrowsing + let forcedPrivate = Preferences.Privacy.privateBrowsingOnly.value || isPrivateBrowsing let isPrivate = Bool(components.valueForQuery("private") ?? "") ?? forcedPrivate self = .url(webURL: url, isPrivate: isPrivate) } else if urlString.starts(with: "\(scheme)://open-text") { @@ -95,7 +95,7 @@ public enum NavigationPath: Equatable { private static func handleText(text: String, with bvc: BrowserViewController) { bvc.openBlankNewTab( attemptLocationFieldFocus: true, - isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, + isPrivate: bvc.privateBrowsingManager.isPrivateBrowsing, searchFor: text) } @@ -106,10 +106,10 @@ public enum NavigationPath: Equatable { if let url = bvc.tabManager.selectedTab?.url, InternalURL(url)?.isAboutHomeURL == true { bvc.focusURLBar() } else { - bvc.openBlankNewTab(attemptLocationFieldFocus: true, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + bvc.openBlankNewTab(attemptLocationFieldFocus: true, isPrivate: bvc.privateBrowsingManager.isPrivateBrowsing) } case .newTab: - bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: bvc.privateBrowsingManager.isPrivateBrowsing) case .newPrivateTab: bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: true) case .bookmarks: diff --git a/Sources/Brave/Frontend/Browser/New Tab Page/Backgrounds/NewTabPageBackgroundButtonsView.swift b/Sources/Brave/Frontend/Browser/New Tab Page/Backgrounds/NewTabPageBackgroundButtonsView.swift index b4f25a79e33..16767bca7f1 100644 --- a/Sources/Brave/Frontend/Browser/New Tab Page/Backgrounds/NewTabPageBackgroundButtonsView.swift +++ b/Sources/Brave/Frontend/Browser/New Tab Page/Backgrounds/NewTabPageBackgroundButtonsView.swift @@ -77,9 +77,12 @@ class NewTabPageBackgroundButtonsView: UIView, PreferencesObserver { } private var safeAreaInsetsConstraint: Constraint? private let collectionViewSafeAreaLayoutGuide = UILayoutGuide() + private let privateBrowsingManager: PrivateBrowsingManager - override init(frame: CGRect) { - super.init(frame: frame) + init(privateBrowsingManager: PrivateBrowsingManager) { + self.privateBrowsingManager = privateBrowsingManager + + super.init(frame: .zero) Preferences.BraveNews.isEnabled.observe(from: self) @@ -106,7 +109,7 @@ class NewTabPageBackgroundButtonsView: UIView, PreferencesObserver { let isLandscape = frame.width > frame.height let braveNewsVisible = - !PrivateBrowsingManager.shared.isPrivateBrowsing && (Preferences.BraveNews.isEnabled.value || Preferences.BraveNews.isShowingOptIn.value) + !privateBrowsingManager.isPrivateBrowsing && (Preferences.BraveNews.isEnabled.value || Preferences.BraveNews.isShowingOptIn.value) imageCreditButton.snp.remakeConstraints { $0.leading.equalTo(collectionViewSafeAreaLayoutGuide).inset(16) diff --git a/Sources/Brave/Frontend/Browser/New Tab Page/NewTabPageViewController.swift b/Sources/Brave/Frontend/Browser/New Tab Page/NewTabPageViewController.swift index 949b62bb17f..fcb571ea6ce 100644 --- a/Sources/Brave/Frontend/Browser/New Tab Page/NewTabPageViewController.swift +++ b/Sources/Brave/Frontend/Browser/New Tab Page/NewTabPageViewController.swift @@ -107,7 +107,7 @@ class NewTabPageViewController: UIViewController { private var background: NewTabPageBackground private let backgroundView = NewTabPageBackgroundView() - private let backgroundButtonsView = NewTabPageBackgroundButtonsView() + private let backgroundButtonsView: NewTabPageBackgroundButtonsView /// A gradient to display over background images to ensure visibility of /// the NTP contents and sponsored logo /// @@ -129,17 +129,21 @@ class NewTabPageViewController: UIViewController { private let notifications: NewTabPageNotifications private var cancellables: Set = [] + private let privateBrowsingManager: PrivateBrowsingManager init( tab: Tab, profile: Profile, dataSource: NTPDataSource, feedDataSource: FeedDataSource, - rewards: BraveRewards + rewards: BraveRewards, + privateBrowsingManager: PrivateBrowsingManager ) { self.tab = tab self.rewards = rewards self.feedDataSource = feedDataSource + self.privateBrowsingManager = privateBrowsingManager + self.backgroundButtonsView = NewTabPageBackgroundButtonsView(privateBrowsingManager: privateBrowsingManager) background = NewTabPageBackground(dataSource: dataSource) notifications = NewTabPageNotifications(rewards: rewards) collectionView = NewTabCollectionView(frame: .zero, collectionViewLayout: layout) @@ -149,12 +153,12 @@ class NewTabPageViewController: UIViewController { Preferences.NewTabPage.showNewTabFavourites.observe(from: self) sections = [ - StatsSectionProvider(openPrivacyHubPressed: { [weak self] in - if PrivateBrowsingManager.shared.isPrivateBrowsing { + StatsSectionProvider(isPrivateBrowsing: tab.isPrivate, openPrivacyHubPressed: { [weak self] in + if self?.privateBrowsingManager.isPrivateBrowsing == true { return } - let host = UIHostingController(rootView: PrivacyReportsManager.prepareView()) + let host = UIHostingController(rootView: PrivacyReportsManager.prepareView(isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)) host.rootView.onDismiss = { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { guard let self = self else { return } @@ -183,7 +187,7 @@ class NewTabPageViewController: UIViewController { self?.handleFavoriteAction(favorite: bookmark, action: action) }, legacyLongPressAction: { [weak self] alertController in self?.present(alertController, animated: true) - }), + }, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing), FavoritesOverflowSectionProvider(action: { [weak self] in self?.delegate?.focusURLBar() }), @@ -194,7 +198,7 @@ class NewTabPageViewController: UIViewController { sections.insert(NTPDefaultBrowserCalloutProvider(), at: 0) } - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { sections.append( BraveNewsSectionProvider( dataSource: feedDataSource, @@ -467,7 +471,7 @@ class NewTabPageViewController: UIViewController { } private func presentNotification() { - if PrivateBrowsingManager.shared.isPrivateBrowsing || notificationShowing { + if privateBrowsingManager.isPrivateBrowsing || notificationShowing { return } @@ -882,7 +886,7 @@ extension NewTabPageViewController: PreferencesObserver { // MARK: - UIScrollViewDelegate extension NewTabPageViewController { var isBraveNewsVisible: Bool { - return !PrivateBrowsingManager.shared.isPrivateBrowsing && (Preferences.BraveNews.isEnabled.value || Preferences.BraveNews.isShowingOptIn.value) + return !privateBrowsingManager.isPrivateBrowsing && (Preferences.BraveNews.isEnabled.value || Preferences.BraveNews.isShowingOptIn.value) } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/BraveNewsSectionProvider.swift b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/BraveNewsSectionProvider.swift index fa5263d7266..b0a09dc15ca 100644 --- a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/BraveNewsSectionProvider.swift +++ b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/BraveNewsSectionProvider.swift @@ -429,14 +429,9 @@ class BraveNewsSectionProvider: NSObject, NTPObservableSectionProvider { var openInNewPrivateTab: UIAction { .init(title: Strings.openNewPrivateTabButtonTitle, image: UIImage(braveSystemNamed: "leo.product.private-window"), handler: mapDeferredHandler(openInNewPrivateTabHandler)) } - let openActions: [UIAction] = [ - openInNewTab, - // Brave News is only available in normal tabs, so this isn't technically required - // but good to be on the safe side - !PrivateBrowsingManager.shared.isPrivateBrowsing ? openInNewPrivateTab : nil, - ].compactMap { $0 } + let children: [UIMenu] = [ - UIMenu(title: "", options: [.displayInline], children: openActions) + UIMenu(title: "", options: [.displayInline], children: [openInNewTab, openInNewPrivateTab]) ] return UIMenu(title: ad.targetURL, children: children) } @@ -486,19 +481,12 @@ class BraveNewsSectionProvider: NSObject, NTPObservableSectionProvider { .init(title: String(format: Strings.BraveNews.enablePublisherContent, item.source.name), image: UIImage(braveSystemNamed: "leo.eye.on"), handler: mapDeferredHandler(toggleSourceHandler)) } - let openActions: [UIAction] = [ - openInNewTab, - // Brave News is only available in normal tabs, so this isn't technically required - // but good to be on the safe side - !PrivateBrowsingManager.shared.isPrivateBrowsing ? openInNewPrivateTab : nil, - ].compactMap({ $0 }) - let manageActions = [ self.dataSource.isSourceHidden(item.source) ? enableSource : disableSource ] var children: [UIMenu] = [ - UIMenu(title: "", options: [.displayInline], children: openActions) + UIMenu(title: "", options: [.displayInline], children: [openInNewTab, openInNewPrivateTab]) ] if context.item.content.contentType != .sponsor { children.append(UIMenu(title: "", options: [.displayInline], children: manageActions)) diff --git a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/FavoritesSectionProvider.swift b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/FavoritesSectionProvider.swift index 5de238af39c..b61d180166f 100644 --- a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/FavoritesSectionProvider.swift +++ b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/FavoritesSectionProvider.swift @@ -21,6 +21,8 @@ class FavoritesSectionProvider: NSObject, NTPObservableSectionProvider { var sectionDidChange: (() -> Void)? var action: (Favorite, BookmarksAction) -> Void var legacyLongPressAction: (UIAlertController) -> Void + + private let isPrivateBrowsing: Bool var hasMoreThanOneFavouriteItems: Bool { frc.fetchedObjects?.count ?? 0 > 0 @@ -30,10 +32,12 @@ class FavoritesSectionProvider: NSObject, NTPObservableSectionProvider { init( action: @escaping (Favorite, BookmarksAction) -> Void, - legacyLongPressAction: @escaping (UIAlertController) -> Void + legacyLongPressAction: @escaping (UIAlertController) -> Void, + isPrivateBrowsing: Bool ) { self.action = action self.legacyLongPressAction = legacyLongPressAction + self.isPrivateBrowsing = isPrivateBrowsing frc = Favorite.frc() super.init() @@ -110,7 +114,7 @@ class FavoritesSectionProvider: NSObject, NTPObservableSectionProvider { cell.imageView.cancelLoading() if let url = fav.url?.asURL { - cell.imageView.loadFavicon(siteURL: url) + cell.imageView.loadFavicon(siteURL: url, isPrivateBrowsing: isPrivateBrowsing) } cell.accessibilityLabel = cell.textLabel.text } @@ -174,7 +178,7 @@ class FavoritesSectionProvider: NSObject, NTPObservableSectionProvider { }) var urlChildren: [UIAction] = [openInNewTab] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !self.isPrivateBrowsing { let openInNewPrivateTab = UIAction( title: Strings.openNewPrivateTabButtonTitle, handler: UIAction.deferredActionHandler { _ in diff --git a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/StatsSectionProvider.swift b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/StatsSectionProvider.swift index e6539d70c6b..fe41ddb8b59 100644 --- a/Sources/Brave/Frontend/Browser/New Tab Page/Sections/StatsSectionProvider.swift +++ b/Sources/Brave/Frontend/Browser/New Tab Page/Sections/StatsSectionProvider.swift @@ -12,10 +12,12 @@ import BraveUI import UIKit class StatsSectionProvider: NSObject, NTPSectionProvider { + private let isPrivateBrowsing: Bool var openPrivacyHubPressed: () -> Void var hidePrivacyHubPressed: () -> Void - init(openPrivacyHubPressed: @escaping () -> Void, hidePrivacyHubPressed: @escaping () -> Void) { + init(isPrivateBrowsing: Bool, openPrivacyHubPressed: @escaping () -> Void, hidePrivacyHubPressed: @escaping () -> Void) { + self.isPrivateBrowsing = isPrivateBrowsing self.openPrivacyHubPressed = openPrivacyHubPressed self.hidePrivacyHubPressed = hidePrivacyHubPressed } @@ -48,6 +50,7 @@ class StatsSectionProvider: NSObject, NTPSectionProvider { let longPress = UILongPressGestureRecognizer(target: self, action: #selector(tappedButton(_:))) cell.view.do { + $0.isPrivateBrowsing = self.isPrivateBrowsing $0.addGestureRecognizer(tap) $0.addGestureRecognizer(longPress) @@ -147,45 +150,10 @@ class BraveShieldStatsView: SpringButton { }() } + private let backgroundView = UIView() + override init(frame: CGRect) { - super.init(frame: frame) - - if !PrivateBrowsingManager.shared.isPrivateBrowsing, Preferences.NewTabPage.showNewTabPrivacyHub.value { - let background = UIView() - background.backgroundColor = .init(white: 0, alpha: 0.25) - background.layer.cornerRadius = 12 - background.layer.cornerCurve = .continuous - background.isUserInteractionEnabled = false - insertSubview(background, at: 0) - background.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - let settingsButton = BraveButton(type: .system).then { - $0.setImage(UIImage(named: "privacy_reports_3dots", in: .module, compatibleWith: nil)!.template, for: .normal) - $0.tintColor = .white - $0.hitTestSlop = UIEdgeInsets(equalInset: -25) - } - - let hidePrivacyHub = UIAction( - title: Strings.PrivacyHub.hidePrivacyHubWidgetActionTitle, - image: UIImage(braveSystemNamed: "leo.eye.off"), - handler: UIAction.deferredActionHandler { [unowned self] _ in - self.hidePrivacyHubPressed?() - }) - - let showPrivacyHub = UIAction( - title: Strings.PrivacyHub.openPrivacyHubWidgetActionTitle, - image: UIImage(named: "privacy_reports_shield", in: .module, compatibleWith: nil)?.template, - handler: UIAction.deferredActionHandler { [unowned self] _ in - self.openPrivacyHubPressed?() - }) - - settingsButton.menu = UIMenu(title: "", options: .displayInline, children: [hidePrivacyHub, showPrivacyHub]) - settingsButton.showsMenuAsPrimaryAction = true - - topStackView.addStackViewItems(.view(privacyReportLabel), .view(settingsButton)) - } + super.init(frame: .zero) statsStackView.addStackViewItems(.view(adsStatView), .view(dataSavedStatView), .view(timeStatView)) contentStackView.addStackViewItems(.view(topStackView), .view(statsStackView)) @@ -205,6 +173,55 @@ class BraveShieldStatsView: SpringButton { fatalError("init(coder:) has not been implemented") } + var isPrivateBrowsing: Bool = false { + didSet { + if oldValue == isPrivateBrowsing { + return + } + + if isPrivateBrowsing { + backgroundView.removeFromSuperview() + topStackView.arrangedSubviews.forEach { + $0.removeFromSuperview() + } + } else if Preferences.NewTabPage.showNewTabPrivacyHub.value { + backgroundView.backgroundColor = .init(white: 0, alpha: 0.25) + backgroundView.layer.cornerRadius = 12 + backgroundView.layer.cornerCurve = .continuous + backgroundView.isUserInteractionEnabled = false + insertSubview(backgroundView, at: 0) + backgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + let settingsButton = BraveButton(type: .system).then { + $0.setImage(UIImage(named: "privacy_reports_3dots", in: .module, compatibleWith: nil)!.template, for: .normal) + $0.tintColor = .white + $0.hitTestSlop = UIEdgeInsets(equalInset: -25) + } + + let hidePrivacyHub = UIAction( + title: Strings.PrivacyHub.hidePrivacyHubWidgetActionTitle, + image: UIImage(braveSystemNamed: "leo.eye.off"), + handler: UIAction.deferredActionHandler { [unowned self] _ in + self.hidePrivacyHubPressed?() + }) + + let showPrivacyHub = UIAction( + title: Strings.PrivacyHub.openPrivacyHubWidgetActionTitle, + image: UIImage(named: "privacy_reports_shield", in: .module, compatibleWith: nil)?.template, + handler: UIAction.deferredActionHandler { [unowned self] _ in + self.openPrivacyHubPressed?() + }) + + settingsButton.menu = UIMenu(title: "", options: .displayInline, children: [hidePrivacyHub, showPrivacyHub]) + settingsButton.showsMenuAsPrimaryAction = true + + topStackView.addStackViewItems(.view(privacyReportLabel), .view(settingsButton)) + } + } + } + deinit { NotificationCenter.default.removeObserver(self) } diff --git a/Sources/Brave/Frontend/Browser/PageZoom/PageZoomView.swift b/Sources/Brave/Frontend/Browser/PageZoom/PageZoomView.swift index 89e7138cbe8..4b02a9cea09 100644 --- a/Sources/Brave/Frontend/Browser/PageZoom/PageZoomView.swift +++ b/Sources/Brave/Frontend/Browser/PageZoom/PageZoomView.swift @@ -13,6 +13,7 @@ import Preferences private struct ZoomView: View { @ScaledMetric private var buttonWidth = 44.0 + var isPrivateBrowsing: Bool var minValue: Double var maxValue: Double @Binding var value: Double @@ -62,12 +63,12 @@ private struct ZoomView: View { Button(action: onReset) { Text(NSNumber(value: value), formatter: PageZoomView.percentFormatter) .font(.system(.footnote).weight(.medium)) - .foregroundColor((value == (PrivateBrowsingManager.shared.isPrivateBrowsing ? 1.0 : Preferences.General.defaultPageZoomLevel.value)) ? .accentColor : Color(UIColor.braveLabel)) + .foregroundColor((value == (isPrivateBrowsing ? 1.0 : Preferences.General.defaultPageZoomLevel.value)) ? .accentColor : Color(UIColor.braveLabel)) .padding() .contentShape(Rectangle()) } .fixedSize(horizontal: true, vertical: false) - .disabled(value == (PrivateBrowsingManager.shared.isPrivateBrowsing ? 1.0 : Preferences.General.defaultPageZoomLevel.value)) + .disabled(value == (isPrivateBrowsing ? 1.0 : Preferences.General.defaultPageZoomLevel.value)) } } @@ -75,6 +76,7 @@ struct PageZoomView: View { @Environment(\.managedObjectContext) private var context private var webView: WKWebView? + private let isPrivateBrowsing: Bool @State private var minValue = 0.5 @State private var maxValue = 3.0 @State private var currentValue: Double @@ -97,11 +99,12 @@ struct PageZoomView: View { var dismiss: (() -> Void)? - init(webView: WKWebView?) { + init(webView: WKWebView?, isPrivateBrowsing: Bool) { self.webView = webView + self.isPrivateBrowsing = isPrivateBrowsing // Private Browsing on Safari iOS always defaults to 100%, and isn't persistently saved. - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if isPrivateBrowsing { _currentValue = State(initialValue: 1.0) return } @@ -126,6 +129,7 @@ struct PageZoomView: View { .frame(maxWidth: .infinity, alignment: .leading) ZoomView( + isPrivateBrowsing: isPrivateBrowsing, minValue: minValue, maxValue: maxValue, value: $currentValue, @@ -156,7 +160,7 @@ struct PageZoomView: View { webView.setValue(currentValue, forKey: PageZoomView.propertyName) // Do NOT store the changes in the Domain - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !isPrivateBrowsing { let domain = Domain.getPersistedDomain(for: url)?.then { $0.zoom_level = currentValue == $0.zoom_level?.doubleValue ? nil : NSNumber(value: currentValue) } @@ -188,7 +192,7 @@ struct PageZoomView: View { #if DEBUG struct PageZoomView_Previews: PreviewProvider { static var previews: some View { - PageZoomView(webView: nil) + PageZoomView(webView: nil, isPrivateBrowsing: false) .previewLayout(PreviewLayout.sizeThatFits) } } diff --git a/Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift b/Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift index 4e76e80d4b2..0fcca931f4a 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Browser/BrowserViewController+Playlist.swift @@ -61,32 +61,35 @@ extension BrowserViewController: PlaylistScriptHandlerDelegate, PlaylistFolderSh guard let tab = tab else { return } if tab === tabManager.selectedTab { - openInPlayListActivity(info: state == .existingItem ? item : nil) - addToPlayListActivity(info: state == .newItem ? item : nil, itemDetected: state == .newItem) - tab.playlistItemState = state tab.playlistItem = item let shouldShowPlaylistURLBarButton = tab.url?.isPlaylistSupportedSiteURL == true && Preferences.Playlist.enablePlaylistURLBarButton.value - switch state { - case .none: - topToolbar.updatePlaylistButtonState(.none) - topToolbar.menuButton.removeBadge(.playlist, animated: true) - toolbar?.menuButton.removeBadge(.playlist, animated: true) - case .newItem: - topToolbar.updatePlaylistButtonState(shouldShowPlaylistURLBarButton ? .addToPlaylist : .none) - if Preferences.Playlist.enablePlaylistMenuBadge.value { - topToolbar.menuButton.addBadge(.playlist, animated: true) - toolbar?.menuButton.addBadge(.playlist, animated: true) - } else { - topToolbar.menuButton.removeBadge(.playlist, animated: true) - toolbar?.menuButton.removeBadge(.playlist, animated: true) + let browsers = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).compactMap({ $0.browserViewController }) + browsers.forEach { browser in + browser.openInPlayListActivity(info: state == .existingItem ? item : nil) + browser.addToPlayListActivity(info: state == .newItem ? item : nil, itemDetected: state == .newItem) + + switch state { + case .none: + browser.topToolbar.updatePlaylistButtonState(.none) + browser.topToolbar.menuButton.removeBadge(.playlist, animated: true) + browser.toolbar?.menuButton.removeBadge(.playlist, animated: true) + case .newItem: + browser.topToolbar.updatePlaylistButtonState(shouldShowPlaylistURLBarButton ? .addToPlaylist : .none) + if Preferences.Playlist.enablePlaylistMenuBadge.value { + browser.topToolbar.menuButton.addBadge(.playlist, animated: true) + browser.toolbar?.menuButton.addBadge(.playlist, animated: true) + } else { + browser.topToolbar.menuButton.removeBadge(.playlist, animated: true) + browser.toolbar?.menuButton.removeBadge(.playlist, animated: true) + } + case .existingItem: + browser.topToolbar.updatePlaylistButtonState(shouldShowPlaylistURLBarButton ? .addedToPlaylist(item) : .none) + browser.topToolbar.menuButton.removeBadge(.playlist, animated: true) + browser.toolbar?.menuButton.removeBadge(.playlist, animated: true) } - case .existingItem: - topToolbar.updatePlaylistButtonState(shouldShowPlaylistURLBarButton ? .addedToPlaylist(item) : .none) - topToolbar.menuButton.removeBadge(.playlist, animated: true) - toolbar?.menuButton.removeBadge(.playlist, animated: true) } } } diff --git a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistDetailViewController.swift b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistDetailViewController.swift index 45c4c95d0d6..c40aa4ee348 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistDetailViewController.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistDetailViewController.swift @@ -12,8 +12,18 @@ import UIKit class PlaylistDetailViewController: UIViewController, UIGestureRecognizerDelegate { private var playerView: VideoView? + private let isPrivateBrowsing: Bool weak var delegate: PlaylistViewControllerDelegate? - + + init(isPrivateBrowsing: Bool) { + self.isPrivateBrowsing = isPrivateBrowsing + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -147,11 +157,10 @@ extension PlaylistDetailViewController { if let url = URL(string: item.pageSrc) { self.dismiss(animated: true, completion: nil) - - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + self.delegate?.openURLInNewTab( url, - isPrivate: isPrivateBrowsing, + isPrivate: self.isPrivateBrowsing, isPrivileged: false) } })) diff --git a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController+TableViewDelegate.swift b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController+TableViewDelegate.swift index 76dbdf91fa6..f5b0fe7f207 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController+TableViewDelegate.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController+TableViewDelegate.swift @@ -149,7 +149,7 @@ extension PlaylistListViewController: UITableViewDelegate { handler: { [weak self] (action, view, completionHandler) in guard let self = self else { return } - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = self.isPrivateBrowsing let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet let alert = UIAlertController( @@ -244,7 +244,7 @@ extension PlaylistListViewController: UITableViewDelegate { // In Private-Browsing, we do not show "Open in New Tab", // we only show "Open in Private Tab" - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = self.isPrivateBrowsing if !isPrivateBrowsing { actions.append( UIAction( diff --git a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift index 22f595c8fc6..f28860a6bbc 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistListViewController.swift @@ -48,6 +48,8 @@ class PlaylistListViewController: UIViewController { weak var delegate: PlaylistViewControllerDelegate? private let playerView: VideoView + let isPrivateBrowsing: Bool + private var observers = Set() private var folderObserver: AnyCancellable? private var sharedFolderLoadingTask: Task? @@ -72,8 +74,9 @@ class PlaylistListViewController: UIViewController { $0.allowsSelectionDuringEditing = true } - init(playerView: VideoView) { + init(playerView: VideoView, isPrivateBrowsing: Bool) { self.playerView = playerView + self.isPrivateBrowsing = isPrivateBrowsing super.init(nibName: nil, bundle: nil) } @@ -638,7 +641,7 @@ extension PlaylistListViewController { activityIndicator.isHidden = false let selectedCell = tableView.cellForRow(at: indexPath) as? PlaylistCell - playerView.setVideoInfo(videoDomain: item.pageSrc, videoTitle: item.pageTitle) + playerView.setVideoInfo(videoDomain: item.pageSrc, videoTitle: item.pageTitle, isPrivateBrowsing: isPrivateBrowsing) PlaylistMediaStreamer.setNowPlayingMediaArtwork(image: selectedCell?.iconView.image) completion?(item) } @@ -753,7 +756,7 @@ extension PlaylistListViewController { if let url = URL(string: item.pageSrc) { self.dismiss(animated: true, completion: nil) - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = self.isPrivateBrowsing self.delegate?.openURLInNewTab( url, isPrivate: isPrivateBrowsing, @@ -826,7 +829,7 @@ extension PlaylistListViewController { let pageURL = URL(string: currentItem.pageSrc) { self.dismiss(animated: true) { - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = self.isPrivateBrowsing browser.tabManager.addTabAndSelect( URLRequest(url: pageURL), isPrivate: isPrivateBrowsing) diff --git a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistNewFolderView.swift b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistNewFolderView.swift index ea8d363503d..c13fae364e8 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistNewFolderView.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistNewFolderView.swift @@ -41,7 +41,7 @@ class PlaylistFolderImageLoader: ObservableObject { func load(domainUrl: URL) { faviconTask?.cancel() faviconTask = Task { @MainActor in - let favicon = try await FaviconFetcher.loadIcon(url: domainUrl, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let favicon = try await FaviconFetcher.loadIcon(url: domainUrl, persistent: true) self.image = favicon.image } } diff --git a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift index e87c085c5f6..bb7effae2ec 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Controllers/PlaylistViewController.swift @@ -49,11 +49,12 @@ class PlaylistViewController: UIViewController { private let player: MediaPlayer private let playerView = VideoView() private let mediaStreamer: PlaylistMediaStreamer + private let isPrivateBrowsing: Bool private let splitController = UISplitViewController() private let folderController = PlaylistFolderController() - private lazy var listController = PlaylistListViewController(playerView: playerView) - private let detailController = PlaylistDetailViewController() + private lazy var listController = PlaylistListViewController(playerView: playerView, isPrivateBrowsing: isPrivateBrowsing) + private lazy var detailController = PlaylistDetailViewController(isPrivateBrowsing: isPrivateBrowsing) private var folderObserver: AnyCancellable? private var playerStateObservers = Set() @@ -68,14 +69,17 @@ class PlaylistViewController: UIViewController { profile: Profile?, mediaPlayer: MediaPlayer, initialItem: PlaylistInfo?, - initialItemPlaybackOffset: Double + initialItemPlaybackOffset: Double, + isPrivateBrowsing: Bool ) { self.openInNewTab = openInNewTab self.openPlaylistSettingsMenu = openPlaylistSettingsMenu self.player = mediaPlayer self.mediaStreamer = PlaylistMediaStreamer(playerView: playerView) + self.isPrivateBrowsing = isPrivateBrowsing self.folderSharingUrl = nil + super.init(nibName: nil, bundle: nil) listController.initialItem = initialItem @@ -89,6 +93,8 @@ class PlaylistViewController: UIViewController { } deinit { + PlaylistCarplayManager.shared.isPlaylistControllerPresented = false + // Store the last played item's time-offset if let item = PlaylistCarplayManager.shared.currentPlaylistItem { updateLastPlayedItem(item: item) @@ -309,7 +315,7 @@ class PlaylistViewController: UIViewController { } if let item = PlaylistCarplayManager.shared.currentPlaylistItem { - playerView.setVideoInfo(videoDomain: item.pageSrc, videoTitle: item.pageTitle) + playerView.setVideoInfo(videoDomain: item.pageSrc, videoTitle: item.pageTitle, isPrivateBrowsing: isPrivateBrowsing) } else { playerView.resetVideoInfo() } @@ -326,7 +332,8 @@ class PlaylistViewController: UIViewController { } else if let item = PlaylistCarplayManager.shared.currentPlaylistItem { self.playerView.setVideoInfo( videoDomain: item.pageSrc, - videoTitle: item.pageTitle) + videoTitle: item.pageTitle, + isPrivateBrowsing: self.isPrivateBrowsing) } self.listController.highlightActiveItem() @@ -908,7 +915,8 @@ extension PlaylistViewController: VideoViewDelegate { PlaylistMediaStreamer.clearNowPlayingInfo() self.playerView.setVideoInfo( videoDomain: item.pageSrc, - videoTitle: item.pageTitle) + videoTitle: item.pageTitle, + isPrivateBrowsing: self.isPrivateBrowsing) PlaylistMediaStreamer.setNowPlayingInfo(item, withPlayer: self.player) } catch { PlaylistMediaStreamer.clearNowPlayingInfo() @@ -955,7 +963,8 @@ extension PlaylistViewController: VideoViewDelegate { playerView.setVideoInfo( videoDomain: item.pageSrc, - videoTitle: item.pageTitle) + videoTitle: item.pageTitle, + isPrivateBrowsing: isPrivateBrowsing) PlaylistMediaStreamer.setNowPlayingInfo(item, withPlayer: self.player) return item diff --git a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift index b5d27cc0038..093ba2698e5 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift @@ -554,8 +554,7 @@ extension PlaylistWebLoader: WKNavigationDelegate { tab.currentPageData = PageData(mainFrameURL: mainDocumentURL) } - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing - let domainForMainFrame = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: !isPrivateBrowsing) + let domainForMainFrame = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: false) if let requestURL = navigationAction.request.url, let targetFrame = navigationAction.targetFrame { diff --git a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCarplayManager.swift b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCarplayManager.swift index 6fbed239f98..ba490f69e0b 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCarplayManager.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCarplayManager.swift @@ -24,10 +24,11 @@ public class PlaylistCarplayManager: NSObject { private var carplaySessionConfiguration: CPSessionConfiguration? let onCarplayUIChangedToRoot = PassthroughSubject() - public var browserController: BrowserViewController? + public weak var browserController: BrowserViewController? var currentlyPlayingItemIndex = -1 var currentPlaylistItem: PlaylistInfo? + var isPlaylistControllerPresented = false // When Picture-In-Picture is enabled, we need to store a reference to the controller to keep it alive, otherwise if it deallocates, the system automatically kills Picture-In-Picture. var playlistController: PlaylistViewController? { @@ -72,23 +73,13 @@ public class PlaylistCarplayManager: NSObject { // Setup Playlist Download Resume Session PlaylistManager.shared.restoreSession() - // REFACTOR to support multiple windows. - // OR find a way to get WebKit to load `Youtube` and other sites WITHOUT having to be in the view hierarchy.. - var currentWindow = browserController?.view.window - - // BrowserController can actually be null when CarPlay is launched by the system - // The only such window that will be available is the actual CarPlay window - // So that's the window we need to use - // This does NOT break Multi-Window because we don't actually have multiple windows running - // when there is no browser controller. There is only a single CarPlay window. - if currentWindow == nil { - currentWindow = - UIApplication.shared.connectedScenes - .filter({ $0.activationState == .foregroundActive }) - .compactMap({ $0 as? UIWindowScene }) - .first?.windows - .filter({ $0.isKeyWindow }).first - } + // REFACTOR to find a way to get WebKit to load `Youtube` and other sites WITHOUT having to be in the view hierarchy.. + let currentWindow = + UIApplication.shared.connectedScenes + .filter({ $0.activationState == .foregroundActive }) + .compactMap({ $0 as? UIWindowScene }) + .first?.windows + .filter({ $0.isKeyWindow }).first // If there is no media player, create one, // pass it to the car-play controller @@ -125,7 +116,8 @@ public class PlaylistCarplayManager: NSObject { profile: browserController?.profile, mediaPlayer: mediaPlayer, initialItem: initialItem, - initialItemPlaybackOffset: initialItemPlaybackOffset) + initialItemPlaybackOffset: initialItemPlaybackOffset, + isPrivateBrowsing: browserController?.privateBrowsingManager.isPrivateBrowsing == true) self.mediaPlayer = mediaPlayer return playlistController } diff --git a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistManager.swift b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistManager.swift index b5327638417..77e58b0c43b 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistManager.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistManager.swift @@ -253,8 +253,6 @@ public class PlaylistManager: NSObject { public func restoreSession() { if !didRestoreSession { - didRestoreSession = true - downloadManager.restoreSession() { [weak self] in self?.reloadData() } diff --git a/Sources/Brave/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift b/Sources/Brave/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift index 45c8ac38574..1540cddb98b 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Utilities/PlaylistThumbnailUtility.swift @@ -150,7 +150,7 @@ public class PlaylistThumbnailRenderer { favIconGenerator = Task { @MainActor in do { - let favicon = try await FaviconFetcher.loadIcon(url: url, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let favicon = try await FaviconFetcher.loadIcon(url: url, persistent: true) await SDImageCache.shared.store(favicon.image, forKey: url.absoluteString) completion(favicon.image) } catch { diff --git a/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayer.swift b/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayer.swift index 0bed804aedf..23139986c02 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayer.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayer.swift @@ -574,7 +574,7 @@ class VideoView: UIView, VideoTrackerBarDelegate { staticImageView.contentMode = .scaleAspectFit } - func setVideoInfo(videoDomain: String, videoTitle: String?) { + func setVideoInfo(videoDomain: String, videoTitle: String?, isPrivateBrowsing: Bool) { var displayTitle = videoTitle ?? "" if displayTitle.isEmpty { @@ -596,7 +596,7 @@ class VideoView: UIView, VideoTrackerBarDelegate { } infoView.titleLabel.text = displayTitle - infoView.updateFavIcon(domain: videoDomain) + infoView.updateFavIcon(domain: videoDomain, isPrivateBrowsing: isPrivateBrowsing) } func resetVideoInfo() { diff --git a/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayerInfoBar.swift b/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayerInfoBar.swift index 3b83a3ce0bf..f144ad03bc3 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayerInfoBar.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/VideoPlayer/UI/VideoPlayerInfoBar.swift @@ -108,14 +108,14 @@ class VideoPlayerInfoBar: UIView { fatalError("init(coder:) has not been implemented") } - func updateFavIcon(domain: String) { + func updateFavIcon(domain: String, isPrivateBrowsing: Bool) { favIconImageView.cancelFaviconLoad() favIconImageView.clearMonogramFavicon() favIconImageView.contentMode = .scaleAspectFit favIconImageView.image = Favicon.defaultImage if let url = URL(string: domain) { - favIconImageView.loadFavicon(for: url) + favIconImageView.loadFavicon(for: url, isPrivateBrowsing: isPrivateBrowsing) } } diff --git a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyHubAllTimeSection.swift b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyHubAllTimeSection.swift index 2b99f6aa9fe..2b84039014c 100644 --- a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyHubAllTimeSection.swift +++ b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyHubAllTimeSection.swift @@ -21,6 +21,7 @@ extension PrivacyReportsView { @State private var mostFrequentTrackerLoading = true @State private var riskiestWebsiteLoading = true + private(set) var isPrivateBrowsing: Bool private(set) var onDismiss: () -> Void private func allTimeItemView(trackerOrWebsite: CountableEntity?, countableLabel: String, header: String) -> some View { @@ -87,7 +88,7 @@ extension PrivacyReportsView { } NavigationLink( - destination: PrivacyReportAllTimeListsView(onDismiss: onDismiss) + destination: PrivacyReportAllTimeListsView(isPrivateBrowsing: isPrivateBrowsing, onDismiss: onDismiss) ) { HStack { Text(Strings.PrivacyHub.allTimeListsButtonText) @@ -122,7 +123,7 @@ extension PrivacyReportsView { #if DEBUG struct PrivacyHubAllTimeSection_Previews: PreviewProvider { static var previews: some View { - PrivacyReportsView.PrivacyHubAllTimeSection(onDismiss: {}) + PrivacyReportsView.PrivacyHubAllTimeSection(isPrivateBrowsing: false, onDismiss: {}) } } #endif diff --git a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportAllTimeListsView.swift b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportAllTimeListsView.swift index d08a5b221ce..597815e7ea4 100644 --- a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportAllTimeListsView.swift +++ b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportAllTimeListsView.swift @@ -18,6 +18,7 @@ struct PrivacyReportAllTimeListsView: View { @State private var trackersLoading = true @State private var websitesLoading = true + private(set) var isPrivateBrowsing: Bool private(set) var onDismiss: () -> Void enum Page: String, CaseIterable, Identifiable { @@ -126,7 +127,7 @@ struct PrivacyReportAllTimeListsView: View { Section { ForEach(websites) { item in HStack { - FaviconImage(url: item.faviconUrl) + FaviconImage(url: item.faviconUrl, isPrivateBrowsing: isPrivateBrowsing) Text(item.domain) Spacer() Text("\(item.count)") @@ -199,8 +200,8 @@ struct PrivacyReportAllTimeListsView: View { struct PrivacyReportAllTimeListsView_Previews: PreviewProvider { static var previews: some View { Group { - PrivacyReportAllTimeListsView(onDismiss: {}) - PrivacyReportAllTimeListsView(onDismiss: {}) + PrivacyReportAllTimeListsView(isPrivateBrowsing: false, onDismiss: {}) + PrivacyReportAllTimeListsView(isPrivateBrowsing: false, onDismiss: {}) .preferredColorScheme(.dark) } } diff --git a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsManager.swift b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsManager.swift index 235222dd33c..41119e82940 100644 --- a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsManager.swift +++ b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsManager.swift @@ -19,9 +19,7 @@ public struct PrivacyReportsManager { /// Instead a periodic timer is run and all requests gathered during this timeframe are saved in one database transaction. public static var pendingBlockedRequests: [(host: String, domain: URL, date: Date)] = [] - public static func processBlockedRequests() { - if PrivateBrowsingManager.shared.isPrivateBrowsing { return } - + private static func processBlockedRequests() { let itemsToSave = pendingBlockedRequests pendingBlockedRequests.removeAll() @@ -35,13 +33,15 @@ public struct PrivacyReportsManager { private static var saveBlockedResourcesTimer: Timer? private static var vpnAlertsTimer: Timer? - public static func scheduleProcessingBlockedRequests() { + public static func scheduleProcessingBlockedRequests(isPrivateBrowsing: Bool) { saveBlockedResourcesTimer?.invalidate() let timeInterval = AppConstants.buildChannel.isPublic ? 60.0 : 10.0 saveBlockedResourcesTimer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { _ in - processBlockedRequests() + if !isPrivateBrowsing { + processBlockedRequests() + } } } @@ -80,9 +80,9 @@ public struct PrivacyReportsManager { // MARK: - View /// Fetches required data to present the privacy reports view and returns the view. - static func prepareView() -> PrivacyReportsView { + static func prepareView(isPrivateBrowsing: Bool) -> PrivacyReportsView { let last = BraveVPNAlert.last(3) - let view = PrivacyReportsView(lastVPNAlerts: last) + let view = PrivacyReportsView(lastVPNAlerts: last, isPrivateBrowsing: isPrivateBrowsing) Preferences.PrivacyReports.ntpOnboardingCompleted.value = true diff --git a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsView.swift b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsView.swift index a5aabf188ba..0dade80a4b8 100644 --- a/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsView.swift +++ b/Sources/Brave/Frontend/Browser/PrivacyHub/PrivacyReportsView.swift @@ -13,6 +13,7 @@ struct PrivacyReportsView: View { let lastVPNAlerts: [BraveVPNAlert]? + private(set) var isPrivateBrowsing: Bool var onDismiss: (() -> Void)? var openPrivacyReportsUrl: (() -> Void)? @@ -97,7 +98,7 @@ struct PrivacyReportsView: View { Divider() } - PrivacyHubAllTimeSection(onDismiss: dismissView) + PrivacyHubAllTimeSection(isPrivateBrowsing: isPrivateBrowsing, onDismiss: dismissView) VStack { Text(Strings.PrivacyHub.privacyReportsDisclaimer) @@ -142,9 +143,9 @@ struct PrivacyReports_Previews: PreviewProvider { static var previews: some View { Group { - PrivacyReportsView(lastVPNAlerts: nil) + PrivacyReportsView(lastVPNAlerts: nil, isPrivateBrowsing: false) - PrivacyReportsView(lastVPNAlerts: nil) + PrivacyReportsView(lastVPNAlerts: nil, isPrivateBrowsing: false) .preferredColorScheme(.dark) } } diff --git a/Sources/Brave/Frontend/Browser/PrivacyProtection/PrivateBrowsingManager.swift b/Sources/Brave/Frontend/Browser/PrivacyProtection/PrivateBrowsingManager.swift index 1020d7f7cfa..d5bf540fb68 100644 --- a/Sources/Brave/Frontend/Browser/PrivacyProtection/PrivateBrowsingManager.swift +++ b/Sources/Brave/Frontend/Browser/PrivacyProtection/PrivateBrowsingManager.swift @@ -7,6 +7,8 @@ import Data import Combine public final class PrivateBrowsingManager: ObservableObject { + + public init() {} @Published public var isPrivateBrowsing = false { didSet { @@ -17,6 +19,4 @@ public final class PrivateBrowsingManager: ObservableObject { } } } - - public static let shared = PrivateBrowsingManager() } diff --git a/Sources/Brave/Frontend/Browser/Search/SearchEngines.swift b/Sources/Brave/Frontend/Browser/Search/SearchEngines.swift index 01877f39744..aaad03c9209 100644 --- a/Sources/Brave/Frontend/Browser/Search/SearchEngines.swift +++ b/Sources/Brave/Frontend/Browser/Search/SearchEngines.swift @@ -77,9 +77,7 @@ public class SearchEngines { } /// If no engine type is specified this method returns search engine for regular browsing. - func defaultEngine(forType type: DefaultEngineType? = nil) -> OpenSearchEngine { - let engineType = type ?? (PrivateBrowsingManager.shared.isPrivateBrowsing ? .privateMode : .standard) - + func defaultEngine(forType engineType: DefaultEngineType) -> OpenSearchEngine { if let name = engineType.option.value, let defaultEngine = orderedEngines.first(where: { $0.engineID == name || $0.shortName == name }) { return defaultEngine @@ -141,7 +139,7 @@ public class SearchEngines { } } - func isEngineDefault(_ engine: OpenSearchEngine, type: DefaultEngineType? = nil) -> Bool { + func isEngineDefault(_ engine: OpenSearchEngine, type: DefaultEngineType) -> Bool { return defaultEngine(forType: type).shortName == engine.shortName } @@ -192,8 +190,8 @@ public class SearchEngines { disabledEngineNames.removeValue(forKey: engine.shortName) } - func disableEngine(_ engine: OpenSearchEngine) { - if isEngineDefault(engine) { + func disableEngine(_ engine: OpenSearchEngine, type: DefaultEngineType) { + if isEngineDefault(engine, type: type) { // Can't disable default engine. return } @@ -232,8 +230,8 @@ public class SearchEngines { } } - func queryForSearchURL(_ url: URL?) -> String? { - return defaultEngine().queryForSearchURL(url) + func queryForSearchURL(_ url: URL?, forType engineType: DefaultEngineType) -> String? { + return defaultEngine(forType: engineType).queryForSearchURL(url) } fileprivate func getDisabledEngineNames() -> [String: Bool] { diff --git a/Sources/Brave/Frontend/Browser/Search/SearchSuggestionDataSource.swift b/Sources/Brave/Frontend/Browser/Search/SearchSuggestionDataSource.swift index 43530a5702e..9b6df7efa32 100644 --- a/Sources/Brave/Frontend/Browser/Search/SearchSuggestionDataSource.swift +++ b/Sources/Brave/Frontend/Browser/Search/SearchSuggestionDataSource.swift @@ -53,7 +53,7 @@ class SearchSuggestionDataSource { // In that case, we count it as if there are no quick suggestions to show // Unless Default Search Engine is different than Quick Search Engine var hasQuickSearchEngines: Bool { - let isDefaultEngineQuickEngine = searchEngines?.defaultEngine().engineID == quickSearchEngines.first?.engineID + let isDefaultEngineQuickEngine = searchEngines?.defaultEngine(forType: tabType == .private ? .privateMode : .standard).engineID == quickSearchEngines.first?.engineID if quickSearchEngines.count == 1 { return !isDefaultEngineQuickEngine @@ -82,7 +82,7 @@ class SearchSuggestionDataSource { var braveSearchPromotionAvailable: Bool { guard Preferences.Review.launchCount.value > 1, - searchEngines?.defaultEngine().shortName != OpenSearchEngine.EngineNames.brave, + searchEngines?.defaultEngine(forType: tabType == .private ? .privateMode : .standard).shortName != OpenSearchEngine.EngineNames.brave, let braveSearchPromotionLaunchDate = Preferences.BraveSearch.braveSearchPromotionLaunchDate.value, Preferences.BraveSearch.braveSearchPromotionCompletionState.value != BraveSearchPromotionState.dismissed.rawValue, Preferences.BraveSearch.braveSearchPromotionCompletionState.value != BraveSearchPromotionState.maybeLaterSameSession.rawValue else { @@ -167,7 +167,7 @@ class SearchSuggestionDataSource { // Show the default search engine first. if !tabType.isPrivate, let userAgent = SearchViewController.userAgent, - let engines = searchEngines?.defaultEngine() { + let engines = searchEngines?.defaultEngine(forType: tabType == .private ? .privateMode : .standard) { suggestClient = SearchSuggestClient(searchEngine: engines, userAgent: userAgent) } } diff --git a/Sources/Brave/Frontend/Browser/Search/SearchViewController.swift b/Sources/Brave/Frontend/Browser/Search/SearchViewController.swift index 8f29ebaa876..1017ee05499 100644 --- a/Sources/Brave/Frontend/Browser/Search/SearchViewController.swift +++ b/Sources/Brave/Frontend/Browser/Search/SearchViewController.swift @@ -333,7 +333,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { } private func submitSeachTemplateQuery(isBraveSearchPromotion: Bool) { - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !dataSource.tabType.isPrivate { RecentSearch.addItem(type: .text, text: dataSource.searchQuery, websiteUrl: nil) } searchDelegate?.searchViewController( @@ -359,7 +359,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { return } - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !dataSource.tabType.isPrivate { RecentSearch.addItem(type: .website, text: localSearchQuery, websiteUrl: url.absoluteString) } searchDelegate?.searchViewController(self, didSelectURL: url) @@ -381,7 +381,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { case .searchSuggestions: if !isBraveSearchPrompt(for: indexPath) { // Assume that only the default search engine can provide search suggestions. - let engine = dataSource.searchEngines?.defaultEngine() + let engine = dataSource.searchEngines?.defaultEngine(forType: dataSource.tabType == .private ? .privateMode : .standard) let suggestion = dataSource.suggestions[indexPath.row] var url = URIFixup.getURL(suggestion) @@ -390,7 +390,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { } if let url = url { - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !dataSource.tabType.isPrivate { RecentSearch.addItem(type: .website, text: suggestion, websiteUrl: url.absoluteString) } searchDelegate?.searchViewController(self, didSelectURL: url) @@ -441,7 +441,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { case .quickBar: return nil case .searchSuggestionsOptIn: return nil case .searchSuggestions: - if let defaultSearchEngine = dataSource.searchEngines?.defaultEngine() { + if let defaultSearchEngine = dataSource.searchEngines?.defaultEngine(forType: dataSource.tabType == .private ? .privateMode : .standard) { if defaultSearchEngine.shortName.contains(Strings.searchSuggestionSectionTitleNoSearchFormat) || defaultSearchEngine.shortName.lowercased().contains("search") { return defaultSearchEngine.displayName } @@ -622,7 +622,7 @@ public class SearchViewController: SiteTableViewController, LoaderListener { cell.imageView?.contentMode = .scaleAspectFit cell.imageView?.layer.borderColor = SearchViewControllerUX.iconBorderColor.cgColor cell.imageView?.layer.borderWidth = SearchViewControllerUX.iconBorderWidth - cell.imageView?.loadFavicon(for: site.tileURL) + cell.imageView?.loadFavicon(for: site.tileURL, isPrivateBrowsing: dataSource.tabType.isPrivate) cell.backgroundColor = .secondaryBraveBackground } diff --git a/Sources/Brave/Frontend/Browser/Tab.swift b/Sources/Brave/Frontend/Browser/Tab.swift index 30dcb4f59f2..1eafd4c6421 100644 --- a/Sources/Brave/Frontend/Browser/Tab.swift +++ b/Sources/Brave/Frontend/Browser/Tab.swift @@ -74,7 +74,6 @@ class Tab: NSObject { private(set) var type: TabType = .regular - var redirectURLs = [URL]() var isPrivate: Bool { @@ -555,7 +554,7 @@ class Tab: NSObject { /// In private browsing the URL is in memory but this is not the case for normal mode /// For Normal Mode Tab information is fetched using Tab ID from var fetchedURL: URL? { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if isPrivate { if let url = url, url.isWebPage() { return url } diff --git a/Sources/Brave/Frontend/Browser/TabManager.swift b/Sources/Brave/Frontend/Browser/TabManager.swift index e2cb9ccf62f..67ce8e422d4 100644 --- a/Sources/Brave/Frontend/Browser/TabManager.swift +++ b/Sources/Brave/Frontend/Browser/TabManager.swift @@ -92,6 +92,9 @@ class TabManager: NSObject { private let syncedTabsQueue = DispatchQueue(label: "synced-tabs-queue") private var syncTabsTask: DispatchWorkItem? private var metricsHeartbeat: Timer? + public let privateBrowsingManager: PrivateBrowsingManager + + let windowId: UUID /// The property returning only existing tab is NTP for current mode var isBrowserEmptyForCurrentMode: Bool { @@ -106,13 +109,15 @@ class TabManager: NSObject { } } - init(prefs: Prefs, rewards: BraveRewards?, tabGeneratorAPI: BraveTabGeneratorAPI?) { + init(windowId: UUID, prefs: Prefs, rewards: BraveRewards?, tabGeneratorAPI: BraveTabGeneratorAPI?, privateBrowsingManager: PrivateBrowsingManager) { assert(Thread.isMainThread) + self.windowId = windowId self.prefs = prefs self.navDelegate = TabManagerNavDelegate() self.rewards = rewards self.tabGeneratorAPI = tabGeneratorAPI + self.privateBrowsingManager = privateBrowsingManager self.tabEventHandlers = TabEventHandlers.create(with: prefs) super.init() @@ -192,7 +197,7 @@ class TabManager: NSObject { // What the users sees displayed based on current private browsing mode var tabsForCurrentMode: [Tab] { - let tabType: TabType = PrivateBrowsingManager.shared.isPrivateBrowsing ? .private : .regular + let tabType: TabType = privateBrowsingManager.isPrivateBrowsing ? .private : .regular return tabs(withType: tabType) } @@ -207,7 +212,7 @@ class TabManager: NSObject { func tabsForCurrentMode(for query: String? = nil) -> [Tab] { if let query = query { - let tabType: TabType = PrivateBrowsingManager.shared.isPrivateBrowsing ? .private : .regular + let tabType: TabType = privateBrowsingManager.isPrivateBrowsing ? .private : .regular return tabs(withType: tabType, query: query) } else { return tabsForCurrentMode @@ -299,11 +304,11 @@ class TabManager: NSObject { return } // Convert the global mode to private if opening private tab from normal tab/ history/bookmark. - if selectedTab?.isPrivate == false && tab?.isPrivate == true { - PrivateBrowsingManager.shared.isPrivateBrowsing = true + if selectedTab?.isPrivate != true && tab?.isPrivate == true { + privateBrowsingManager.isPrivateBrowsing = true } // Make sure to wipe the private tabs if the user has the pref turned on - if !TabType.of(tab).isPrivate { + if !TabType.of(tab).isPrivate && (Preferences.Privacy.privateBrowsingOnly.value || !Preferences.Privacy.persistentPrivateBrowsing.value) { removeAllPrivateTabs() } @@ -356,7 +361,7 @@ class TabManager: NSObject { guard let newSelectedTab = tab, let previousTab = previous, let newTabUrl = newSelectedTab.url, let previousTabUrl = previousTab.url else { return } - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !privateBrowsingManager.isPrivateBrowsing { if previousTab.displayFavicon == nil { adsRewardsLog.warning("No favicon found in \(previousTab) to report to rewards panel") } @@ -378,7 +383,9 @@ class TabManager: NSObject { // we only want to remove all private tabs when leaving PBM and not when entering. func willSwitchTabMode(leavingPBM: Bool) { if leavingPBM { - removeAllPrivateTabs() + if Preferences.Privacy.privateBrowsingOnly.value || !Preferences.Privacy.persistentPrivateBrowsing.value { + removeAllPrivateTabs() + } } } @@ -495,7 +502,7 @@ class TabManager: NSObject { } private func saveTabOrder() { - if PrivateBrowsingManager.shared.isPrivateBrowsing { return } + if Preferences.Privacy.privateBrowsingOnly.value || (privateBrowsingManager.isPrivateBrowsing && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } let allTabIds = allTabs.compactMap { $0.id } SessionTab.saveTabOrder(tabIds: allTabIds) } @@ -504,10 +511,14 @@ class TabManager: NSObject { assert(Thread.isMainThread) let isPrivate = tab.type == .private - if !isPrivate { - SessionTab.createIfNeeded(tabId: tab.id, + let isPersistentTab = !isPrivate || (isPrivate && !Preferences.Privacy.privateBrowsingOnly.value && Preferences.Privacy.persistentPrivateBrowsing.value) + + if isPersistentTab { + SessionTab.createIfNeeded(windowId: windowId, + tabId: tab.id, title: Strings.newTab, - tabURL: request?.url ?? TabManager.ntpInteralURL) + tabURL: request?.url ?? TabManager.ntpInteralURL, + isPrivate: isPrivate) } delegates.forEach { $0.get()?.tabManager(self, willAddTab: tab) } @@ -539,7 +550,7 @@ class TabManager: NSObject { } // Ignore on restore. - if flushToDisk && !zombie && !isPrivate { + if flushToDisk && !zombie && isPersistentTab { saveTab(tab, saveOrder: true) } @@ -554,11 +565,13 @@ class TabManager: NSObject { tab.webStateDebounceTimer?.invalidate() if state == .complete || state == .loaded || state == .pushstate || state == .popstate || state == .replacestate { - // Saving Tab Private Mode - not supported yet. - if !tab.isPrivate { - self.preserveScreenshots() - self.saveTab(tab) + + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { + return } + + self.preserveScreenshots() + self.saveTab(tab) } } } @@ -579,8 +592,10 @@ class TabManager: NSObject { } func saveAllTabs() { - if PrivateBrowsingManager.shared.isPrivateBrowsing { return } - SessionTab.updateAll(tabs: tabs(withType: .regular).compactMap({ + if Preferences.Privacy.privateBrowsingOnly.value || (privateBrowsingManager.isPrivateBrowsing && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } + + let tabs = Preferences.Privacy.persistentPrivateBrowsing.value ? allTabs : tabs(withType: .regular) + SessionTab.updateAll(tabs: tabs.compactMap({ if let sessionData = $0.webView?.sessionData { return ($0.id, sessionData, $0.title, $0.url ?? TabManager.ntpInteralURL) } @@ -589,7 +604,7 @@ class TabManager: NSObject { } func saveTab(_ tab: Tab, saveOrder: Bool = false) { - if PrivateBrowsingManager.shared.isPrivateBrowsing { return } + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } SessionTab.update(tabId: tab.id, interactionState: tab.webView?.sessionData ?? Data(), title: tab.title, url: tab.url ?? TabManager.ntpInteralURL) if saveOrder { saveTabOrder() @@ -615,7 +630,6 @@ class TabManager: NSObject { // reaches zero and destroys all its data. BraveWebView.removeNonPersistentStore() - configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() } } @@ -911,6 +925,7 @@ class TabManager: NSObject { savedTabs = SessionTab.all() } + savedTabs = savedTabs.filter({ $0.sessionWindow?.windowId == windowId }) if savedTabs.isEmpty { return nil } var tabToSelect: Tab? @@ -925,20 +940,19 @@ class TabManager: NSObject { flushToDisk: false, zombie: true, id: savedTab.tabId, - isPrivate: false) - - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + isPrivate: savedTab.isPrivate) tab.lastTitle = savedTab.title tab.favicon = FaviconFetcher.getIconFromCache(for: tabURL) ?? Favicon.default tab.setScreenshot(savedTab.screenshot) Task { @MainActor in - tab.favicon = try await FaviconFetcher.loadIcon(url: tabURL, kind: .smallIcon, persistent: !isPrivateBrowsing) + tab.favicon = try await FaviconFetcher.loadIcon(url: tabURL, kind: .smallIcon, persistent: !tab.isPrivate) tab.setScreenshot(savedTab.screenshot) } - if savedTab.isSelected { + // Do not select the private tab since we always restore to regular mode! + if savedTab.isSelected && !savedTab.isPrivate { tabToSelect = tab } } @@ -1161,8 +1175,11 @@ extension TabManager: WKNavigationDelegate { return } - // Saving Tab Private Mode - not supported yet. - if let tab = tabForWebView(webView), !tab.isPrivate { + if let tab = tabForWebView(webView) { + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { + return + } + preserveScreenshots() saveTab(tab) } diff --git a/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift b/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift index b7fa66e9258..357944324ae 100644 --- a/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift +++ b/Sources/Brave/Frontend/Browser/TabManagerNavDelegate.swift @@ -68,7 +68,7 @@ class TabManagerNavDelegate: NSObject, WKNavigationDelegate { } private func defaultAllowPolicy() -> WKNavigationActionPolicy { - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = tabManager?.privateBrowsingManager.isPrivateBrowsing == true if isPrivateBrowsing || !Preferences.General.followUniversalLinks.value { // Stop Brave from opening universal links by using the private enum value // `_WKNavigationActionPolicyAllowWithoutTryingAppLink` which is defined here: diff --git a/Sources/Brave/Frontend/Browser/Tabs/RecentlyClosedTabs/RecentlyClosedTabsView.swift b/Sources/Brave/Frontend/Browser/Tabs/RecentlyClosedTabs/RecentlyClosedTabsView.swift index 5a08cb5d6e8..247608a6ba3 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/RecentlyClosedTabs/RecentlyClosedTabsView.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/RecentlyClosedTabs/RecentlyClosedTabsView.swift @@ -57,7 +57,7 @@ struct RecentlyClosedTabsView: View { onRecentlyClosedSelected?(recentlyClosed) }) { HStack { - FaviconImage(url: recentlyClosed.url) + FaviconImage(url: recentlyClosed.url, isPrivateBrowsing: tabManager?.privateBrowsingManager.isPrivateBrowsing == true) VStack(alignment: .leading) { Text(recentlyClosed.title ?? "") .font(.footnote.weight(.semibold)) diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabBarCell.swift b/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabBarCell.swift index 6df2d9ffb35..0273eb7d4ee 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabBarCell.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabBarCell.swift @@ -39,7 +39,23 @@ class TabBarCell: UICollectionViewCell { } } weak var tab: Tab? - weak var tabManager: TabManager? + weak var tabManager: TabManager? { + didSet { + privateModeCancellable = tabManager?.privateBrowsingManager + .$isPrivateBrowsing + .removeDuplicates() + .sink(receiveValue: { [weak self] isPrivateBrowsing in + self?.updateColors(isPrivateBrowsing) + }) + + Preferences.General.nightModeEnabled.objectWillChange + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.updateColors(self?.tabManager?.privateBrowsingManager.isPrivateBrowsing == true) + } + .store(in: &cancellables) + } + } var closeTabCallback: ((Tab) -> Void)? private var cancellables: Set = [] @@ -62,19 +78,6 @@ class TabBarCell: UICollectionViewCell { updateFont() isSelected = false - privateModeCancellable = PrivateBrowsingManager.shared - .$isPrivateBrowsing - .removeDuplicates() - .sink(receiveValue: { [weak self] isPrivateBrowsing in - self?.updateColors(isPrivateBrowsing) - }) - - Preferences.General.nightModeEnabled.objectWillChange - .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateColors(PrivateBrowsingManager.shared.isPrivateBrowsing) - } - .store(in: &cancellables) } private var privateModeCancellable: AnyCancellable? diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabsBarViewController.swift b/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabsBarViewController.swift index 5f046eac2f4..6264b8c2fc6 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabsBarViewController.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabBar/TabsBarViewController.swift @@ -14,7 +14,7 @@ protocol TabsBarViewControllerDelegate: AnyObject { func tabsBarDidLongPressAddTab(_ tabsBarController: TabsBarViewController, button: UIButton) func tabsBarDidSelectAddNewTab(_ isPrivate: Bool) func tabsBarDidChangeReaderModeVisibility(_ isHidden: Bool) - + func tabsBarDidSelectAddNewWindow(_ isPrivate: Bool) } class TabsBarViewController: UIViewController { @@ -70,6 +70,8 @@ class TabsBarViewController: UIViewController { view.backgroundColor = Preferences.General.nightModeEnabled.value ? .nightModeBackground : .urlBarBackground collectionView.backgroundColor = view.backgroundColor + collectionView.dragDelegate = UIApplication.shared.supportsMultipleScenes ? self : nil + collectionView.dropDelegate = UIApplication.shared.supportsMultipleScenes ? self : nil tabManager?.addDelegate(self) @@ -100,8 +102,9 @@ class TabsBarViewController: UIViewController { } var newTabMenu: [UIAction] = [] + let isPrivateBrowsing = tabManager?.privateBrowsingManager.isPrivateBrowsing == true - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !isPrivateBrowsing { let openNewPrivateTab = UIAction( title: Strings.Hotkey.newPrivateTabTitle, image: UIImage(systemName: "plus.square.fill.on.square.fill"), @@ -113,16 +116,24 @@ class TabsBarViewController: UIViewController { } let openNewTab = UIAction( - title: PrivateBrowsingManager.shared.isPrivateBrowsing ? Strings.Hotkey.newPrivateTabTitle : Strings.Hotkey.newTabTitle, - image: PrivateBrowsingManager.shared.isPrivateBrowsing ? UIImage(systemName: "plus.square.fill.on.square.fill") : UIImage(systemName: "plus.square.on.square"), + title: isPrivateBrowsing ? Strings.Hotkey.newPrivateTabTitle : Strings.Hotkey.newTabTitle, + image: isPrivateBrowsing ? UIImage(systemName: "plus.square.fill.on.square.fill") : UIImage(systemName: "plus.square.on.square"), handler: UIAction.deferredActionHandler { [unowned self] _ in - self.delegate?.tabsBarDidSelectAddNewTab(PrivateBrowsingManager.shared.isPrivateBrowsing) + self.delegate?.tabsBarDidSelectAddNewTab(isPrivateBrowsing) }) newTabMenu.append(openNewTab) + + newTabMenu.append(UIAction(title: Strings.newWindowTitle, image: UIImage(braveSystemNamed: "leo.window"), handler: UIAction.deferredActionHandler { [unowned self] _ in + self.delegate?.tabsBarDidSelectAddNewWindow(false) + })) + + newTabMenu.append(UIAction(title: Strings.newPrivateWindowTitle, image: UIImage(braveSystemNamed: "leo.window.tab-private"), handler: UIAction.deferredActionHandler { [unowned self] _ in + self.delegate?.tabsBarDidSelectAddNewWindow(true) + })) plusButton.menu = UIMenu(title: "", identifier: nil, children: newTabMenu) - privateModeCancellable = PrivateBrowsingManager.shared + privateModeCancellable = tabManager?.privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -132,7 +143,7 @@ class TabsBarViewController: UIViewController { Preferences.General.nightModeEnabled.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.updateColors(PrivateBrowsingManager.shared.isPrivateBrowsing) + self?.updateColors(self?.tabManager?.privateBrowsingManager.isPrivateBrowsing == true) } .store(in: &cancellables) } @@ -199,7 +210,7 @@ class TabsBarViewController: UIViewController { } @objc func addTabPressed() { - tabManager?.addTabAndSelect(isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing) + tabManager?.addTabAndSelect(isPrivate: tabManager?.privateBrowsingManager.isPrivateBrowsing == true) } @objc private func didLongPressAddTab(_ longPress: UILongPressGestureRecognizer) { @@ -423,6 +434,54 @@ extension TabsBarViewController: UICollectionViewDataSource { } } +// MARK: - UICollectionViewDragDelegate +extension TabsBarViewController: UICollectionViewDragDelegate, UICollectionViewDropDelegate { + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + guard let tab = tabList[indexPath.row], + let windowId = tabManager?.windowId else { + return [] + } + + let userActivity = NSUserActivity(activityType: BrowserState.sceneId).then { + $0.addUserInfoEntries(from: [ + "TabID": tab.id.uuidString, + "TabWindowID": windowId.uuidString, + "isPrivate": tab.isPrivate + ]) + } + + let itemProvider = NSItemProvider(object: userActivity) + itemProvider.registerObject(userActivity, visibility: .all) + return [UIDragItem(itemProvider: itemProvider)] + } + + func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { + guard coordinator.items.first?.sourceIndexPath == nil else { return } + let destinationIndexPath: IndexPath + + if let indexPath = coordinator.destinationIndexPath { + destinationIndexPath = indexPath + } else { + let section = max(collectionView.numberOfSections - 1, 0) + let row = collectionView.numberOfItems(inSection: section) + destinationIndexPath = IndexPath(row: max(row - 1, 0), section: section) + } + + if coordinator.proposal.operation == .move { + guard let item = coordinator.items.first else { return } + + // TODO: Figure out how to get the item here... + // LocalObject is nil because the tab is in another process? :S + + _ = coordinator.drop(item.dragItem, toItemAt: destinationIndexPath) + } + } + + func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { + return .init(operation: .move, intent: .insertAtDestinationIndexPath) + } +} + // MARK: - TabManagerDelegate extension TabsBarViewController: TabManagerDelegate { func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) { diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+KeyCommands.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+KeyCommands.swift index 53ef5ed254f..5c5d947e8ea 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+KeyCommands.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+KeyCommands.swift @@ -105,7 +105,7 @@ extension TabTrayController { UIKeyCommand(title: Strings.Hotkey.openNewTabFromTabTrayKeyCodeTitle, action: #selector(didOpenNewTabKeyCommand), input: "t", modifierFlags: .command) ] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !tabManager.privateBrowsingManager.isPrivateBrowsing { navigationCommands += [ UIKeyCommand(title: Strings.Hotkey.recentlyClosedTabTitle, action: #selector(reopenRecentlyClosedTabCommand), input: "t", modifierFlags: [.command, .shift]) ] diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+TableViewDelegate.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+TableViewDelegate.swift index cc4ecb978cc..19c053f2dac 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+TableViewDelegate.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController+TableViewDelegate.swift @@ -50,7 +50,7 @@ extension TabTrayController: UITableViewDataSource, UITableViewDelegate { $0.layer.cornerRadius = 6 $0.layer.cornerCurve = .continuous $0.layer.masksToBounds = true - $0.loadFavicon(for: distantTab.url) + $0.loadFavicon(for: distantTab.url, isPrivateBrowsing: tabManager.privateBrowsingManager.isPrivateBrowsing) } } diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift index e5cd30b65d6..cfd6282b0bb 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift @@ -85,7 +85,7 @@ class TabTrayController: LoadingViewController { private(set) var privateMode: Bool = false { didSet { // Should be set immediately before other logic executes - PrivateBrowsingManager.shared.isPrivateBrowsing = privateMode + tabManager.privateBrowsingManager.isPrivateBrowsing = privateMode applySnapshot() privateModeButton.isSelected = privateMode @@ -176,7 +176,7 @@ class TabTrayController: LoadingViewController { overlayDetails: EmptyOverlayStateDetails(title: Strings.noSearchResultsfound)) override var preferredStatusBarStyle: UIStatusBarStyle { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if tabManager.privateBrowsingManager.isPrivateBrowsing { return .lightContent } @@ -298,7 +298,7 @@ class TabTrayController: LoadingViewController { UIBarButtonItem(customView: doneButton), ] - privateModeCancellable = PrivateBrowsingManager.shared + privateModeCancellable = tabManager.privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -418,7 +418,7 @@ class TabTrayController: LoadingViewController { } override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - guard !PrivateBrowsingManager.shared.isPrivateBrowsing, let recentlyClosedTab = RecentlyClosed.all().first else { + guard !tabManager.privateBrowsingManager.isPrivateBrowsing, let recentlyClosedTab = RecentlyClosed.all().first else { return } @@ -568,7 +568,7 @@ class TabTrayController: LoadingViewController { } @objc private func tappedButton(_ gestureRecognizer: UIGestureRecognizer) { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if tabManager.privateBrowsingManager.isPrivateBrowsing { return } @@ -609,10 +609,21 @@ class TabTrayController: LoadingViewController { tabTypeSelector.selectedSegmentIndex = 0 tabTypeSelector.sendActions(for: UIControl.Event.valueChanged) - tabTrayView.showPrivateModeInfo() - // New private tab is created immediately to reflect changes on NTP. - // If user drags the modal down or dismisses it, a new private tab will be ready. - tabManager.addTabAndSelect(isPrivate: true) + if !Preferences.Privacy.persistentPrivateBrowsing.value || tabManager.tabsForCurrentMode.isEmpty { + tabTrayView.showPrivateModeInfo() + // New private tab is created immediately to reflect changes on NTP. + // If user drags the modal down or dismisses it, a new private tab will be ready. + tabManager.addTabAndSelect(isPrivate: true) + navigationController?.setNavigationBarHidden(true, animated: false) + } else { + if tabManager.tabsForCurrentMode.isEmpty { + tabManager.addTabAndSelect(isPrivate: true) + } + + tabTrayView.hidePrivateModeInfo() + tabTrayView.collectionView.reloadData() + navigationController?.setNavigationBarHidden(false, animated: false) + } } else { tabTrayView.hidePrivateModeInfo() @@ -620,11 +631,10 @@ class TabTrayController: LoadingViewController { // Reloding the collection view in order to mark the selecte the tab tabManager.selectTab(tabManager.tabsForCurrentMode[safe: tabManager.normalTabSelectedIndex]) tabTrayView.collectionView.reloadData() + navigationController?.setNavigationBarHidden(false, animated: false) } - navigationController?.setNavigationBarHidden(privateMode, animated: false) tabTypeSelector.isHidden = privateMode - } func remove(tab: Tab) { diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/Views/TabTrayCell.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/Views/TabTrayCell.swift index 0cb35657b94..a6ff3538714 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/Views/TabTrayCell.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/Views/TabTrayCell.swift @@ -77,7 +77,7 @@ class TabCell: UICollectionViewCell { if let displayFavicon = tab.displayFavicon { favicon.image = displayFavicon.image ?? Favicon.defaultImage } else if let url = tab.url, !url.isLocal, !InternalURL.isValid(url: url) { - favicon.loadFavicon(for: url) + favicon.loadFavicon(for: url, isPrivateBrowsing: tab.isPrivate) } else { favicon.image = Favicon.defaultImage } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift index dc6c6f33f11..6eccfcf63c7 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/BottomToolbarView.swift @@ -26,10 +26,12 @@ class BottomToolbarView: UIView, ToolbarProtocol { private let contentView = UIStackView() private var cancellables: Set = [] let line = UIView.separatorLine + private let privateBrowsingManager: PrivateBrowsingManager - fileprivate override init(frame: CGRect) { + init(privateBrowsingManager: PrivateBrowsingManager) { + self.privateBrowsingManager = privateBrowsingManager actionButtons = [backButton, forwardButton, addTabButton, searchButton, tabsButton, menuButton] - super.init(frame: frame) + super.init(frame: .zero) setupAccessibility() backgroundColor = Preferences.General.nightModeEnabled.value ? .nightModeBackground : .urlBarBackground @@ -48,7 +50,7 @@ class BottomToolbarView: UIView, ToolbarProtocol { $0.leading.trailing.equalToSuperview() } - privateModeCancellable = PrivateBrowsingManager.shared + privateModeCancellable = privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -58,7 +60,7 @@ class BottomToolbarView: UIView, ToolbarProtocol { Preferences.General.nightModeEnabled.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.updateColors(PrivateBrowsingManager.shared.isPrivateBrowsing) + self?.updateColors(privateBrowsingManager.isPrivateBrowsing) } .store(in: &cancellables) diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/AddEditBookmarkTableViewController.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/AddEditBookmarkTableViewController.swift index eeb428b73ee..d444799ce13 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/AddEditBookmarkTableViewController.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/AddEditBookmarkTableViewController.swift @@ -49,11 +49,11 @@ class AddEditBookmarkTableViewController: UITableViewController { private lazy var bookmarkDetailsView: BookmarkFormFieldsProtocol = { switch mode { case .addBookmark(let title, let url): - return BookmarkDetailsView(title: title, url: url) + return BookmarkDetailsView(title: title, url: url, isPrivateBrowsing: isPrivateBrowsing) case .addFolder(let title), .addFolderUsingTabs(title: let title, _): return FolderDetailsViewTableViewCell(title: title, viewHeight: UX.cellHeight) case .editBookmark(let bookmark), .editFavorite(let bookmark): - return BookmarkDetailsView(title: bookmark.title, url: bookmark.url) + return BookmarkDetailsView(title: bookmark.title, url: bookmark.url, isPrivateBrowsing: isPrivateBrowsing) case .editFolder(let folder): return FolderDetailsViewTableViewCell(title: folder.title, viewHeight: UX.cellHeight) } @@ -150,10 +150,12 @@ class AddEditBookmarkTableViewController: UITableViewController { private var saveLocation: BookmarkSaveLocation private var rootFolderName: String private var rootFolderId: Int = 0 // MobileBookmarks Folder Id + private var isPrivateBrowsing: Bool - init(bookmarkManager: BookmarkManager, mode: BookmarkEditMode) { + init(bookmarkManager: BookmarkManager, mode: BookmarkEditMode, isPrivateBrowsing: Bool) { self.bookmarkManager = bookmarkManager self.mode = mode + self.isPrivateBrowsing = isPrivateBrowsing saveLocation = mode.initialSaveLocation presentationMode = .currentSelection @@ -371,7 +373,7 @@ class AddEditBookmarkTableViewController: UITableViewController { isLoading = true for tab in tabs { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if tab.isPrivate { if let url = tab.url, url.isWebPage(), !(InternalURL(url)?.isAboutHomeURL ?? false) { bookmarkManager.add(url: url, title: tab.title, parentFolder: parentFolder) } @@ -437,7 +439,9 @@ class AddEditBookmarkTableViewController: UITableViewController { } private func showNewFolderVC() { - let vc = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: .addFolder(title: Strings.newFolderDefaultName)) + let vc = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, + mode: .addFolder(title: Strings.newFolderDefaultName), + isPrivateBrowsing: isPrivateBrowsing) navigationController?.pushViewController(vc, animated: true) } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarkDetailsView.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarkDetailsView.swift index ec422c8b7ad..fa07d94e89c 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarkDetailsView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarkDetailsView.swift @@ -52,7 +52,7 @@ class BookmarkDetailsView: AddEditHeaderView, BookmarkFormFieldsProtocol { // MARK: - Initialization - convenience init(title: String?, url: String?) { + convenience init(title: String?, url: String?, isPrivateBrowsing: Bool) { self.init(frame: .zero) backgroundColor = .secondaryBraveGroupedBackground @@ -76,7 +76,7 @@ class BookmarkDetailsView: AddEditHeaderView, BookmarkFormFieldsProtocol { if url?.isBookmarklet == true { url = url?.removingPercentEncoding } else if let url = url, let favUrl = URL(string: url) { - faviconImageView.loadFavicon(siteURL: favUrl) + faviconImageView.loadFavicon(siteURL: favUrl, isPrivateBrowsing: isPrivateBrowsing) } titleTextField.text = title ?? Strings.newBookmarkDefaultName diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift index c89412ca5b6..8ae67253b2f 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift @@ -417,7 +417,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco cell.imageView?.clearMonogramFavicon() if let urlString = item.url, let url = URL(string: urlString) { - cell.imageView?.loadFavicon(for: url) { [weak cell] favicon in + cell.imageView?.loadFavicon(for: url, isPrivateBrowsing: isPrivateBrowsing) { [weak cell] favicon in if favicon?.isMonogramImage == true, let icon = item.bookmarkNode.icon { cell?.imageView?.image = icon } @@ -614,7 +614,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco var newTabActionMenu: [UIAction] = [openInNewTabAction] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !isPrivateBrowsing { newTabActionMenu.append(newPrivateTabAction) } @@ -701,7 +701,7 @@ extension BookmarksViewController { } if let mode = mode { - let vc = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode) + let vc = AddEditBookmarkTableViewController(bookmarkManager: bookmarkManager, mode: mode, isPrivateBrowsing: isPrivateBrowsing) self.navigationController?.pushViewController(vc, animated: true) } } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/HistoryViewController.swift b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/HistoryViewController.swift index 32d9990386f..4477c39f12d 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/HistoryViewController.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/HistoryViewController.swift @@ -240,10 +240,10 @@ class HistoryViewController: SiteTableViewController, ToolbarUrlActionsProtocol let domain = Domain.getOrCreate( forUrl: historyItem.url, - persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + persistent: !isPrivateBrowsing) if domain.url?.asURL != nil { - cell.imageView?.loadFavicon(for: historyItem.url) + cell.imageView?.loadFavicon(for: historyItem.url, isPrivateBrowsing: isPrivateBrowsing) } else { cell.imageView?.clearMonogramFavicon() cell.imageView?.image = Favicon.defaultImage @@ -340,7 +340,7 @@ class HistoryViewController: SiteTableViewController, ToolbarUrlActionsProtocol var newTabActionMenu: [UIAction] = [openInNewTabAction] - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !isPrivateBrowsing { newTabActionMenu.append(newPrivateTabAction) } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift b/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift index e08c09a52a5..10f0d5929c7 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/HeaderContainerView.swift @@ -32,8 +32,8 @@ class HeaderContainerView: UIView { private var cancellables: Set = [] - override init(frame: CGRect) { - super.init(frame: frame) + init(privateBrowsingManager: PrivateBrowsingManager) { + super.init(frame: .zero) addSubview(contentView) contentView.addSubview(expandedBarStackView) @@ -51,7 +51,7 @@ class HeaderContainerView: UIView { $0.edges.equalToSuperview() } - PrivateBrowsingManager.shared + privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -62,7 +62,7 @@ class HeaderContainerView: UIView { Preferences.General.nightModeEnabled.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.updateColors(PrivateBrowsingManager.shared.isPrivateBrowsing) + self?.updateColors(privateBrowsingManager.isPrivateBrowsing) } .store(in: &cancellables) } diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift index 0ebfbbede90..1fb90bcba3d 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TabLocationView.swift @@ -251,7 +251,7 @@ class TabLocationView: UIView { private var isVoiceSearchAvailable: Bool - init(voiceSearchSupported: Bool) { + init(voiceSearchSupported: Bool, privateBrowsingManager: PrivateBrowsingManager) { isVoiceSearchAvailable = voiceSearchSupported super.init(frame: .zero) @@ -306,7 +306,7 @@ class TabLocationView: UIView { dragInteraction.allowsSimultaneousRecognitionDuringLift = true self.addInteraction(dragInteraction) - privateModeCancellable = PrivateBrowsingManager.shared.$isPrivateBrowsing + privateModeCancellable = privateBrowsingManager.$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in self?.updateColors(isPrivateBrowsing) diff --git a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift index 2aab9368269..1b3fc2fa259 100644 --- a/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift +++ b/Sources/Brave/Frontend/Browser/Toolbars/UrlBar/TopToolbarView.swift @@ -70,6 +70,7 @@ class TopToolbarView: UIView, ToolbarProtocol { private var cancellables: Set = [] private var privateModeCancellable: AnyCancellable? + private let privateBrowsingManager: PrivateBrowsingManager private(set) var displayTabTraySwipeGestureRecognizer: UISwipeGestureRecognizer? @@ -119,7 +120,7 @@ class TopToolbarView: UIView, ToolbarProtocol { private var locationTextField: AutocompleteTextField? - lazy var locationView = TabLocationView(voiceSearchSupported: isVoiceSearchAvailable).then { + lazy var locationView = TabLocationView(voiceSearchSupported: isVoiceSearchAvailable, privateBrowsingManager: privateBrowsingManager).then { $0.translatesAutoresizingMaskIntoConstraints = false $0.readerModeState = ReaderModeState.unavailable $0.delegate = self @@ -245,8 +246,9 @@ class TopToolbarView: UIView, ToolbarProtocol { // MARK: Lifecycle - init(voiceSearchSupported: Bool) { + init(voiceSearchSupported: Bool, privateBrowsingManager: PrivateBrowsingManager) { isVoiceSearchAvailable = voiceSearchSupported + self.privateBrowsingManager = privateBrowsingManager super.init(frame: .zero) @@ -293,7 +295,7 @@ class TopToolbarView: UIView, ToolbarProtocol { // Make sure we hide any views that shouldn't be showing in non-overlay mode. updateViewsForOverlayModeAndToolbarChanges() - privateModeCancellable = PrivateBrowsingManager.shared + privateModeCancellable = privateBrowsingManager .$isPrivateBrowsing .removeDuplicates() .sink(receiveValue: { [weak self] isPrivateBrowsing in @@ -303,7 +305,7 @@ class TopToolbarView: UIView, ToolbarProtocol { Preferences.General.nightModeEnabled.objectWillChange .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.updateColors(PrivateBrowsingManager.shared.isPrivateBrowsing) + self?.updateColors(privateBrowsingManager.isPrivateBrowsing) } .store(in: &cancellables) @@ -652,7 +654,7 @@ class TopToolbarView: UIView, ToolbarProtocol { var shieldIcon = "brave.logo" let shieldsOffIcon = "brave.logo.greyscale" if let currentURL = currentURL { - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing let domain = Domain.getOrCreate(forUrl: currentURL, persistent: !isPrivateBrowsing) if domain.areAllShieldsOff { shieldIcon = shieldsOffIcon diff --git a/Sources/Brave/Frontend/ClientPreferences.swift b/Sources/Brave/Frontend/ClientPreferences.swift index a32cbeeec4a..57432a61399 100644 --- a/Sources/Brave/Frontend/ClientPreferences.swift +++ b/Sources/Brave/Frontend/ClientPreferences.swift @@ -125,6 +125,8 @@ extension Preferences { static let lockWithPasscode = Option(key: "privacy.lock-with-passcode", default: false) /// Forces all private tabs public static let privateBrowsingOnly = Option(key: "privacy.private-only", default: false) + /// Whether or not private browsing tabs can be session restored (persistent private browsing) + public static let persistentPrivateBrowsing = Option(key: "privacy.private-browsing-persistence", default: false) /// Blocks all cookies and access to local storage static let blockAllCookies = Option(key: "privacy.block-all-cookies", default: false) /// The toggles states for clear private data screen diff --git a/Sources/Brave/Frontend/Login/LoginListViewController.swift b/Sources/Brave/Frontend/Login/LoginListViewController.swift index 49ff9940926..6d762ec4f02 100644 --- a/Sources/Brave/Frontend/Login/LoginListViewController.swift +++ b/Sources/Brave/Frontend/Login/LoginListViewController.swift @@ -212,7 +212,7 @@ extension LoginListViewController { $0.layer.cornerCurve = .continuous $0.layer.masksToBounds = true if let signOnRealmURL = URL(string: loginInfo.signOnRealm) { - $0.loadFavicon(for: signOnRealmURL) + $0.loadFavicon(for: signOnRealmURL, isPrivateBrowsing: false) } } diff --git a/Sources/Brave/Frontend/Rewards/BraveRewards.swift b/Sources/Brave/Frontend/Rewards/BraveRewards.swift index e70e29f0e88..a7a00c9bbd3 100644 --- a/Sources/Brave/Frontend/Rewards/BraveRewards.swift +++ b/Sources/Brave/Frontend/Rewards/BraveRewards.swift @@ -306,7 +306,7 @@ extension BraveRewards { case staging } - var storageURL: URL + public var storageURL: URL public var environment: Environment public var adsBuildChannel: BraveAds.BuildChannelInfo = .init() public var isDebug: Bool? diff --git a/Sources/Brave/Frontend/Settings/SearchCustomEngineViewController.swift b/Sources/Brave/Frontend/Settings/SearchCustomEngineViewController.swift index 40d6aca27de..a05b903e573 100644 --- a/Sources/Brave/Frontend/Settings/SearchCustomEngineViewController.swift +++ b/Sources/Brave/Frontend/Settings/SearchCustomEngineViewController.swift @@ -41,6 +41,7 @@ class SearchCustomEngineViewController: UIViewController { // MARK: Properties private var profile: Profile + private var privateBrowsingManager: PrivateBrowsingManager private var urlText: String? @@ -84,8 +85,9 @@ class SearchCustomEngineViewController: UIViewController { // MARK: Lifecycle - init(profile: Profile) { + init(profile: Profile, privateBrowsingManager: PrivateBrowsingManager) { self.profile = profile + self.privateBrowsingManager = privateBrowsingManager super.init(nibName: nil, bundle: nil) } @@ -170,7 +172,7 @@ class SearchCustomEngineViewController: UIViewController { // MARK: Actions - @objc func checkAddEngineType(_ nav: UINavigationController?) { + @objc func checkAddEngineType() { if isAutoAddEnabled { addOpenSearchEngine() } else { @@ -392,7 +394,7 @@ extension SearchCustomEngineViewController { faviconTask?.cancel() faviconTask = Task { @MainActor in do { - let icon = try await FaviconFetcher.loadIcon(url: url, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let icon = try await FaviconFetcher.loadIcon(url: url, persistent: !privateBrowsingManager.isPrivateBrowsing) self.faviconImage = icon.image ?? Favicon.defaultImage } catch { self.faviconImage = Favicon.defaultImage @@ -474,7 +476,7 @@ extension SearchCustomEngineViewController { faviconTask?.cancel() faviconTask = Task { @MainActor in - let favicon = try? await FaviconFetcher.loadIcon(url: hostUrl, persistent: !PrivateBrowsingManager.shared.isPrivateBrowsing) + let favicon = try? await FaviconFetcher.loadIcon(url: hostUrl, persistent: !privateBrowsingManager.isPrivateBrowsing) if let image = favicon?.image { engineImage = image } diff --git a/Sources/Brave/Frontend/Settings/SearchQuickEnginesViewController.swift b/Sources/Brave/Frontend/Settings/SearchQuickEnginesViewController.swift index 439584771fb..85ac796cf25 100644 --- a/Sources/Brave/Frontend/Settings/SearchQuickEnginesViewController.swift +++ b/Sources/Brave/Frontend/Settings/SearchQuickEnginesViewController.swift @@ -28,11 +28,13 @@ class SearchQuickEnginesViewController: UITableViewController { private var searchEngines: SearchEngines private let profile: Profile + private let isPrivateBrowsing: Bool // MARK: Lifecycle - init(profile: Profile) { + init(profile: Profile, isPrivateBrowsing: Bool) { self.profile = profile + self.isPrivateBrowsing = isPrivateBrowsing self.searchEngines = profile.searchEngines super.init(nibName: nil, bundle: nil) } @@ -142,7 +144,7 @@ extension SearchQuickEnginesViewController { if toggle.isOn { searchEngines.enableEngine(engine) } else { - searchEngines.disableEngine(engine) + searchEngines.disableEngine(engine, type: isPrivateBrowsing ? .privateMode : .standard) } } } diff --git a/Sources/Brave/Frontend/Settings/SearchSettingsTableViewController.swift b/Sources/Brave/Frontend/Settings/SearchSettingsTableViewController.swift index c33cf2fe9fb..f20bb01de8a 100644 --- a/Sources/Brave/Frontend/Settings/SearchSettingsTableViewController.swift +++ b/Sources/Brave/Frontend/Settings/SearchSettingsTableViewController.swift @@ -59,6 +59,7 @@ class SearchSettingsTableViewController: UITableViewController { private var searchEngines: SearchEngines private let profile: Profile private var showDeletion = false + private var privateBrowsingManager: PrivateBrowsingManager private func searchPickerEngines(type: DefaultEngineType) -> [OpenSearchEngine] { let isPrivate = type == .privateMode @@ -84,8 +85,9 @@ class SearchSettingsTableViewController: UITableViewController { // MARK: Lifecycle - init(profile: Profile) { + init(profile: Profile, privateBrowsingManager: PrivateBrowsingManager) { self.profile = profile + self.privateBrowsingManager = privateBrowsingManager self.searchEngines = profile.searchEngines super.init(nibName: nil, bundle: nil) } @@ -283,10 +285,10 @@ class SearchSettingsTableViewController: UITableViewController { } else if indexPath.section == Section.current.rawValue && indexPath.item == CurrentEngineType.private.rawValue { navigationController?.pushViewController(configureSearchEnginePicker(.privateMode), animated: true) } else if indexPath.section == Section.current.rawValue && indexPath.item == CurrentEngineType.quick.rawValue { - let quickSearchEnginesViewController = SearchQuickEnginesViewController(profile: profile) + let quickSearchEnginesViewController = SearchQuickEnginesViewController(profile: profile, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) navigationController?.pushViewController(quickSearchEnginesViewController, animated: true) } else if indexPath.section == Section.customSearch.rawValue && indexPath.item == customSearchEngines.count { - let customEngineViewController = SearchCustomEngineViewController(profile: profile) + let customEngineViewController = SearchCustomEngineViewController(profile: profile, privateBrowsingManager: privateBrowsingManager) navigationController?.pushViewController(customEngineViewController, animated: true) } diff --git a/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/Sources/Brave/Frontend/Settings/SettingsViewController.swift index 80897fd7af7..e8b5852b8cb 100644 --- a/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -315,7 +315,7 @@ class SettingsViewController: TableViewController { Row( text: Strings.searchEngines, selection: { [unowned self] in - let viewController = SearchSettingsTableViewController(profile: self.profile) + let viewController = SearchSettingsTableViewController(profile: self.profile, privateBrowsingManager: tabManager.privateBrowsingManager) self.navigationController?.pushViewController(viewController, animated: true) }, image: UIImage(braveSystemNamed: "leo.search"), accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self), Row( @@ -514,7 +514,7 @@ class SettingsViewController: TableViewController { )) // We do NOT persistently save page-zoom settings in Private Browsing - if !PrivateBrowsingManager.shared.isPrivateBrowsing { + if !tabManager.privateBrowsingManager.isPrivateBrowsing { display.rows.append( Row(text: Strings.PageZoom.settingsMenuTitle, selection: { [weak self] in diff --git a/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift b/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift index 3dfc390891d..f214e228fe4 100644 --- a/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift +++ b/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift @@ -7,6 +7,7 @@ import SwiftUI import Preferences import Strings import BraveShared +import Data struct OtherPrivacySettingsSectionView: View { @State private var showPrivateBrowsingConfirmation = false @@ -34,6 +35,7 @@ struct OtherPrivacySettingsSectionView: View { Task { @MainActor in try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + Preferences.Privacy.persistentPrivateBrowsing.value = false await settings.clearPrivateData([CookiesAndCacheClearable()]) // First remove all tabs so that only a blank tab exists. @@ -52,6 +54,25 @@ struct OtherPrivacySettingsSectionView: View { ) }) + if !Preferences.Privacy.privateBrowsingOnly.value { + OptionToggleView(title: Strings.persistentPrivateBrowsing, + subtitle: nil, + option: Preferences.Privacy.persistentPrivateBrowsing) { newValue in + Task { @MainActor in + if newValue { + settings.tabManager.saveAllTabs() + } else { + let tabs = settings.tabManager.allTabs.filter({ $0.isPrivate }) + SessionTab.deleteAll(tabIds: tabs.map({ $0.id })) + + if !settings.tabManager.privateBrowsingManager.isPrivateBrowsing { + settings.tabManager.willSwitchTabMode(leavingPBM: true) + } + } + } + } + } + ShieldToggleView( title: Strings.blockMobileAnnoyances, subtitle: nil, diff --git a/Sources/Brave/Frontend/Shields/ShieldsActivityItemSourceProvider.swift b/Sources/Brave/Frontend/Shields/ShieldsActivityItemSourceProvider.swift index 79b703a094b..b4d99a87464 100644 --- a/Sources/Brave/Frontend/Shields/ShieldsActivityItemSourceProvider.swift +++ b/Sources/Brave/Frontend/Shields/ShieldsActivityItemSourceProvider.swift @@ -23,12 +23,13 @@ final class ShieldsActivityItemSourceProvider { static let shared = ShieldsActivityItemSourceProvider() - func setupGlobalShieldsActivityController() -> UIActivityViewController { + func setupGlobalShieldsActivityController(isPrivateBrowsing: Bool) -> UIActivityViewController { let backgroundImage = UIImage(named: "share-activity-background", in: .module, compatibleWith: nil)! let statsView = UIView(frame: CGRect(size: backgroundImage.size)).then { let backgroundImageView = UIImageView(image: backgroundImage) let statsInfoView = BraveShieldStatsView() + statsInfoView.isPrivateBrowsing = isPrivateBrowsing $0.addSubview(backgroundImageView) $0.addSubview(statsInfoView) diff --git a/Sources/Brave/Frontend/Shields/ShieldsViewController.swift b/Sources/Brave/Frontend/Shields/ShieldsViewController.swift index f936d5f914f..843f0fe60d8 100644 --- a/Sources/Brave/Frontend/Shields/ShieldsViewController.swift +++ b/Sources/Brave/Frontend/Shields/ShieldsViewController.swift @@ -62,7 +62,7 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { private func updateToggleStatus() { var domain: Domain? if let url = url { - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = tab.isPrivate domain = Domain.getOrCreate(forUrl: url, persistent: !isPrivateBrowsing) } @@ -104,7 +104,7 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { let isOn = allOff ? !on : on Domain.setBraveShield( forUrl: url, shield: shield, isOn: isOn, - isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing) + isPrivateBrowsing: tab.isPrivate) } private func updateGlobalShieldState(_ on: Bool, animated: Bool = false) { @@ -229,7 +229,7 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { super.viewDidLoad() if let url = url { - shieldsView.simpleShieldView.faviconImageView.loadFavicon(for: url) + shieldsView.simpleShieldView.faviconImageView.loadFavicon(for: url, isPrivateBrowsing: tab.isPrivate) } else { shieldsView.simpleShieldView.faviconImageView.isHidden = true } @@ -309,7 +309,7 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { @objc private func tappedShareShieldsButton() { let globalShieldsActivityController = - ShieldsActivityItemSourceProvider.shared.setupGlobalShieldsActivityController() + ShieldsActivityItemSourceProvider.shared.setupGlobalShieldsActivityController(isPrivateBrowsing: tab.isPrivate) globalShieldsActivityController.popoverPresentationController?.sourceView = view present(globalShieldsActivityController, animated: true, completion: nil) diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift index 6077fd6ccb5..7803e334741 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveSearchScriptHandler.swift @@ -98,7 +98,7 @@ class BraveSearchScriptHandler: TabContentScript { } private func handleCanSetBraveSearchAsDefault(replyHandler: (Any?, String?) -> Void) { - if PrivateBrowsingManager.shared.isPrivateBrowsing { + if tab?.isPrivate == true { Logger.module.debug("Private mode detected, skipping setting Brave Search as a default") replyHandler(false, nil) return diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift index 89f6f4c11dd..faa37a3a60e 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/BraveTalkScriptHandler.swift @@ -123,7 +123,7 @@ class BraveTalkScriptHandler: TabContentScript { } private func handleBraveRequestAdsEnabled(_ replyHandler: @escaping (Any?, String?) -> Void) { - guard let rewards = rewards, !PrivateBrowsingManager.shared.isPrivateBrowsing else { + guard let rewards = rewards, tab?.isPrivate != true else { replyHandler(false, nil) return } diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift index bb02aaadd34..84af8fe7769 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift @@ -65,7 +65,7 @@ extension ContentBlockerHelper: TabContentScript { let dto = try JSONDecoder().decode(ContentBlockerDTO.self, from: data) Task { @MainActor in - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = self.tab?.isPrivate == true let domain = Domain.getOrCreate(forUrl: currentTabURL, persistent: !isPrivateBrowsing) if domain.areAllShieldsOff { // if domain is "all_off", can just skip @@ -110,7 +110,7 @@ extension ContentBlockerHelper: TabContentScript { if blockedType == .ad, Preferences.PrivacyReports.captureShieldsData.value, let domainURL = URL(string: domainURLString), let blockedResourceHost = requestURL.baseDomain, - !PrivateBrowsingManager.shared.isPrivateBrowsing { + tab?.isPrivate != true { PrivacyReportsManager.pendingBlockedRequests.append((blockedResourceHost, domainURL, Date())) } diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift index 72c2bbaa502..d5fac2ab186 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift @@ -66,7 +66,7 @@ class RequestBlockingContentScriptHandler: TabContentScript { // we use `NSURL(idnString: String)` to parse them guard let requestURL = NSURL(idnString: dto.data.resourceURL) as URL? else { return } guard let sourceURL = NSURL(idnString: dto.data.sourceURL) as URL? else { return } - let isPrivateBrowsing = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivateBrowsing = tab.isPrivate Task { @MainActor in let domain = Domain.getOrCreate(forUrl: currentTabURL, persistent: !isPrivateBrowsing) @@ -90,7 +90,7 @@ class RequestBlockingContentScriptHandler: TabContentScript { if shouldBlock, Preferences.PrivacyReports.captureShieldsData.value, let domainURL = URL(string: domainURLString), let blockedResourceHost = requestURL.baseDomain, - !PrivateBrowsingManager.shared.isPrivateBrowsing { + !isPrivateBrowsing { PrivacyReportsManager.pendingBlockedRequests.append((blockedResourceHost, domainURL, Date())) } diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift index 5b7a654637a..acdd7f3dba3 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RewardsReportingScriptHandler.swift @@ -43,7 +43,7 @@ class RewardsReportingScriptHandler: TabContentScript { var referrerUrl: String? } - if PrivateBrowsingManager.shared.isPrivateBrowsing || !rewards.isEnabled { + if tab?.isPrivate == true || !rewards.isEnabled { return } diff --git a/Sources/Brave/Migration/Migration.swift b/Sources/Brave/Migration/Migration.swift index ff4f14d3f85..70a717ec5cd 100644 --- a/Sources/Brave/Migration/Migration.swift +++ b/Sources/Brave/Migration/Migration.swift @@ -74,7 +74,7 @@ public class Migration { // Migrate from TabMO to SessionTab and SessionWindow public static func migrateTabStateToWebkitState(diskImageStore: DiskImageStore?) { - let isPrivate = PrivateBrowsingManager.shared.isPrivateBrowsing + let isPrivate = false // Private tabs at the time of writing this code was never persistent, so it wouldn't "restore". if Preferences.Migration.tabMigrationToInteractionStateCompleted.value { SessionWindow.createIfNeeded(index: 0, isPrivate: isPrivate, isSelected: true) diff --git a/Sources/Brave/Shortcuts/ActivityShortcutManager.swift b/Sources/Brave/Shortcuts/ActivityShortcutManager.swift index a8bcb2ed701..3649fc7e95b 100644 --- a/Sources/Brave/Shortcuts/ActivityShortcutManager.swift +++ b/Sources/Brave/Shortcuts/ActivityShortcutManager.swift @@ -129,7 +129,7 @@ public class ActivityShortcutManager: NSObject { private func handleActivityDetails(type: ActivityType, using bvc: BrowserViewController) { switch type { case .newTab: - bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, isExternal: true) + bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: bvc.privateBrowsingManager.isPrivateBrowsing, isExternal: true) bvc.popToBVC() case .newPrivateTab: bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: true, isExternal: true) @@ -137,7 +137,7 @@ public class ActivityShortcutManager: NSObject { case .clearBrowsingHistory: bvc.clearHistoryAndOpenNewTab() case .enableBraveVPN: - bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, isExternal: true) + bvc.openBlankNewTab(attemptLocationFieldFocus: false, isPrivate: bvc.privateBrowsingManager.isPrivateBrowsing, isExternal: true) bvc.popToBVC() switch BraveVPN.vpnState { @@ -216,8 +216,7 @@ public class ActivityShortcutManager: NSObject { // MARK: Intent Donation Methods public func donateCustomIntent(for type: IntentType, with urlString: String) { - guard !PrivateBrowsingManager.shared.isPrivateBrowsing, - !urlString.isEmpty, + guard !urlString.isEmpty, URL(string: urlString) != nil else { return } diff --git a/Sources/Brave/States/BrowserState.swift b/Sources/Brave/States/BrowserState.swift new file mode 100644 index 00000000000..c2165f18a45 --- /dev/null +++ b/Sources/Brave/States/BrowserState.swift @@ -0,0 +1,29 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// 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 Foundation +import UIKit + +public class BrowserState { + public static let sceneId = "com.brave.ios.browser-scene" + + let window: UIWindow + let profile: Profile + + init(window: UIWindow, profile: Profile) { + self.window = window + self.profile = profile + } + + public static func userActivity(for windowId: UUID, isPrivate: Bool) -> NSUserActivity { + return NSUserActivity(activityType: sceneId).then { + $0.targetContentIdentifier = windowId.uuidString + $0.addUserInfoEntries(from: [ + "WindowID": windowId.uuidString, + "isPrivate": isPrivate + ]) + } + } +} diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 14f90fc5045..e72e2a22ef3 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -141,6 +141,10 @@ extension Strings { bundle: .module, value: "An unknown error occurred while opening the Downloads folder in the Files app.", comment: "Error description when there is an error while navigating to Files App") + public static let openInNewWindowTitle = NSLocalizedString("OpenInNewWindowTitle", tableName: "BraveShared", bundle: .module, value: "Open in New Window", comment: "Context menu item for opening a link in a new window") + public static let openInNewPrivateWindowTitle = NSLocalizedString("OpenInNewPrivateWindowTitle", tableName: "BraveShared", bundle: .module, value: "Open in New Private Window", comment: "Context menu item for opening a link in a new private browsing window") + public static let newWindowTitle = NSLocalizedString("NewWindowTitle", tableName: "BraveShared", bundle: .module, value: "New Window", comment: "Context menu item for opening a new window") + public static let newPrivateWindowTitle = NSLocalizedString("NewPrivateWindowTitle", tableName: "BraveShared", bundle: .module, value: "New Private Window", comment: "Context menu item for opening a new private browsing window") } // MARK:- DefaultBrowserIntroCalloutViewController.swift @@ -1104,6 +1108,9 @@ extension Strings { public static let youtubeMediaQualityOn = NSLocalizedString("YoutubeMediaQualityOn", tableName: "BraveShared", bundle: .module, value: "On", comment: "Setting that enables high quality playback always") public static let showTabsBar = NSLocalizedString("ShowTabsBar", tableName: "BraveShared", bundle: .module, value: "Tabs Bar", comment: "Setting to show/hide the tabs bar") public static let privateBrowsingOnly = NSLocalizedString("PrivateBrowsingOnly", tableName: "BraveShared", bundle: .module, value: "Private Browsing Only", comment: "Setting to keep app in private mode") + public static let persistentPrivateBrowsingAlertTitle = NSLocalizedString("PersistentPrivateBrowsingAlertTitle", tableName: "BraveShared", bundle: .module, value: "Restore Private Tabs", comment: "Persistent private browsing alert title to existing users") + public static let persistentPrivateBrowsingAlertMessage = NSLocalizedString("PersistentPrivateBrowsingAlertMessage", tableName: "BraveShared", bundle: .module, value: "Allows Brave to restore private browsing tabs, even if you close / re-open the app", comment: "Persistent private browsing alert message to existing users") + public static let persistentPrivateBrowsing = NSLocalizedString("PersistentPrivateBrowsing", tableName: "BraveShared", bundle: .module, value: "Persistent Private Browsing", comment: "Setting to allow the app to restore private browsing tabs") public static let shieldsDefaults = NSLocalizedString("ShieldsDefaults", tableName: "BraveShared", bundle: .module, value: "Brave Shields Global Defaults", comment: "Section title for adbblock, tracking protection, HTTPS-E, and cookies") public static let shieldsDefaultsFooter = NSLocalizedString("ShieldsDefaultsFooter", tableName: "BraveShared", bundle: .module, value: "These are the default Shields settings for new sites. Changing these won't affect your existing per-site settings.", comment: "Section footer for global shields defaults") public static let HTTPSEverywhere = NSLocalizedString("HTTPSEverywhere", tableName: "BraveShared", bundle: .module, value: "Upgrade Connections to HTTPS", comment: "") @@ -1901,6 +1908,16 @@ extension Strings { NSLocalizedString("playlist.deleteForOfflineButtonTitle", tableName: "BraveShared", bundle: .module, value: "Delete Offline Cache", comment: "The title of the button indicating that the user delete the offline data. (deletes the data that allows them to play offline)") + + public static let playlistAlreadyShowingTitle = + NSLocalizedString("playlist.playlistAlreadyShowingTitle", tableName: "BraveShared", bundle: .module, + value: "Sorry", + comment: "Playlist alert title when playlist is already showing on a different window") + + public static let playlistAlreadyShowingBody = + NSLocalizedString("playlist.playlistAlreadyShowingBody", tableName: "BraveShared", bundle: .module, + value: "Playlist is already active on another window", + comment: "Playlist alert message when playlist is already showing on a different window") } public struct PlaylistFolders { diff --git a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift index 3595b964f80..075851ff452 100644 --- a/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift +++ b/Sources/BraveWallet/Crypto/Stores/KeyringStore.swift @@ -116,6 +116,13 @@ public class KeyringStore: ObservableObject { /// The origin of the active tab (if applicable). Used for fetching/selecting network for the DApp origin. public var origin: URLOrigin? + /// If this KeyringStore instance is creating a wallet. + /// This flag is used to know when to dismiss onboarding when multiple windows are visible. + private var isCreatingWallet = false + /// If this KeyringStore instance is restoring a wallet. + /// This flag is used to know when to dismiss onboarding when multiple windows are visible. + private var isRestoringWallet = false + /// Internal flag kept for when `setSelectedAccount` is executing so we can wait for /// completion before reacting to observed changes. Ex. chain changed event fires after /// `setSelectedAccount` changes network, but before it can set the new account. @@ -229,6 +236,8 @@ public class KeyringStore: ObservableObject { } func markOnboardingCompleted() { + self.isCreatingWallet = false + self.isRestoringWallet = false self.isOnboardingVisible = false } @@ -277,6 +286,7 @@ public class KeyringStore: ObservableObject { } func createWallet(password: String, completion: ((String) -> Void)? = nil) { + isCreatingWallet = true keyringService.createWallet(password) { [weak self] mnemonic in self?.updateKeyringInfo() completion?(mnemonic) @@ -298,6 +308,7 @@ public class KeyringStore: ObservableObject { } func restoreWallet(phrase: String, password: String, isLegacyBraveWallet: Bool, completion: ((Bool) -> Void)? = nil) { + isRestoringWallet = true keyringService.restoreWallet( phrase, password: password, @@ -445,6 +456,12 @@ extension KeyringStore: BraveWalletKeyringServiceObserver { } public func keyringCreated(_ keyringId: BraveWallet.KeyringId) { + if isOnboardingVisible, !isCreatingWallet, keyringId == BraveWallet.KeyringId.default { + // Another window has created a wallet. We should dismiss onboarding on this + // window and allow the other window to continue with it's onboarding flow. + isOnboardingVisible = false + } + Task { @MainActor in let newKeyring = await keyringService.keyringInfo(keyringId) if let newKeyringCoin = newKeyring.coin { @@ -459,6 +476,12 @@ extension KeyringStore: BraveWalletKeyringServiceObserver { } public func keyringRestored(_ keyringId: BraveWallet.KeyringId) { + if isOnboardingVisible && !isRestoringWallet, keyringId == BraveWallet.KeyringId.default { + // Another window has restored a wallet. We should dismiss onboarding on this + // window and allow the other window to continue with it's onboarding flow. + isOnboardingVisible = false + } + updateKeyringInfo() } diff --git a/Sources/Data/models/SessionTab.swift b/Sources/Data/models/SessionTab.swift index 8866fb396a6..1ab593b212c 100644 --- a/Sources/Data/models/SessionTab.swift +++ b/Sources/Data/models/SessionTab.swift @@ -127,6 +127,11 @@ extension SessionTab { deleteAll(context: .new(inMemory: false)) } + public static func deleteAll(tabIds: [UUID]) { + let predicate = NSPredicate(format: "\(#keyPath(SessionTab.tabId)) IN %@", tabIds) + deleteAll(predicate: predicate, context: .new(inMemory: false)) + } + public static func deleteAll(olderThan timeInterval: TimeInterval) { let lastUpdatedKeyPath = #keyPath(SessionTab.lastUpdated) let date = Date().advanced(by: -timeInterval) as NSDate @@ -207,17 +212,17 @@ extension SessionTab { } } - public static func createIfNeeded(tabId: UUID, title: String, tabURL: URL) { + public static func createIfNeeded(windowId: UUID, tabId: UUID, title: String, tabURL: URL, isPrivate: Bool) { DataController.perform { context in guard !SessionTab.exists(tabId: tabId, in: context), - let window = SessionWindow.getActiveWindow(context: context) else { return } + let window = SessionWindow.from(windowId: windowId, in: context) else { return } _ = SessionTab(context: context, sessionWindow: window, sessionTabGroup: nil, index: Int32(window.sessionTabs?.count ?? 0), interactionState: Data(), - isPrivate: false, + isPrivate: isPrivate, isSelected: false, lastUpdated: .now, screenshotData: Data(), @@ -232,6 +237,23 @@ extension SessionTab { } } } + + public static func move(tab tabId: UUID, toWindow windowId: UUID) { + DataController.performOnMainContext { context in + guard let tab = SessionTab.from(tabId: tabId, in: context), + let window = SessionWindow.from(windowId: windowId, in: context) else { + return + } + + tab.sessionWindow = window + + do { + try context.save() + } catch { + Logger.module.error("performTask save error: \(error.localizedDescription, privacy: .public)") + } + } + } } // MARK: - Private diff --git a/Sources/Data/models/SessionWindow.swift b/Sources/Data/models/SessionWindow.swift index be4eafb553e..defccb0475d 100644 --- a/Sources/Data/models/SessionWindow.swift +++ b/Sources/Data/models/SessionWindow.swift @@ -64,6 +64,11 @@ extension SessionWindow { return Self.from(windowId: windowId, in: DataController.viewContext) } + public static func from(windowId: UUID, in context: NSManagedObjectContext) -> SessionWindow? { + let predicate = NSPredicate(format: "\(#keyPath(SessionWindow.windowId)) == %@", windowId.uuidString) + return first(where: predicate, context: context) + } + public static func createIfNeeded(index: Int32, isPrivate: Bool, isSelected: Bool) { DataController.performOnMainContext { context in if SessionWindow.getActiveWindow(context: context) != nil { @@ -80,11 +85,34 @@ extension SessionWindow { } } + public static func createWindow(isPrivate: Bool, isSelected: Bool, uuid: UUID) { + DataController.performOnMainContext { context in + if let sessionWindow = SessionWindow.from(windowId: uuid, in: context) { + Self.all().forEach { + $0.isSelected = false + } + + sessionWindow.isSelected = isSelected + return + } + + let count = SessionWindow.count(context: context) ?? 0 + let window = SessionWindow(context: context, index: Int32(count), isPrivate: isPrivate, isSelected: isSelected) + window.windowId = uuid + + do { + try context.save() + } catch { + Logger.module.error("performTask save error: \(error.localizedDescription, privacy: .public)") + } + } + } + /// Marks the specified window as selected /// Since only one window can be active at a time, all other windows are marked as deselected public static func setSelected(windowId: UUID) { DataController.perform { context in - guard let window = Self.from(windowId: windowId) else { return } + guard let window = Self.from(windowId: windowId, in: context) else { return } let predicate = NSPredicate(format: "isSelected == true") all(where: predicate, context: context)?.forEach { @@ -94,16 +122,34 @@ extension SessionWindow { window.isSelected = true } } + + public static func all() -> [SessionWindow] { + let sortDescriptors = [NSSortDescriptor(key: #keyPath(SessionWindow.index), ascending: true)] + return all(sortDescriptors: sortDescriptors) ?? [] + } + + public static func delete(windowId: UUID) { + DataController.perform { context in + guard let sessionWindow = SessionWindow.from(windowId: windowId, in: context) else { + return + } + + sessionWindow.sessionTabs?.forEach { + $0.delete(context: .existing(context)) + } + + sessionWindow.sessionTabGroups?.forEach { + $0.delete(context: .existing(context)) + } + + sessionWindow.delete(context: .existing(context)) + } + } } // MARK: - Private extension SessionWindow { - private static func from(windowId: UUID, in context: NSManagedObjectContext) -> SessionWindow? { - let predicate = NSPredicate(format: "\(#keyPath(SessionWindow.windowId)) == %@", windowId.uuidString) - return first(where: predicate, context: context) - } - private static func entity(_ context: NSManagedObjectContext) -> NSEntityDescription? { return NSEntityDescription.entity(forEntityName: "SessionWindow", in: context) } diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.tab-private.symbolset/Contents.json b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.tab-private.symbolset/Contents.json new file mode 100644 index 00000000000..2f415ce6e4c --- /dev/null +++ b/Sources/DesignSystem/Icons/Symbols.xcassets/leo.window.tab-private.symbolset/Contents.json @@ -0,0 +1,11 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "idiom" : "universal" + } + ] +} diff --git a/Sources/Onboarding/ProductNotifications/CookieNotificationBlockingConsentView.swift b/Sources/Onboarding/ProductNotifications/CookieNotificationBlockingConsentView.swift index 93478e880b5..d5ace344006 100644 --- a/Sources/Onboarding/ProductNotifications/CookieNotificationBlockingConsentView.swift +++ b/Sources/Onboarding/ProductNotifications/CookieNotificationBlockingConsentView.swift @@ -148,8 +148,6 @@ public struct CookieNotificationBlockingConsentView: View { } } - - #if DEBUG struct CookieNotificationBlockingConsentView_Previews: PreviewProvider { static var previews: some View { diff --git a/Tests/ClientTests/NavigationRouterTests.swift b/Tests/ClientTests/NavigationRouterTests.swift index 3bd2315d979..193046acb00 100644 --- a/Tests/ClientTests/NavigationRouterTests.swift +++ b/Tests/ClientTests/NavigationRouterTests.swift @@ -9,16 +9,18 @@ import Preferences import XCTest class NavigationRouterTests: XCTestCase { + + private let privateBrowsingManager = PrivateBrowsingManager() override func setUp() { super.setUp() - PrivateBrowsingManager.shared.isPrivateBrowsing = false + privateBrowsingManager.isPrivateBrowsing = false Preferences.Privacy.privateBrowsingOnly.reset() } override func tearDown() { super.tearDown() - PrivateBrowsingManager.shared.isPrivateBrowsing = false + privateBrowsingManager.isPrivateBrowsing = false Preferences.Privacy.privateBrowsingOnly.reset() } @@ -38,7 +40,7 @@ class NavigationRouterTests: XCTestCase { return false } - guard let navItem = NavigationPath(url: appURL) else { + guard let navItem = NavigationPath(url: appURL, isPrivateBrowsing: self.privateBrowsingManager.isPrivateBrowsing) else { XCTFail("Invalid Navigation Path") return false } @@ -55,17 +57,17 @@ class NavigationRouterTests: XCTestCase { // Test URL with double escaped encoded characters (URL was encoded twice) XCTAssertTrue(testURLEncoding("http://google.com%3Fa%3D1%26b%3D2%26c%3Dfoo%2520bar")) - let emptyNav = NavigationPath(url: URL(string: "\(appScheme)://open-url?private=true")!) + let emptyNav = NavigationPath(url: URL(string: "\(appScheme)://open-url?private=true")!, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) XCTAssertEqual(emptyNav, NavigationPath.url(webURL: nil, isPrivate: true)) - let badNav = NavigationPath(url: URL(string: "\(appScheme)://open-url?url=blah")!) + let badNav = NavigationPath(url: URL(string: "\(appScheme)://open-url?url=blah")!, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing) XCTAssertEqual(badNav, NavigationPath.url(webURL: URL(string: "blah"), isPrivate: false)) } func testSearchScheme() { let query = "Foo Bar".addingPercentEncoding(withAllowedCharacters: .alphanumerics)! let appURL = "\(appScheme)://search?q=" + query - let navItem = NavigationPath(url: URL(string: appURL)!)! + let navItem = NavigationPath(url: URL(string: appURL)!, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)! XCTAssertEqual(navItem, NavigationPath.text("Foo Bar")) } @@ -73,7 +75,7 @@ class NavigationRouterTests: XCTestCase { func testDefaultNavigationPath() { let url = URL(string: "https://duckduckgo.com")! let appURL = URL(string: "\(self.appScheme)://open-url?url=\(url.absoluteString.escape()!)")! - let path = NavigationPath(url: appURL)! + let path = NavigationPath(url: appURL, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)! XCTAssertEqual(path, NavigationPath.url(webURL: url, isPrivate: false)) } @@ -83,29 +85,29 @@ class NavigationRouterTests: XCTestCase { let url = URL(string: "https://duckduckgo.com")! let appURL = URL(string: "\(self.appScheme)://open-url?url=\(url.absoluteString.escape()!)")! - let path = NavigationPath(url: appURL)! + let path = NavigationPath(url: appURL, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)! // Should inheritely be private by default XCTAssertEqual(path, NavigationPath.url(webURL: url, isPrivate: true)) } func testNavigationPathAlreadyInPrivateBrowsingMode() { - PrivateBrowsingManager.shared.isPrivateBrowsing = true + privateBrowsingManager.isPrivateBrowsing = true let url = URL(string: "https://duckduckgo.com")! let appURL = URL(string: "\(self.appScheme)://open-url?url=\(url.absoluteString.escape()!)")! - let path = NavigationPath(url: appURL)! + let path = NavigationPath(url: appURL, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)! // Should inheritely be private by default XCTAssertEqual(path, NavigationPath.url(webURL: url, isPrivate: true)) } func testNavigationPathForcedRegularMode() { - PrivateBrowsingManager.shared.isPrivateBrowsing = true + privateBrowsingManager.isPrivateBrowsing = true let url = URL(string: "https://duckduckgo.com")! let appURL = URL(string: "\(self.appScheme)://open-url?url=\(url.absoluteString.escape()!)&private=false")! - let path = NavigationPath(url: appURL)! + let path = NavigationPath(url: appURL, isPrivateBrowsing: privateBrowsingManager.isPrivateBrowsing)! // Should be regular due to specified argument in the URL XCTAssertEqual(path, NavigationPath.url(webURL: url, isPrivate: false)) diff --git a/Tests/ClientTests/SearchEnginesTests.swift b/Tests/ClientTests/SearchEnginesTests.swift index 35b4aa3937d..6a75ea6159e 100644 --- a/Tests/ClientTests/SearchEnginesTests.swift +++ b/Tests/ClientTests/SearchEnginesTests.swift @@ -26,8 +26,6 @@ class SearchEnginesTests: XCTestCase { Preferences.Search.defaultEngineName.reset() Preferences.Search.defaultPrivateEngineName.reset() Preferences.Search.yahooEngineMigrationCompleted.reset() - - PrivateBrowsingManager.shared.isPrivateBrowsing = false } func testIncludesExpectedEngines() { @@ -45,7 +43,7 @@ class SearchEnginesTests: XCTestCase { // If this is our first run, Google should be first for the en locale. let profile = MockProfile() let engines = SearchEngines(files: profile.files, locale: Locale(identifier: "pl_PL")) - XCTAssertEqual(engines.defaultEngine().shortName, DefaultSearchEngineName) + XCTAssertEqual(engines.defaultEngine(forType: .standard).shortName, DefaultSearchEngineName) // The default is `DefaultSearchEngineName` for both regular and private browsing. // Different search engine options might apply to certain regions. // Default locale for running tests should be en_US. @@ -74,20 +72,20 @@ class SearchEnginesTests: XCTestCase { let engineSet = engines.orderedEngines engines.updateDefaultEngine((engineSet?[0])!.shortName, forType: .standard) - XCTAssertTrue(engines.isEngineDefault((engineSet?[0])!)) - XCTAssertFalse(engines.isEngineDefault((engineSet?[1])!)) + XCTAssertTrue(engines.isEngineDefault((engineSet?[0])!, type: .standard)) + XCTAssertFalse(engines.isEngineDefault((engineSet?[1])!, type: .standard)) // The first ordered engine is the default. XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[0].shortName) engines.updateDefaultEngine((engineSet?[1])!.shortName, forType: .standard) - XCTAssertFalse(engines.isEngineDefault((engineSet?[0])!)) - XCTAssertTrue(engines.isEngineDefault((engineSet?[1])!)) + XCTAssertFalse(engines.isEngineDefault((engineSet?[0])!, type: .standard)) + XCTAssertTrue(engines.isEngineDefault((engineSet?[1])!, type: .standard)) // The first ordered engine is the default. XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName) let engines2 = SearchEngines(files: profile.files) // The default engine should have been persisted. - XCTAssertTrue(engines2.isEngineDefault((engineSet?[1])!)) + XCTAssertTrue(engines2.isEngineDefault((engineSet?[1])!, type: .standard)) // The first ordered engine is the default. XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName) } @@ -136,7 +134,7 @@ class SearchEnginesTests: XCTestCase { // You can't disable the default engine. engines.updateDefaultEngine((engineSet?[1])!.shortName, forType: .standard) - engines.disableEngine((engineSet?[1])!) + engines.disableEngine((engineSet?[1])!, type: .standard) XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!)) // The default engine is included in the quick search engines. @@ -147,7 +145,7 @@ class SearchEnginesTests: XCTestCase { XCTAssertTrue(engines.isEngineEnabled((engineSet?[0])!)) XCTAssertEqual(1, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count) - engines.disableEngine((engineSet?[0])!) + engines.disableEngine((engineSet?[0])!, type: .standard) XCTAssertFalse(engines.isEngineEnabled((engineSet?[0])!)) XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count) @@ -161,7 +159,7 @@ class SearchEnginesTests: XCTestCase { // The enabling should be persisted. engines.enableEngine((engineSet?[2])!) - engines.disableEngine((engineSet?[1])!) + engines.disableEngine((engineSet?[1])!, type: .standard) engines.enableEngine((engineSet?[0])!) let engines2 = SearchEngines(files: profile.files) @@ -232,7 +230,7 @@ class SearchEnginesTests: XCTestCase { engines.migrateDefaultYahooSearchEngines() - XCTAssertTrue(engines.defaultEngine().shortName == "Yahoo") + XCTAssertTrue(engines.defaultEngine(forType: .standard).shortName == "Yahoo") XCTAssertTrue(engines.defaultEngine(forType: .privateMode).shortName == "Yahoo") // Testing Both standard and private mode Yahoo JAPAN SE @@ -242,7 +240,7 @@ class SearchEnginesTests: XCTestCase { engines.migrateDefaultYahooSearchEngines() - XCTAssertTrue(engines.defaultEngine().shortName == "Yahoo! JAPAN") + XCTAssertTrue(engines.defaultEngine(forType: .standard).shortName == "Yahoo! JAPAN") XCTAssertTrue(engines.defaultEngine(forType: .privateMode).shortName == "Yahoo! JAPAN") engines.updateDefaultEngine("Google", forType: .standard) diff --git a/Tests/ClientTests/TabManagerTests.swift b/Tests/ClientTests/TabManagerTests.swift index 7f8493b5905..08b2df90587 100644 --- a/Tests/ClientTests/TabManagerTests.swift +++ b/Tests/ClientTests/TabManagerTests.swift @@ -5,6 +5,7 @@ @testable import Brave @testable import Data import Shared +import Preferences import Storage import UIKit import WebKit @@ -107,20 +108,29 @@ open class MockTabManagerDelegate: TabManagerDelegate { let didAdd = MethodSpy(functionName: "tabManager(_:didAddTab:)") var manager: TabManager! + private let privateBrowsingManager = PrivateBrowsingManager() override func setUp() { super.setUp() DataController.shared.initializeOnce() let profile = MockProfile() - manager = TabManager(prefs: profile.prefs, rewards: nil, tabGeneratorAPI: nil) - PrivateBrowsingManager.shared.isPrivateBrowsing = false + manager = TabManager(windowId: UUID(), prefs: profile.prefs, rewards: nil, tabGeneratorAPI: nil, privateBrowsingManager: privateBrowsingManager) + privateBrowsingManager.isPrivateBrowsing = false } override func tearDown() { - PrivateBrowsingManager.shared.isPrivateBrowsing = false + privateBrowsingManager.isPrivateBrowsing = false super.tearDown() } + + private func setPersistentPrivateMode(_ isPersistent: Bool) { + Preferences.Privacy.persistentPrivateBrowsing.value = isPersistent + + if isPersistent { + Preferences.Privacy.privateBrowsingOnly.value = false + } + } // BRAVE TODO: We no longer "store tabs", happens async from CoreData, so this test has to reflect CD instead /* @@ -210,7 +220,39 @@ open class MockTabManagerDelegate: TabManagerDelegate { delegate.verify("Not all delegate methods were called") } - func testDeletePrivateTabsOnExit() { + func testDeletePrivateTabsPersistenceOnExit() { + setPersistentPrivateMode(true) + + // create one private and one normal tab + let tab = manager.addTab(isPrivate: false) + manager.selectTab(tab) + manager.selectTab(manager.addTab(isPrivate: true)) + + XCTAssertEqual(TabType.of(manager.selectedTab).isPrivate, true, "The selected tab should be the private tab") + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should only be one private tab") + + manager.selectTab(tab) + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "If the normal tab is selected the private tab should NOT be deleted") + XCTAssertEqual(manager.tabs(withType: .regular).count, 1, "The regular tab should stil be around") + + manager.selectTab(manager.addTab(isPrivate: true)) + XCTAssertEqual(manager.tabs(withType: .private).count, 2, "There should be two private tabs") + manager.willSwitchTabMode(leavingPBM: true) + XCTAssertEqual(manager.tabs(withType: .private).count, 2, "After willSwitchTabMode there should be 2 private tabs") + + manager.selectTab(manager.addTab(isPrivate: true)) + manager.selectTab(manager.addTab(isPrivate: true)) + XCTAssertEqual(manager.tabs(withType: .private).count, 4, "Private tabs should not be deleted when another one is added") + manager.selectTab(manager.addTab(isPrivate: false)) + XCTAssertEqual(manager.tabs(withType: .private).count, 4, "But once we add a normal tab we've switched out of private mode. Private tabs should be be persistent") + XCTAssertEqual(manager.tabs(withType: .regular).count, 2, "The original normal tab and the new one should both still exist") + + setPersistentPrivateMode(false) + } + + func testDeletePrivateTabsOnNonPersistenceExit() { + setPersistentPrivateMode(false) + // create one private and one normal tab let tab = manager.addTab(isPrivate: false) manager.selectTab(tab) @@ -236,7 +278,27 @@ open class MockTabManagerDelegate: TabManagerDelegate { XCTAssertEqual(manager.tabs(withType: .regular).count, 2, "The original normal tab and the new one should both still exist") } - func testTogglePBMDelete() { + func testTogglePBMDeletePersistent() { + setPersistentPrivateMode(true) + + let tab = manager.addTab(isPrivate: false) + manager.selectTab(tab) + manager.selectTab(manager.addTab(isPrivate: false)) + manager.selectTab(manager.addTab(isPrivate: true)) + + manager.willSwitchTabMode(leavingPBM: false) + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should be 1 private tab") + manager.willSwitchTabMode(leavingPBM: true) + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should be 1 private tab") + manager.removeTab(tab) + XCTAssertEqual(manager.tabs(withType: .regular).count, 1, "There should be 1 normal tab") + + setPersistentPrivateMode(false) + } + + func testTogglePBMDeleteNonPersistent() { + setPersistentPrivateMode(false) + let tab = manager.addTab(isPrivate: false) manager.selectTab(tab) manager.selectTab(manager.addTab(isPrivate: false)) @@ -327,7 +389,52 @@ open class MockTabManagerDelegate: TabManagerDelegate { delegate.verify("Not all delegate methods were called") } - func testDelegatesCalledWhenRemovingPrivateTabs() { + func testDelegatesCalledWhenRemovingPrivateTabsPersistence() { + setPersistentPrivateMode(true) + + //setup + let delegate = MockTabManagerDelegate() + + // create one private and one normal tab + let tab = manager.addTab(isPrivate: false) + let newTab = manager.addTab(isPrivate: false) + manager.selectTab(tab) + manager.selectTab(manager.addTab(isPrivate: true)) + manager.addDelegate(delegate) + + // Double check a few things + XCTAssertEqual(TabType.of(manager.selectedTab).isPrivate, true, "The selected tab should be the private tab") + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "There should only be one private tab") + + // switch to normal mode. Which should not delete the private tabs + manager.willSwitchTabMode(leavingPBM: true) + + //make sure tabs are NOT cleared properly and indexes are reset + XCTAssertEqual(manager.tabs(withType: .private).count, 1, "Private tab should not have been deleted") + XCTAssertNotEqual(manager.selectedIndex, -1, "The selected index should have been reset") + + // didSelect should still be called when switching between a tab + let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in + XCTAssertNotNil(tabs[1], "there should be a previous tab") + let next = tabs[0]! + XCTAssertFalse(next.isPrivate) + } + + // make sure delegate method is actually called + delegate.expect([didSelect]) + + // select the new tab to trigger the delegate methods + manager.selectTab(newTab) + + // check + delegate.verify("Not all delegate methods were called") + + setPersistentPrivateMode(false) + } + + func testDelegatesCalledWhenRemovingPrivateTabsNonPersistence() { + setPersistentPrivateMode(false) + //setup let delegate = MockTabManagerDelegate() diff --git a/Tests/ClientTests/TabSessionTests.swift b/Tests/ClientTests/TabSessionTests.swift index 3dbd52d6e92..b3b30b391ee 100644 --- a/Tests/ClientTests/TabSessionTests.swift +++ b/Tests/ClientTests/TabSessionTests.swift @@ -77,6 +77,7 @@ private class WebViewNavigationAdapter: NSObject, WKNavigationDelegate { @MainActor class TabSessionTests: XCTestCase { private var tabManager: TabManager! private let maxTimeout = 60.0 + private let privateBrowsingManager = PrivateBrowsingManager() override class func setUp() { super.setUp() @@ -89,7 +90,7 @@ private class WebViewNavigationAdapter: NSObject, WKNavigationDelegate { DataController.shared.initializeOnce() tabManager = { () -> TabManager in let profile = BrowserProfile(localName: "profile") - return TabManager(prefs: profile.prefs, rewards: nil, tabGeneratorAPI: nil) + return TabManager(windowId: UUID(), prefs: profile.prefs, rewards: nil, tabGeneratorAPI: nil, privateBrowsingManager: privateBrowsingManager) }() } diff --git a/Tests/ClientTests/TestFavicons.swift b/Tests/ClientTests/TestFavicons.swift index d70be03b8ec..fee90987000 100644 --- a/Tests/ClientTests/TestFavicons.swift +++ b/Tests/ClientTests/TestFavicons.swift @@ -28,7 +28,7 @@ import Favicon func testImageViewLoad() { let expectation = XCTestExpectation(description: "favicon.load") let imageView = UIImageView() - imageView.loadFavicon(for: URL(string: "http://www.google.de")!, monogramFallbackCharacter: nil) { _ in + imageView.loadFavicon(for: URL(string: "http://www.google.de")!, isPrivateBrowsing: false, monogramFallbackCharacter: nil) { _ in // Should be a default icon therefore not truly async XCTAssertNotNil(imageView.image) expectation.fulfill() diff --git a/package-lock.json b/package-lock.json index a50ff466451..caf22c88817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@mozilla/readability": "^0.4.2", "brave-core-ios": "https://github.com/brave/brave-browser/releases/download/v1.57.13/brave-core-ios-1.57.13.tgz", - "leo-sf-symbols": "github:brave/leo-sf-symbols#60a41d77d4e58bd48284848a04ad3f9b79bf7daa", + "leo-sf-symbols": "github:brave/leo-sf-symbols#9d272b2fdccdfced7785ee3b527b33df5a33a989", "page-metadata-parser": "^1.1.3", "webpack-cli": "^4.8.0" }, @@ -783,9 +783,9 @@ } }, "node_modules/leo-sf-symbols": { - "version": "1.0.7", - "resolved": "git+ssh://git@github.com/brave/leo-sf-symbols.git#60a41d77d4e58bd48284848a04ad3f9b79bf7daa", - "integrity": "sha512-234URdy8AbdEYRQC+YybQ6JGQiKMztPgBTt6lILS64udhr/3nj0d6pT3xa4mqRhX6TYmSyFRTWxKUjZ2ZmI2SQ==", + "version": "1.0.9", + "resolved": "git+ssh://git@github.com/brave/leo-sf-symbols.git#9d272b2fdccdfced7785ee3b527b33df5a33a989", + "integrity": "sha512-pH1vv7MeTBfORv7rIX8lVBC+7Cs3xk8IRcVVcfZWGsDcza9l2AegWr56xYSGk2HyiTwBzsdMIBkEyOf64QXrlw==", "license": "MPL-2.0" }, "node_modules/loader-runner": { @@ -2055,9 +2055,9 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "leo-sf-symbols": { - "version": "git+ssh://git@github.com/brave/leo-sf-symbols.git#60a41d77d4e58bd48284848a04ad3f9b79bf7daa", - "integrity": "sha512-234URdy8AbdEYRQC+YybQ6JGQiKMztPgBTt6lILS64udhr/3nj0d6pT3xa4mqRhX6TYmSyFRTWxKUjZ2ZmI2SQ==", - "from": "leo-sf-symbols@github:brave/leo-sf-symbols#60a41d77d4e58bd48284848a04ad3f9b79bf7daa" + "version": "git+ssh://git@github.com/brave/leo-sf-symbols.git#9d272b2fdccdfced7785ee3b527b33df5a33a989", + "integrity": "sha512-pH1vv7MeTBfORv7rIX8lVBC+7Cs3xk8IRcVVcfZWGsDcza9l2AegWr56xYSGk2HyiTwBzsdMIBkEyOf64QXrlw==", + "from": "leo-sf-symbols@github:brave/leo-sf-symbols#9d272b2fdccdfced7785ee3b527b33df5a33a989" }, "loader-runner": { "version": "4.2.0", diff --git a/package.json b/package.json index 4725d50cb23..8cbadc54525 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dependencies": { "@mozilla/readability": "^0.4.2", "brave-core-ios": "https://github.com/brave/brave-browser/releases/download/v1.57.13/brave-core-ios-1.57.13.tgz", - "leo-sf-symbols": "github:brave/leo-sf-symbols#60a41d77d4e58bd48284848a04ad3f9b79bf7daa", + "leo-sf-symbols": "github:brave/leo-sf-symbols#9d272b2fdccdfced7785ee3b527b33df5a33a989", "page-metadata-parser": "^1.1.3", "webpack-cli": "^4.8.0" },