From c784569040e9be09342a6faf4069ff38175c68ba Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 7 Jul 2024 16:23:47 -0700 Subject: [PATCH] Make AccountManager init itself, and use AccountManager.shared in AppDelegate. --- Mac/AppDelegate.swift | 43 +++++++------- .../Sources/Account/AccountManager.swift | 20 +++---- Modules/Core/Sources/Core/AppConfig.swift | 30 +++++----- iOS/AppDelegate.swift | 56 ++++++++----------- 4 files changed, 68 insertions(+), 81 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 4cf830285..0cf5fdab1 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -101,21 +101,16 @@ import Sparkle #endif private var themeImportPath: String? - private let accountManager: AccountManager override init() { NSWindow.allowsAutomaticWindowTabbing = false - - self.accountManager = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!) - AccountManager.shared = self.accountManager - super.init() #if !MAC_APP_STORE let crashReporterConfig = PLCrashReporterConfig.defaultConfiguration() - crashReporter = PLCrashReporter(configuration: crashReporterConfig) - crashReporter.enable() + self.crashReporter = PLCrashReporter(configuration: crashReporterConfig) + self.crashReporter.enable() #endif NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) @@ -172,9 +167,9 @@ import Sparkle FaviconGenerator.faviconTemplateImage = AppAssets.faviconTemplateImage - let localAccount = accountManager.defaultAccount + let localAccount = AccountManager.shared.defaultAccount - if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() { + if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { // Import feeds. Either old NNW 3 feeds or the default feeds. if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) { DefaultFeedsImporter.importDefaultFeeds(account: localAccount) @@ -197,7 +192,7 @@ import Sparkle NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) Task { - self.unreadCount = self.accountManager.unreadCount + self.unreadCount = AccountManager.shared.unreadCount } if InspectorWindowController.shouldOpenAtStartup { @@ -290,7 +285,7 @@ import Sparkle ArticleStringFormatter.emptyCaches() MultilineTextFieldSizer.emptyCache() IconImageCache.shared.emptyCache() - accountManager.emptyCaches() + AccountManager.shared.emptyCaches() saveState() @@ -302,7 +297,7 @@ import Sparkle func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { Task { - await self.accountManager.receiveRemoteNotification(userInfo: userInfo) + await AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) } } @@ -319,7 +314,7 @@ import Sparkle ArticleThemeDownloader.cleanUp() Task { - await accountManager.sendArticleStatusAll() + await AccountManager.shared.sendArticleStatusAll() self.isShutDownSyncDone = true } @@ -330,7 +325,7 @@ import Sparkle // MARK: Notifications @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { - unreadCount = accountManager.unreadCount + unreadCount = AccountManager.shared.unreadCount } } @@ -430,15 +425,15 @@ import Sparkle let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false if item.action == #selector(refreshAll(_:)) { - return !accountManager.refreshInProgress && !accountManager.activeAccounts.isEmpty + return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty } if item.action == #selector(importOPMLFromFile(_:)) { - return accountManager.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) + return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) } if item.action == #selector(addAppNews(_:)) { - return !isDisplayingSheet && !accountManager.anyAccountHasNetNewsWireNewsSubscription() && !accountManager.activeAccounts.isEmpty + return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty } if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) { @@ -446,7 +441,7 @@ import Sparkle } if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { - return !isDisplayingSheet && !accountManager.activeAccounts.isEmpty + return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty } #if !MAC_APP_STORE @@ -526,7 +521,7 @@ import Sparkle @IBAction func refreshAll(_ sender: Any?) { Task { - await accountManager.refreshAll(errorHandler: ErrorHandler.present) + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) } } @@ -601,7 +596,7 @@ import Sparkle } @IBAction func addAppNews(_ sender: Any?) { - if accountManager.anyAccountHasNetNewsWireNewsSubscription() { + if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { return } addFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News") @@ -698,12 +693,12 @@ import Sparkle extension AppDelegate { @IBAction func debugSearch(_ sender: Any?) { - accountManager.defaultAccount.debugRunSearch() + AccountManager.shared.defaultAccount.debugRunSearch() } @IBAction func debugDropConditionalGetInfo(_ sender: Any?) { #if DEBUG - for account in accountManager.activeAccounts { + for account in AccountManager.shared.activeAccounts { account.debugDropConditionalGetInfo() } #endif @@ -969,7 +964,7 @@ private extension AppDelegate { func handleMarkAsRead(articlePathInfo: ArticlePathInfo) { - guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { + guard let accountID = articlePathInfo.accountID, let account = AccountManager.shared.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } @@ -989,7 +984,7 @@ private extension AppDelegate { func handleMarkAsStarred(articlePathInfo: ArticlePathInfo) { - guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { + guard let accountID = articlePathInfo.accountID, let account = AccountManager.shared.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } diff --git a/Modules/Account/Sources/Account/AccountManager.swift b/Modules/Account/Sources/Account/AccountManager.swift index 13c50013b..1e1a59d89 100644 --- a/Modules/Account/Sources/Account/AccountManager.swift +++ b/Modules/Account/Sources/Account/AccountManager.swift @@ -11,6 +11,7 @@ import Web import Articles import ArticlesDatabase import Database +import Core @MainActor public final class AccountManager: UnreadCountProvider { @@ -20,7 +21,11 @@ import Database public let defaultAccount: Account - private let accountsFolder: String + private let accountsFolderURL: URL + private var accountsFolder: String { + accountsFolderURL.path + } + private var accountsDictionary = [String: Account]() private let defaultAccountFolderName = "OnMyMac" @@ -91,19 +96,12 @@ import Database return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray) } - public init(accountsFolder: String) { + public init() { - self.accountsFolder = accountsFolder + self.accountsFolderURL = AppConfig.dataSubfolder(named: "Accounts") // The local "On My Mac" account must always exist, even if it's empty. - let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac") - do { - try FileManager.default.createDirectory(atPath: localAccountFolder, withIntermediateDirectories: true, attributes: nil) - } - catch { - assertionFailure("Could not create folder for OnMyMac account.") - abort() - } + let localAccountFolder = AppConfig.ensureSubfolder(named: "OnMyMac", folderURL: self.accountsFolderURL).path defaultAccount = Account(dataFolder: localAccountFolder, type: .onMyMac, accountID: defaultAccountIdentifier) accountsDictionary[defaultAccount.accountID] = defaultAccount diff --git a/Modules/Core/Sources/Core/AppConfig.swift b/Modules/Core/Sources/Core/AppConfig.swift index 0af0efefd..efdfc95f4 100644 --- a/Modules/Core/Sources/Core/AppConfig.swift +++ b/Modules/Core/Sources/Core/AppConfig.swift @@ -22,7 +22,7 @@ public final class AppConfig { let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) let tempFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) folderURL = URL(fileURLWithPath: tempFolder, isDirectory: true) - try! FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) + createFolderIfNecessary(folderURL) } return folderURL @@ -30,36 +30,38 @@ public final class AppConfig { /// Returns URL to subfolder in cache folder (creating the folder if it doesn’t exist) public static func cacheSubfolder(named name: String) -> URL { - subfolder(name, in: cacheFolder) + ensureSubfolder(named: name, folderURL: cacheFolder) } public static let dataFolder: URL = { #if os(macOS) - var dataFolder = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + var dataFolder = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) dataFolder = dataFolder.appendingPathComponent(appName) - - try! FileManager.default.createDirectory(at: dataFolder, withIntermediateDirectories: true, attributes: nil) - return dataFolder - #elseif os(iOS) - FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + var dataFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! #endif + + createFolderIfNecessary(dataFolder) + return dataFolder }() /// Returns URL to subfolder in data folder (creating the folder if it doesn’t exist) public static func dataSubfolder(named name: String) -> URL { - subfolder(name, in: dataFolder) + ensureSubfolder(named: name, folderURL: dataFolder) } + public static func ensureSubfolder(named name: String, folderURL: URL) -> URL { + + let folder = folderURL.appendingPathComponent(name, isDirectory: true) + createFolderIfNecessary(folder) + return folder + } } private extension AppConfig { - static func subfolder(_ name: String, in folderURL: URL) -> URL { - - let folder = folderURL.appendingPathComponent(name, isDirectory: true) - try! FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil) - return folder + static func createFolderIfNecessary(_ folderURL: URL) { + try! FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) } } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 0f4ddd703..b0e3521c1 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -52,17 +52,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var isSyncArticleStatusRunning = false var isWaitingForSyncTasks = false - - let accountManager: AccountManager override init() { - let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString - let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) - self.accountManager = AccountManager(accountsFolder: documentAccountsFolderPath) - AccountManager.shared = accountManager - super.init() appDelegate = self @@ -79,8 +71,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD os_log("Is first run.", log: log, type: .info) } - if isFirstRun && !accountManager.anyAccountHasAtLeastOneFeed() { - let localAccount = accountManager.defaultAccount + if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { + let localAccount = AccountManager.shared.defaultAccount DefaultFeedsImporter.importDefaultFeeds(account: localAccount) } @@ -92,7 +84,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD initializeHomeScreenQuickActions() Task { @MainActor in - self.unreadCount = accountManager.unreadCount + self.unreadCount = AccountManager.shared.unreadCount } UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in @@ -121,7 +113,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { resumeDatabaseProcessingIfNecessary() - await accountManager.receiveRemoteNotification(userInfo: userInfo) + await AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) suspendApplication() return .newData } @@ -136,7 +128,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD MultilineUILabelSizer.emptyCache() SingleLineUILabelSizer.emptyCache() IconImageCache.shared.emptyCache() - accountManager.emptyCaches() + AccountManager.shared.emptyCaches() Task.detached { await DownloadWithCacheManager.shared.cleanupCache() @@ -147,7 +139,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { - unreadCount = accountManager.unreadCount + unreadCount = AccountManager.shared.unreadCount } } @@ -165,13 +157,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } Task { @MainActor in - await self.accountManager.refreshAll(errorHandler: errorHandler) + await AccountManager.shared.refreshAll(errorHandler: errorHandler) } } func resumeDatabaseProcessingIfNecessary() { - if accountManager.isSuspended { - accountManager.resumeAll() + if AccountManager.shared.isSuspended { + AccountManager.shared.resumeAll() os_log("Application processing resumed.", log: self.log, type: .info) } } @@ -191,12 +183,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Task { @MainActor in if let lastRefresh = AppDefaults.shared.lastRefresh { if Date() > lastRefresh.addingTimeInterval(15 * 60) { - await accountManager.refreshAll(errorHandler: ErrorHandler.log) + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } else { - await accountManager.syncArticleStatusAll() + await AccountManager.shared.syncArticleStatusAll() } } else { - await accountManager.refreshAll(errorHandler: ErrorHandler.log) + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } } } @@ -281,7 +273,7 @@ private extension AppDelegate { return } - if accountManager.refreshInProgress || isSyncArticleStatusRunning { + if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning { os_log("Waiting for sync to finish...", log: self.log, type: .info) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.waitToComplete(completion: completion) @@ -318,7 +310,7 @@ private extension AppDelegate { } Task { @MainActor in - await self.accountManager.syncArticleStatusAll() + await AccountManager.shared.syncArticleStatusAll() completeProcessing() } } @@ -326,8 +318,8 @@ private extension AppDelegate { func suspendApplication() { guard UIApplication.shared.applicationState == .background else { return } - accountManager.suspendNetworkAll() - accountManager.suspendDatabaseAll() + AccountManager.shared.suspendNetworkAll() + AccountManager.shared.suspendDatabaseAll() ArticleThemeDownloader.cleanUp() CoalescingQueue.standard.performCallsImmediately() @@ -382,11 +374,11 @@ private extension AppDelegate { os_log("Woken to perform account refresh.", log: self.log, type: .info) Task { @MainActor in - if self.accountManager.isSuspended { - self.accountManager.resumeAll() + if AccountManager.shared.isSuspended { + AccountManager.shared.resumeAll() } - await self.accountManager.refreshAll(errorHandler: ErrorHandler.log) - if !self.accountManager.isSuspended { + await AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) + if !AccountManager.shared.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() self.suspendApplication() os_log("Account refresh operation completed.", log: self.log, type: .info) @@ -418,7 +410,7 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() - guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { + guard let accountID = articlePathInfo.accountID, let account = AccountManager.shared.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } @@ -438,7 +430,7 @@ private extension AppDelegate { self.prepareAccountsForBackground() try? await account.syncArticleStatus() - if !self.accountManager.isSuspended { + if !AccountManager.shared.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() self.prepareAccountsForBackground() self.suspendApplication() @@ -454,7 +446,7 @@ private extension AppDelegate { resumeDatabaseProcessingIfNecessary() - guard let accountID = articlePathInfo.accountID, let account = accountManager.existingAccount(with: accountID) else { + guard let accountID = articlePathInfo.accountID, let account = AccountManager.shared.existingAccount(with: accountID) else { os_log(.debug, "No account found from notification.") return } @@ -473,7 +465,7 @@ private extension AppDelegate { try? await account.markArticles(articles, statusKey: .starred, flag: true) try? await account.syncArticleStatus() - if !self.accountManager.isSuspended { + if !AccountManager.shared.isSuspended { try? WidgetDataEncoder.shared.encodeWidgetData() self.prepareAccountsForBackground() self.suspendApplication()