diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index d4959f9e69a..3e2983c05a9 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -368,7 +368,12 @@ extension AppDelegate { // 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) { + if let windowIdString = BrowserState.getWindowInfo(from: session).windowId, + let windowId = UUID(uuidString: windowIdString) { + SessionWindow.delete(windowId: windowId) + } else if let userActivity = session.scene?.userActivity, + let windowIdString = BrowserState.getWindowInfo(from: userActivity).windowId, + let windowId = UUID(uuidString: windowIdString) { SessionWindow.delete(windowId: windowId) } } diff --git a/App/iOS/Delegates/AppState.swift b/App/iOS/Delegates/AppState.swift index 380c74e3ce1..20b43cb7562 100644 --- a/App/iOS/Delegates/AppState.swift +++ b/App/iOS/Delegates/AppState.swift @@ -50,6 +50,7 @@ public class AppState { DataController.shared.initializeOnce() Migration.postCoreDataInitMigrations() Migration.migrateTabStateToWebkitState(diskImageStore: diskImageStore) + Migration.migrateLostTabsActiveWindow() } break case .active: diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index e8d38da7b0c..df4a2523306 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -20,6 +20,12 @@ import BraveCore import BraveNews import Preferences +private extension Logger { + static var module: Logger { + .init(subsystem: "\(Bundle.main.bundleIdentifier ?? "com.brave.ios")", category: "SceneDelegate") + } +} + class SceneDelegate: UIResponder, UIWindowSceneDelegate { // This property must be non-null because even though it's optional, @@ -30,7 +36,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { static var shouldHandleInstallAttributionFetch = 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 } @@ -48,8 +53,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let conditions = scene.activationConditions conditions.canActivateForTargetContentIdentifierPredicate = NSPredicate(value: true) - if let windowId = session.userInfo?["WindowID"] as? UUID { - let preferPredicate = NSPredicate(format: "self == %@", windowId.uuidString) + if let windowId = session.userInfo?["WindowID"] as? String { + let preferPredicate = NSPredicate(format: "self == %@", windowId) conditions.prefersToActivateForTargetContentIdentifierPredicate = preferPredicate } @@ -156,7 +161,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let response = connectionOptions.notificationResponse { if response.notification.request.identifier == BrowserViewController.defaultBrowserNotificationId { guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { - log.error("Failed to unwrap iOS settings URL") + Logger.module.error("[SCENE] - Failed to unwrap iOS settings URL") return } UIApplication.shared.open(settingsUrl) @@ -167,7 +172,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidDisconnect(_ scene: UIScene) { - log.debug("SCENE DISCONNECTED") + Logger.module.debug("[SCENE] - Scene Disconnected") } func sceneDidBecomeActive(_ scene: UIScene) { @@ -232,13 +237,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let scene = scene as? UIWindowScene else { - log.debug("Invalid Scene - Scene is not a UIWindowScene") + Logger.module.error("[SCENE] - Scene is not a UIWindowScene") return } URLContexts.forEach({ guard let routerpath = NavigationPath(url: $0.url, isPrivateBrowsing: scene.browserViewController?.privateBrowsingManager.isPrivateBrowsing == true) else { - log.debug("Invalid Navigation Path: \($0.url)") + Logger.module.error("[SCENE] - Invalid Navigation Path: \($0.url)") return } @@ -247,7 +252,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) { - log.debug("Updated User Activity for Scene") + Logger.module.debug("[SCENE] - Updated User Activity for Scene") } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { @@ -416,68 +421,86 @@ extension SceneDelegate { 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 - urlToOpen = userActivity.userInfo?["OpenURL"] as? URL + let windowInfo = BrowserState.getWindowInfo(from: userActivity) + windowId = UUID(uuidString: windowInfo.windowId ?? "") ?? UUID() + isPrivate = windowInfo.isPrivate + urlToOpen = windowInfo.openURL privateBrowsingManager.isPrivateBrowsing = isPrivate // Create a new session window - SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: windowId) - - scene.userActivity = BrowserState.userActivity(for: windowId, isPrivate: isPrivate) - 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) { + SessionWindow.createWindow(isPrivate: isPrivate, isSelected: true, uuid: windowId) - // Restore the scene from the Session's User-Info WindowID + scene.userActivity = BrowserState.userActivity(for: windowId.uuidString, isPrivate: isPrivate) + BrowserState.setWindowInfo(for: scene.session, windowId: windowId.uuidString, isPrivate: isPrivate) - windowId = windowUUID - isPrivate = sceneIsPrivate - privateBrowsingManager.isPrivateBrowsing = sceneIsPrivate - urlToOpen = scene.session.userInfo?["OpenURL"] as? URL - - scene.userActivity = BrowserState.userActivity(for: windowId, isPrivate: isPrivate) + Logger.module.info("[SCENE] - USER ACTIVITY RESTORED") } 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 }) + let windowInfo = BrowserState.getWindowInfo(from: scene.session) + if let sceneWindowId = windowInfo.windowId, + let windowUUID = UUID(uuidString: sceneWindowId) { + + // Restore the scene from the Session's User-Info WindowID + windowId = windowUUID + isPrivate = windowInfo.isPrivate + privateBrowsingManager.isPrivateBrowsing = windowInfo.isPrivate + urlToOpen = windowInfo.openURL - 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 + scene.userActivity = BrowserState.userActivity(for: windowId.uuidString, isPrivate: isPrivate) + BrowserState.setWindowInfo(for: scene.session, windowId: windowId.uuidString, isPrivate: isPrivate) + + Logger.module.info("[SCENE] - SCENE SESSION RESTORED") + } else if UIApplication.shared.supportsMultipleScenes { + if let activeWindowId = SessionWindow.getActiveWindow(context: DataController.swiftUIContext)?.windowId { + let activeSession = UIApplication.shared.openSessions + .compactMap({ BrowserState.getWindowInfo(from: $0) }) + .first(where: { $0.windowId == activeWindowId.uuidString }) + + if activeSession != nil { + // An existing window is already active on screen + // So create a new window + windowId = UUID() + SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: windowId) + Logger.module.info("[SCENE] - CREATED NEW WINDOW") + } else { + // Restore the active window since none is active on screen + windowId = activeWindowId + Logger.module.info("[SCENE] - RESTORING ACTIVE WINDOW ID") + } } 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. + windowId = UUID() + SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: windowId) + Logger.module.info("[SCENE] - WE HIT THE IMPOSSIBLE! - CREATING A NEW WINDOW ON MULTI-SCENE DEVICE!") + } + + isPrivate = false + privateBrowsingManager.isPrivateBrowsing = false + urlToOpen = nil + + scene.userActivity = BrowserState.userActivity(for: windowId.uuidString, isPrivate: false) + BrowserState.setWindowInfo(for: scene.session, windowId: windowId.uuidString, isPrivate: false) + } else { + // iPhones don't have a userActivity or session user info + if let activeWindowId = SessionWindow.getActiveWindow(context: DataController.swiftUIContext)?.windowId { // Restore the active window since none is active on screen windowId = activeWindowId + Logger.module.info("[SCENE] - RESTORING ACTIVE WINDOW ID") + } 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. + windowId = UUID() + SessionWindow.createWindow(isPrivate: false, isSelected: true, uuid: windowId) + Logger.module.info("[SCENE] - WE HIT THE IMPOSSIBLE! - CREATING A NEW WINDOW!") } - } 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.uuidString, isPrivate: false) + BrowserState.setWindowInfo(for: scene.session, windowId: windowId.uuidString, isPrivate: false) } - - 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 @@ -509,7 +532,7 @@ extension SceneDelegate { 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 { + guard let sceneWindowId = BrowserState.getWindowInfo(from: $0.session).windowId else { return false } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController.swift index 02fc2712fa7..c1262de2d14 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController.swift @@ -1168,17 +1168,17 @@ public class BrowserViewController: UIViewController { let isPrivateBrowsing = SessionWindow.from(windowId: windowId)?.isPrivate == true var userActivity = view.window?.windowScene?.userActivity - if userActivity == nil { - userActivity = BrowserState.userActivity(for: windowId, isPrivate: isPrivateBrowsing) + + if let userActivity = userActivity { + BrowserState.setWindowInfo(for: userActivity, windowId: windowId.uuidString, isPrivate: isPrivateBrowsing) } else { - userActivity?.targetContentIdentifier = windowId.uuidString - userActivity?.addUserInfoEntries(from: ["WindowID": windowId.uuidString, - "isPrivate": isPrivateBrowsing]) + userActivity = BrowserState.userActivity(for: windowId.uuidString, isPrivate: isPrivateBrowsing) } - view.window?.windowScene?.userActivity = userActivity - view.window?.windowScene?.session.userInfo = ["WindowID": windowId.uuidString, - "isPrivate": isPrivateBrowsing] + if let scene = view.window?.windowScene { + scene.userActivity = userActivity + BrowserState.setWindowInfo(for: scene.session, windowId: windowId.uuidString, isPrivate: isPrivateBrowsing) + } for session in UIApplication.shared.openSessions { UIApplication.shared.requestSceneSessionRefresh(session) @@ -1996,12 +1996,8 @@ 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 activity = BrowserState.userActivity(for: UUID().uuidString, isPrivate: isPrivate, openURL: url) + let options = UIScene.ActivationRequestOptions().then { $0.requestingScene = view.window?.windowScene } diff --git a/Sources/Brave/Frontend/Browser/NavigationRouter.swift b/Sources/Brave/Frontend/Browser/NavigationRouter.swift index 62f607d26da..29706cdf34a 100644 --- a/Sources/Brave/Frontend/Browser/NavigationRouter.swift +++ b/Sources/Brave/Frontend/Browser/NavigationRouter.swift @@ -23,7 +23,7 @@ public enum NavigationPath: Equatable { public init?(url: URL, isPrivateBrowsing: Bool) { let urlString = url.absoluteString - if url.scheme == "http" || url.scheme == "https" || url.isIPFSScheme { + if url.scheme?.lowercased() == "http" || url.scheme?.lowercased() == "https" || url.isIPFSScheme { self = .url(webURL: url, isPrivate: isPrivateBrowsing) return } diff --git a/Sources/Brave/Migration/Migration.swift b/Sources/Brave/Migration/Migration.swift index b763446daaf..44a6dc13387 100644 --- a/Sources/Brave/Migration/Migration.swift +++ b/Sources/Brave/Migration/Migration.swift @@ -165,6 +165,49 @@ public class Migration { Preferences.Migration.tabMigrationToInteractionStateCompleted.value = true } } + + public static func migrateLostTabsActiveWindow() { + if UIApplication.shared.supportsMultipleScenes { return } + if Preferences.Migration.lostTabsWindowIDMigrationOne.value { return } + + let sessionWindows = SessionWindow.all() + guard let activeWindow = sessionWindows.first(where: { $0.isSelected }) else { + return + } + + let windowIds = UIApplication.shared.openSessions + .compactMap({ BrowserState.getWindowInfo(from: $0).windowId }) + .filter({ $0 != activeWindow.windowId.uuidString }) + + let zombieTabs = sessionWindows + .filter({ windowIds.contains($0.windowId.uuidString) }) + .compactMap({ + $0.sessionTabs + }) + .flatMap({ $0 }) + + if !zombieTabs.isEmpty { + let activeURLs = activeWindow.sessionTabs?.compactMap({ $0.url }) ?? [] + + // Restore private tabs if persistency is enabled + if Preferences.Privacy.persistentPrivateBrowsing.value { + zombieTabs.filter({ $0.isPrivate }).forEach { + if !activeURLs.contains($0.url) { + SessionTab.move(tab: $0.tabId, toWindow: activeWindow.windowId) + } + } + } + + // Restore regular tabs + zombieTabs.filter({ !$0.isPrivate }).forEach { + if !activeURLs.contains($0.url) { + SessionTab.move(tab: $0.tabId, toWindow: activeWindow.windowId) + } + } + } + + Preferences.Migration.lostTabsWindowIDMigrationOne.value = true + } public static func postCoreDataInitMigrations() { if Preferences.Migration.coreDataCompleted.value { return } @@ -237,6 +280,11 @@ fileprivate extension Preferences { static let adBlockAndTrackingProtectionShieldLevelCompleted = Option( key: "migration.ad-block-and-tracking-protection-shield-level-completed", default: false ) + + static let lostTabsWindowIDMigrationOne = Option( + key: "migration.lost-tabs-window-id-one", + default: !UIApplication.shared.supportsMultipleScenes + ) } /// Migrate a given key from `Prefs` into a specific option diff --git a/Sources/Brave/States/BrowserState.swift b/Sources/Brave/States/BrowserState.swift index c2165f18a45..06752ce75bf 100644 --- a/Sources/Brave/States/BrowserState.swift +++ b/Sources/Brave/States/BrowserState.swift @@ -17,13 +17,74 @@ public class BrowserState { self.profile = profile } - public static func userActivity(for windowId: UUID, isPrivate: Bool) -> NSUserActivity { + public static func userActivity(for windowId: String, isPrivate: Bool, openURL: URL? = nil) -> NSUserActivity { return NSUserActivity(activityType: sceneId).then { - $0.targetContentIdentifier = windowId.uuidString + $0.targetContentIdentifier = windowId $0.addUserInfoEntries(from: [ - "WindowID": windowId.uuidString, - "isPrivate": isPrivate + SessionState.windowIDKey: windowId, + SessionState.isPrivateKey: isPrivate, ]) + + if let openURL = openURL { + $0.addUserInfoEntries(from: [ + SessionState.openURLKey: openURL + ]) + } } } + + public static func setWindowInfo(for activity: NSUserActivity, windowId: String, isPrivate: Bool) { + if activity.userInfo == nil { + activity.userInfo = [:] + } + + activity.targetContentIdentifier = windowId + activity.addUserInfoEntries(from: [ + SessionState.windowIDKey: windowId, + SessionState.isPrivateKey: isPrivate + ]) + } + + public static func getWindowInfo(from session: UISceneSession) -> SessionState { + guard let userInfo = session.userInfo else { + return SessionState(windowId: nil, isPrivate: false, openURL: nil) + } + + return SessionState(windowId: userInfo[SessionState.windowIDKey] as? String, + isPrivate: userInfo[SessionState.isPrivateKey] as? Bool == true, + openURL: userInfo[SessionState.openURLKey] as? URL) + } + + public static func getWindowInfo(from activity: NSUserActivity) -> SessionState { + guard let userInfo = activity.userInfo else { + return SessionState(windowId: nil, isPrivate: false, openURL: nil) + } + + return SessionState(windowId: userInfo[SessionState.windowIDKey] as? String, + isPrivate: userInfo[SessionState.isPrivateKey] as? Bool == true, + openURL: userInfo[SessionState.openURLKey] as? URL) + } + + public static func setWindowInfo(for session: UISceneSession, windowId: String, isPrivate: Bool) { + let userInfo: [String: Any] = [ + SessionState.windowIDKey: windowId, + SessionState.isPrivateKey: isPrivate + ] + + if session.userInfo == nil { + session.userInfo = userInfo + } else { + session.userInfo?.merge(with: userInfo) + } + } + + public struct SessionState { + public let windowId: String? + public let isPrivate: Bool + public let openURL: URL? + + static let windowIDKey = "WindowID" + static let isPrivateKey = "isPrivate" + static let openURLKey = "OpenURL" + } }