Revision control

Copy as Markdown

// 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 UIKit
/// The `ThemeManager` will be responsible for providing the theme throughout the app
public final class DefaultThemeManager: ThemeManager, Notifiable {
// These have been carried over from the legacy system to maintain backwards compatibility
enum ThemeKeys {
static let themeName = "prefKeyThemeName"
static let systemThemeIsOn = "prefKeySystemThemeSwitchOnOff"
static let hasMigratedToNewAppearanceMenu = "prefKeyhasMigratedToNewAppearanceMenu"
enum AutomaticBrightness {
static let isOn = "prefKeyAutomaticSwitchOnOff"
static let thresholdValue = "prefKeyAutomaticSliderValue"
}
enum NightMode {
static let isOn = "profile.NightModeStatus"
}
}
// MARK: - Variables
private var windows: [WindowUUID: UIWindow] = [:]
private var privateBrowsingState: [WindowUUID: Bool] = [:]
private var allWindowUUIDs: [WindowUUID] { return Array(windows.keys) }
public var notificationCenter: NotificationProtocol
private var userDefaults: UserDefaultsInterface
private var mainQueue: DispatchQueueInterface
private var sharedContainerIdentifier: String
private var isNewAppearanceMenuOnClosure: () -> Bool
private var nightModeIsOn: Bool {
return userDefaults.bool(forKey: ThemeKeys.NightMode.isOn)
}
public var systemThemeIsOn: Bool {
return userDefaults.bool(forKey: ThemeKeys.systemThemeIsOn)
}
public var automaticBrightnessIsOn: Bool {
return userDefaults.bool(forKey: ThemeKeys.AutomaticBrightness.isOn)
}
public var automaticBrightnessValue: Float {
return userDefaults.float(forKey: ThemeKeys.AutomaticBrightness.thresholdValue)
}
public var isNewAppearanceMenuOn: Bool {
return isNewAppearanceMenuOnClosure()
}
public var hasMigratedToNewAppearanceMenu: Bool {
return userDefaults.bool(forKey: ThemeKeys.hasMigratedToNewAppearanceMenu)
}
// MARK: - Initializers
public init(
userDefaults: UserDefaultsInterface = UserDefaults.standard,
notificationCenter: NotificationProtocol = NotificationCenter.default,
mainQueue: DispatchQueueInterface = DispatchQueue.main,
sharedContainerIdentifier: String,
isNewAppearanceMenuOnClosure: @escaping () -> Bool = { false }
) {
self.userDefaults = userDefaults
self.notificationCenter = notificationCenter
self.mainQueue = mainQueue
self.sharedContainerIdentifier = sharedContainerIdentifier
self.isNewAppearanceMenuOnClosure = isNewAppearanceMenuOnClosure
self.userDefaults.register(defaults: [
ThemeKeys.systemThemeIsOn: true,
ThemeKeys.NightMode.isOn: false
])
setupNotifications(forObserver: self,
observing: [UIScreen.brightnessDidChangeNotification,
UIApplication.didBecomeActiveNotification])
}
// MARK: - Themeing general functions
public func getCurrentTheme(for window: WindowUUID?) -> Theme {
guard let window else {
assertionFailure("Attempt to get the theme for a nil window UUID.")
return DarkTheme()
}
return getThemeFrom(type: determineThemeType(for: window))
}
public func resolvedTheme(with shouldShowPrivateTheme: Bool) -> Theme {
return shouldShowPrivateTheme ? PrivateModeTheme() : getThemeFrom(type: determineUserTheme())
}
public func applyThemeUpdatesToWindows() {
allWindowUUIDs.forEach {
applyThemeChanges(for: $0, using: determineThemeType(for: $0))
}
}
// MARK: - Manual theme functions
public func setManualTheme(to newTheme: ThemeType) {
updateSavedTheme(to: newTheme)
applyThemeUpdatesToWindows()
}
public func getUserManualTheme() -> ThemeType {
guard let savedThemeDescription = userDefaults.string(forKey: ThemeKeys.themeName),
let savedTheme = ThemeType(rawValue: savedThemeDescription)
else { return getThemeTypeBasedOnSystem() }
return savedTheme
}
// MARK: - System theme functions
public func setSystemTheme(isOn: Bool) {
userDefaults.set(isOn, forKey: ThemeKeys.systemThemeIsOn)
applyThemeUpdatesToWindows()
}
private func getThemeTypeBasedOnSystem() -> ThemeType {
return UIScreen.main.traitCollection.userInterfaceStyle == .dark ? ThemeType.dark : ThemeType.light
}
// MARK: - Private theme functions
public func setPrivateTheme(isOn: Bool, for window: WindowUUID) {
guard getPrivateThemeIsOn(for: window) != isOn else { return }
privateBrowsingState[window] = isOn
applyThemeChanges(for: window, using: determineThemeType(for: window))
}
public func getPrivateThemeIsOn(for window: WindowUUID) -> Bool {
return privateBrowsingState[window] ?? false
}
// MARK: - Automatic brightness theme functions
public func setAutomaticBrightness(isOn: Bool) {
guard automaticBrightnessIsOn != isOn else { return }
userDefaults.set(isOn, forKey: ThemeKeys.AutomaticBrightness.isOn)
applyThemeUpdatesToWindows()
}
public func setAutomaticBrightnessValue(_ value: Float) {
userDefaults.set(value, forKey: ThemeKeys.AutomaticBrightness.thresholdValue)
applyThemeUpdatesToWindows()
}
private func getThemeTypeBasedOnBrightness() -> ThemeType {
return Float(UIScreen.main.brightness) < automaticBrightnessValue ? .dark : .light
}
// MARK: - Window specific functions
public func windowNonspecificTheme() -> Theme {
switch getUserManualTheme() {
case .dark, .nightMode, .privateMode: return DarkTheme()
case .light: return LightTheme()
}
}
public func windowDidClose(uuid: WindowUUID) {
windows.removeValue(forKey: uuid)
}
public func setWindow(_ window: UIWindow, for uuid: WindowUUID) {
windows[uuid] = window
updateSavedTheme(to: getUserManualTheme())
applyThemeChanges(for: uuid, using: determineThemeType(for: uuid))
}
// MARK: - Private helper methods
private func updateSavedTheme(to newTheme: ThemeType) {
userDefaults.set(newTheme.rawValue, forKey: ThemeKeys.themeName)
}
private func applyThemeChanges(for window: WindowUUID, using newTheme: ThemeType) {
// Overwrite the user interface style on the window attached to our scene
// once we have multiple scenes we need to update all of them
let style = self.getCurrentTheme(for: window).type.getInterfaceStyle()
self.windows[window]?.overrideUserInterfaceStyle = style
notifyCurrentThemeDidChange(for: window)
}
private func notifyCurrentThemeDidChange(for window: WindowUUID) {
mainQueue.ensureMainThread { [weak self] in
self?.notificationCenter.post(
name: .ThemeDidChange,
withUserInfo: window.userInfo
)
}
}
private func determineThemeType(for window: WindowUUID) -> ThemeType {
if getPrivateThemeIsOn(for: window) { return .privateMode }
return determineUserTheme()
}
private func determineUserTheme() -> ThemeType {
// Check if a migration override should be applied. This is mainly done because the new behaviour splits
// dark theme appearance of the app and web content. Once FXIOS-11655, both this check and nightMode
// in general will be removed.
if let migratedTheme = migratedTheme() { return migratedTheme }
if !isNewAppearanceMenuOn && nightModeIsOn { return .nightMode }
if systemThemeIsOn { return getThemeTypeBasedOnSystem() }
if automaticBrightnessIsOn { return getThemeTypeBasedOnBrightness() }
return getUserManualTheme()
}
private func getThemeFrom(type: ThemeType) -> Theme {
switch type {
case .light:
return LightTheme()
case .dark:
return DarkTheme()
case .nightMode:
return NightModeTheme()
case .privateMode:
return PrivateModeTheme()
}
}
// MARK: - Notifiable
public func handleNotifications(_ notification: Notification) {
switch notification.name {
case UIScreen.brightnessDidChangeNotification,
UIApplication.didBecomeActiveNotification:
applyThemeUpdatesToWindows()
default:
return
}
}
/// Checks if theme migration should override the current theme selection.
/// Returns:
/// - .dark if migration conditions are met and NightMode is active.
/// - nil otherwise.
/// NOTE(FXIOS-11655): This code will be removed once the new appearance menu experiment ends.
private func migratedTheme() -> ThemeType? {
if isNewAppearanceMenuOn && !hasMigratedToNewAppearanceMenu {
// Mark that migration has been performed to avoid repeating the process.
userDefaults.set(true, forKey: ThemeKeys.hasMigratedToNewAppearanceMenu)
if nightModeIsOn {
// If nightMode was on, force dark mode in the new UI and update all other themes.
updateSavedTheme(to: .dark)
setSystemTheme(isOn: false)
setAutomaticBrightness(isOn: false)
return .dark
} else if automaticBrightnessIsOn {
// If automaticBrightness was on, apply the computed theme.
updateSavedTheme(to: getThemeTypeBasedOnBrightness())
setSystemTheme(isOn: false)
setAutomaticBrightness(isOn: false)
}
} else if !isNewAppearanceMenuOn && hasMigratedToNewAppearanceMenu {
// Reset the migration flag (mostly for debugging or rare cases).
userDefaults.set(false, forKey: ThemeKeys.hasMigratedToNewAppearanceMenu)
}
return nil
}
}