Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #8362: Tabs Disappearing (#8420)
Browse files Browse the repository at this point in the history
Fixed Migration for lost tabs.
Fixed Window restoration when we don't get an activity from Apple.
  • Loading branch information
Brandon-T authored and iccub committed Nov 16, 2023
1 parent 95e1456 commit 66c6548
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 79 deletions.
7 changes: 6 additions & 1 deletion App/iOS/Delegates/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
1 change: 1 addition & 0 deletions App/iOS/Delegates/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class AppState {
DataController.shared.initializeOnce()
Migration.postCoreDataInitMigrations()
Migration.migrateTabStateToWebkitState(diskImageStore: diskImageStore)
Migration.migrateLostTabsActiveWindow()
}
break
case .active:
Expand Down
141 changes: 82 additions & 59 deletions App/iOS/Delegates/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,7 +36,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
static var shouldHandleInstallAttributionFetch = false

private var cancellables: Set<AnyCancellable> = []
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 }
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -232,13 +237,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
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
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
24 changes: 10 additions & 14 deletions Sources/Brave/Frontend/Browser/BrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Brave/Frontend/Browser/NavigationRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
48 changes: 48 additions & 0 deletions Sources/Brave/Migration/Migration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -237,6 +280,11 @@ fileprivate extension Preferences {
static let adBlockAndTrackingProtectionShieldLevelCompleted = Option<Bool>(
key: "migration.ad-block-and-tracking-protection-shield-level-completed", default: false
)

static let lostTabsWindowIDMigrationOne = Option<Bool>(
key: "migration.lost-tabs-window-id-one",
default: !UIApplication.shared.supportsMultipleScenes
)
}

/// Migrate a given key from `Prefs` into a specific option
Expand Down
Loading

0 comments on commit 66c6548

Please sign in to comment.