2018-02-05 13:29:46 -08:00
|
|
|
|
//
|
|
|
|
|
// AppDelegate.swift
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// NetNewsWire
|
2018-02-05 13:29:46 -08:00
|
|
|
|
//
|
2019-04-15 15:03:05 -05:00
|
|
|
|
// Created by Maurice Parker on 4/8/19.
|
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
2018-02-05 13:29:46 -08:00
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import UIKit
|
2019-06-19 23:26:03 +08:00
|
|
|
|
import BackgroundTasks
|
2025-01-03 11:48:19 -08:00
|
|
|
|
import os
|
2025-02-01 19:43:44 -08:00
|
|
|
|
import RSCore
|
|
|
|
|
import Account
|
2019-04-15 15:03:05 -05:00
|
|
|
|
|
2018-02-05 13:29:46 -08:00
|
|
|
|
@UIApplicationMain
|
2025-02-01 20:04:30 -08:00
|
|
|
|
final class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 16:25:54 -08:00
|
|
|
|
var window: UIWindow?
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 20:28:37 -08:00
|
|
|
|
private var backgroundTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler")
|
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
2025-02-01 20:28:37 -08:00
|
|
|
|
private var isWaitingForSyncTasks = false
|
|
|
|
|
|
2019-06-20 07:09:42 +08:00
|
|
|
|
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
2025-02-01 20:28:37 -08:00
|
|
|
|
private var isSyncArticleStatusRunning = false
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 19:55:10 -08:00
|
|
|
|
private var coordinator: SceneCoordinator?
|
2025-02-01 17:34:45 -08:00
|
|
|
|
|
2025-01-03 11:48:19 -08:00
|
|
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
|
|
|
|
|
2025-02-01 20:04:30 -08:00
|
|
|
|
private var unreadCount = 0 {
|
2019-04-15 15:03:05 -05:00
|
|
|
|
didSet {
|
|
|
|
|
if unreadCount != oldValue {
|
2024-11-03 22:40:38 -08:00
|
|
|
|
UNUserNotificationCenter.current().setBadgeCount(unreadCount)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
override init() {
|
|
|
|
|
super.init()
|
2019-09-12 10:59:26 -05:00
|
|
|
|
|
2025-02-01 19:37:59 -08:00
|
|
|
|
_ = AccountManager.shared
|
|
|
|
|
_ = ArticleThemesManager.shared
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 20:28:37 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: AccountManager.shared)
|
2019-04-26 15:24:39 -05:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
2025-02-01 13:28:49 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDidTriggerManualRefresh(_:)), name: .userDidTriggerManualRefresh, object: nil)
|
2025-02-01 17:34:45 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
|
|
2019-04-15 13:30:10 -05:00
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
2025-02-01 17:34:45 -08:00
|
|
|
|
|
2019-04-15 15:03:05 -05:00
|
|
|
|
AppDefaults.registerDefaults()
|
2019-11-06 16:53:13 -06:00
|
|
|
|
|
2025-01-26 21:06:22 -08:00
|
|
|
|
let isFirstRun = AppDefaults.isFirstRun
|
2019-04-15 15:03:05 -05:00
|
|
|
|
if isFirstRun {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Is first run.")
|
2019-04-15 15:03:05 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-18 10:24:32 -05:00
|
|
|
|
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
|
|
|
|
let localAccount = AccountManager.shared.defaultAccount
|
|
|
|
|
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-11-06 16:53:13 -06:00
|
|
|
|
registerBackgroundTasks()
|
2019-11-08 12:20:21 -06:00
|
|
|
|
CacheCleaner.purgeIfNecessary()
|
2019-09-01 16:54:07 -05:00
|
|
|
|
initializeHomeScreenQuickActions()
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 20:36:18 -08:00
|
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { granted, _ in
|
|
|
|
|
guard granted else { return }
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
UIApplication.shared.registerForRemoteNotifications()
|
2019-04-23 07:48:22 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-30 02:48:25 -05:00
|
|
|
|
|
2019-10-02 19:42:16 -05:00
|
|
|
|
UNUserNotificationCenter.current().delegate = self
|
2025-02-01 20:16:33 -08:00
|
|
|
|
|
2025-02-01 20:06:58 -08:00
|
|
|
|
_ = UserNotificationManager.shared
|
2025-02-01 20:09:34 -08:00
|
|
|
|
_ = ExtensionContainersFile.shared
|
2025-02-01 20:16:33 -08:00
|
|
|
|
_ = ExtensionFeedAddRequestFile.shared
|
|
|
|
|
_ = WidgetDataEncoder.shared
|
2025-02-01 20:03:01 -08:00
|
|
|
|
_ = ArticleStatusSyncTimer.shared
|
2025-02-01 20:28:37 -08:00
|
|
|
|
_ = FaviconDownloader.shared
|
|
|
|
|
_ = FeedIconDownloader.shared
|
|
|
|
|
|
2019-05-20 13:51:08 -05:00
|
|
|
|
#if DEBUG
|
2025-02-01 20:03:01 -08:00
|
|
|
|
ArticleStatusSyncTimer.shared.update()
|
2019-05-20 13:51:08 -05:00
|
|
|
|
#endif
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
// Create window.
|
2025-02-01 16:25:54 -08:00
|
|
|
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
|
|
|
self.window = window
|
2025-02-01 17:34:45 -08:00
|
|
|
|
|
|
|
|
|
// Create UI and add it to window.
|
2025-02-01 16:25:54 -08:00
|
|
|
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
|
|
|
let rootSplitViewController = storyboard.instantiateInitialViewController() as! RootSplitViewController
|
2025-02-01 17:34:45 -08:00
|
|
|
|
rootSplitViewController.presentsWithGesture = true
|
|
|
|
|
rootSplitViewController.showsSecondaryOnlyButton = true
|
|
|
|
|
rootSplitViewController.preferredDisplayMode = .oneBesideSecondary
|
|
|
|
|
|
2025-02-01 19:55:10 -08:00
|
|
|
|
coordinator = SceneCoordinator(rootSplitViewController: rootSplitViewController)
|
|
|
|
|
rootSplitViewController.coordinator = coordinator
|
|
|
|
|
rootSplitViewController.delegate = coordinator
|
2025-02-01 16:25:54 -08:00
|
|
|
|
|
|
|
|
|
window.rootViewController = rootSplitViewController
|
2025-02-01 17:34:45 -08:00
|
|
|
|
|
|
|
|
|
window.tintColor = AppColor.accent
|
|
|
|
|
updateUserInterfaceStyle()
|
|
|
|
|
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
|
|
|
|
|
|
2025-02-01 16:25:54 -08:00
|
|
|
|
window.makeKeyAndVisible()
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
Task { @MainActor in
|
|
|
|
|
// Ensure Feeds view shows on first run on iPad — otherwise the UI is empty.
|
|
|
|
|
if UIDevice.current.userInterfaceIdiom == .pad && AppDefaults.isFirstRun {
|
2025-02-01 19:53:02 -08:00
|
|
|
|
rootSplitViewController.show(.primary)
|
2025-02-01 17:34:45 -08:00
|
|
|
|
}
|
2025-02-01 20:36:18 -08:00
|
|
|
|
|
|
|
|
|
// Update unread count.
|
|
|
|
|
self.unreadCount = AccountManager.shared.unreadCount
|
2025-02-01 17:34:45 -08:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 16:25:54 -08:00
|
|
|
|
return true
|
2018-02-05 13:29:46 -08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
|
|
|
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
2020-05-02 10:02:58 -05:00
|
|
|
|
DispatchQueue.main.async {
|
2025-02-01 13:28:49 -08:00
|
|
|
|
AccountManager.shared.resumeAllIfSuspended()
|
2020-05-02 10:02:58 -05:00
|
|
|
|
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
|
|
|
|
self.suspendApplication()
|
|
|
|
|
completionHandler(.newData)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-30 02:48:25 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
2025-02-01 20:03:01 -08:00
|
|
|
|
ArticleStatusSyncTimer.shared.stop()
|
2019-06-28 10:28:02 -05:00
|
|
|
|
}
|
2021-05-08 12:42:44 -07:00
|
|
|
|
|
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
|
|
|
|
IconImageCache.shared.emptyCache()
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
// MARK: - Notifications
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
2025-02-01 20:28:37 -08:00
|
|
|
|
assert(Thread.isMainThread)
|
|
|
|
|
assert(note.object is AccountManager)
|
|
|
|
|
unreadCount = AccountManager.shared.unreadCount
|
2018-02-05 13:29:46 -08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
@objc func accountRefreshDidFinish(_ note: Notification) {
|
2025-01-26 21:06:22 -08:00
|
|
|
|
AppDefaults.lastRefresh = Date()
|
2019-06-28 10:28:02 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 13:28:49 -08:00
|
|
|
|
@objc func userDidTriggerManualRefresh(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
guard let errorHandler = note.userInfo?[UserInfoKey.errorHandler] as? ErrorHandlerBlock else {
|
|
|
|
|
assertionFailure("Expected errorHandler in .userDidTriggerManualRefresh userInfo")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manualRefresh(errorHandler: errorHandler)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
@objc func userDefaultsDidChange(_ note: Notification) {
|
|
|
|
|
updateUserInterfaceStyle()
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
// MARK: - API
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 13:28:49 -08:00
|
|
|
|
func manualRefresh(errorHandler: @escaping ErrorHandlerBlock) {
|
|
|
|
|
assert(Thread.isMainThread)
|
2025-02-01 19:55:10 -08:00
|
|
|
|
coordinator?.cleanUp(conditional: true)
|
2020-03-11 14:47:00 -06:00
|
|
|
|
AccountManager.shared.refreshAll(errorHandler: errorHandler)
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
func prepareAccountsForBackground() {
|
2025-02-01 20:16:33 -08:00
|
|
|
|
ExtensionFeedAddRequestFile.shared.suspend()
|
2025-02-01 20:03:01 -08:00
|
|
|
|
ArticleStatusSyncTimer.shared.invalidate()
|
2019-06-19 23:26:03 +08:00
|
|
|
|
scheduleBackgroundFeedRefresh()
|
2019-10-31 19:20:52 -05:00
|
|
|
|
syncArticleStatus()
|
2025-02-01 20:16:33 -08:00
|
|
|
|
WidgetDataEncoder.shared.encode()
|
2019-12-01 16:51:25 -06:00
|
|
|
|
waitForSyncTasksToFinish()
|
2018-02-05 13:29:46 -08:00
|
|
|
|
}
|
2025-01-03 11:48:19 -08:00
|
|
|
|
|
2019-06-28 10:28:02 -05:00
|
|
|
|
func prepareAccountsForForeground() {
|
2025-02-01 20:16:33 -08:00
|
|
|
|
ExtensionFeedAddRequestFile.shared.resume()
|
2025-02-01 20:03:01 -08:00
|
|
|
|
ArticleStatusSyncTimer.shared.update()
|
2020-04-21 20:23:46 -05:00
|
|
|
|
|
2025-01-26 21:06:22 -08:00
|
|
|
|
if let lastRefresh = AppDefaults.lastRefresh {
|
2019-04-26 15:24:39 -05:00
|
|
|
|
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
2019-06-27 14:21:07 -05:00
|
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
2019-06-28 10:28:02 -05:00
|
|
|
|
} else {
|
|
|
|
|
AccountManager.shared.syncArticleStatusAll()
|
2019-04-26 15:24:39 -05:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-06-27 14:21:07 -05:00
|
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
2019-04-26 15:24:39 -05:00
|
|
|
|
}
|
2018-02-05 13:29:46 -08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-02 19:42:16 -05:00
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
2024-11-03 22:40:38 -08:00
|
|
|
|
completionHandler([.list, .banner, .badge, .sound])
|
2019-10-02 19:42:16 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 17:48:47 -08:00
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
2019-10-03 09:53:21 -05:00
|
|
|
|
defer { completionHandler() }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-12-23 20:15:25 +08:00
|
|
|
|
let userInfo = response.notification.request.content.userInfo
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-12-23 20:15:25 +08:00
|
|
|
|
switch response.actionIdentifier {
|
|
|
|
|
case "MARK_AS_READ":
|
|
|
|
|
handleMarkAsRead(userInfo: userInfo)
|
|
|
|
|
case "MARK_AS_STARRED":
|
|
|
|
|
handleMarkAsStarred(userInfo: userInfo)
|
|
|
|
|
default:
|
2025-02-01 17:48:47 -08:00
|
|
|
|
handle(response)
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
2025-02-01 19:55:10 -08:00
|
|
|
|
self.coordinator?.dismissIfLaunchingFromExternalAction()
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|
2019-10-03 09:53:21 -05:00
|
|
|
|
}
|
2025-02-01 17:48:47 -08:00
|
|
|
|
}
|
2018-02-05 13:29:46 -08:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
// MARK: - App Initialization
|
2019-09-01 16:54:07 -05:00
|
|
|
|
|
|
|
|
|
private extension AppDelegate {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 20:33:50 -08:00
|
|
|
|
enum ShortcutItemType: String {
|
|
|
|
|
case firstUnread = "com.ranchero.NetNewsWire.FirstUnread"
|
|
|
|
|
case showSearch = "com.ranchero.NetNewsWire.ShowSearch"
|
|
|
|
|
case addFeed = "com.ranchero.NetNewsWire.ShowAdd"
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-01 16:54:07 -05:00
|
|
|
|
private func initializeHomeScreenQuickActions() {
|
|
|
|
|
let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
|
2019-09-26 15:51:16 -05:00
|
|
|
|
let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle")
|
2025-02-01 20:33:50 -08:00
|
|
|
|
let unreadItem = UIApplicationShortcutItem(type: ShortcutItemType.firstUnread.rawValue, localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-09-01 16:54:07 -05:00
|
|
|
|
let searchTitle = NSLocalizedString("Search", comment: "Search")
|
|
|
|
|
let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
|
2025-02-01 20:33:50 -08:00
|
|
|
|
let searchItem = UIApplicationShortcutItem(type: ShortcutItemType.showSearch.rawValue, localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil)
|
2019-09-01 16:54:07 -05:00
|
|
|
|
|
2019-09-02 15:45:09 -05:00
|
|
|
|
let addTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
|
2019-09-02 15:14:26 -05:00
|
|
|
|
let addIcon = UIApplicationShortcutIcon(systemImageName: "plus")
|
2025-02-01 20:33:50 -08:00
|
|
|
|
let addItem = UIApplicationShortcutItem(type: ShortcutItemType.addFeed.rawValue, localizedTitle: addTitle, localizedSubtitle: nil, icon: addIcon, userInfo: nil)
|
2019-09-02 15:14:26 -05:00
|
|
|
|
|
2019-09-02 16:05:55 -05:00
|
|
|
|
UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem]
|
2019-09-01 16:54:07 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 17:34:45 -08:00
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
|
|
private extension AppDelegate {
|
|
|
|
|
|
|
|
|
|
func updateUserInterfaceStyle() {
|
|
|
|
|
|
|
|
|
|
assert(Thread.isMainThread)
|
2025-02-01 19:37:59 -08:00
|
|
|
|
guard let window else {
|
2025-02-01 17:55:37 -08:00
|
|
|
|
// Could be nil legitimately — this can get called before window is set up.
|
2025-02-01 17:34:45 -08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let updatedStyle = AppDefaults.userInterfaceColorPalette.uiUserInterfaceStyle
|
|
|
|
|
if window.overrideUserInterfaceStyle != updatedStyle {
|
|
|
|
|
window.overrideUserInterfaceStyle = updatedStyle
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Go To Background
|
2019-12-02 14:14:35 -06:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
private extension AppDelegate {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
func waitForSyncTasksToFinish() {
|
2019-12-06 15:16:20 -07:00
|
|
|
|
guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
isWaitingForSyncTasks = true
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
self.completeProcessing(true)
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Accounts wait for progress terminated for running too long.")
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
2025-01-22 22:18:09 -08:00
|
|
|
|
self?.waitToComplete { [weak self] suspend in
|
2019-12-01 16:51:25 -06:00
|
|
|
|
self?.completeProcessing(suspend)
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
func waitToComplete(completion: @escaping (Bool) -> Void) {
|
2019-12-06 15:16:20 -07:00
|
|
|
|
guard UIApplication.shared.applicationState == .background else {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("App came back to foreground, no longer waiting.")
|
2019-12-01 16:51:25 -06:00
|
|
|
|
completion(false)
|
2019-10-31 19:20:52 -05:00
|
|
|
|
return
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2025-02-01 20:16:33 -08:00
|
|
|
|
if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || WidgetDataEncoder.shared.isRunning {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Waiting for sync to finish…")
|
2019-10-31 19:20:52 -05:00
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
2019-12-01 16:51:25 -06:00
|
|
|
|
self?.waitToComplete(completion: completion)
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Refresh progress complete.")
|
2019-12-01 16:51:25 -06:00
|
|
|
|
completion(true)
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
func completeProcessing(_ suspend: Bool) {
|
|
|
|
|
if suspend {
|
2019-12-02 14:14:35 -06:00
|
|
|
|
suspendApplication()
|
2019-12-01 16:51:25 -06:00
|
|
|
|
}
|
|
|
|
|
UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
|
|
|
|
|
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
|
|
|
|
isWaitingForSyncTasks = false
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
func syncArticleStatus() {
|
2019-12-01 16:51:25 -06:00
|
|
|
|
guard !isSyncArticleStatusRunning else { return }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-01 16:51:25 -06:00
|
|
|
|
isSyncArticleStatusRunning = true
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
let completeProcessing = { [unowned self] in
|
2019-12-01 16:51:25 -06:00
|
|
|
|
self.isSyncArticleStatusRunning = false
|
2019-10-31 19:20:52 -05:00
|
|
|
|
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
|
|
|
|
|
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
|
|
|
|
|
completeProcessing()
|
2025-01-03 11:48:19 -08:00
|
|
|
|
self.logger.info("Accounts sync processing terminated for running too long.")
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-10-31 19:20:52 -05:00
|
|
|
|
DispatchQueue.main.async {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
AccountManager.shared.syncArticleStatusAll {
|
2019-10-31 19:20:52 -05:00
|
|
|
|
completeProcessing()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-02 14:14:35 -06:00
|
|
|
|
func suspendApplication() {
|
2019-12-06 15:16:20 -07:00
|
|
|
|
guard UIApplication.shared.applicationState == .background else { return }
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-12-04 17:27:39 -07:00
|
|
|
|
AccountManager.shared.suspendNetworkAll()
|
2020-02-02 12:11:39 -08:00
|
|
|
|
AccountManager.shared.suspendDatabaseAll()
|
2021-09-21 09:10:56 +08:00
|
|
|
|
ArticleThemeDownloader.shared.cleanUp()
|
2020-02-02 12:11:39 -08:00
|
|
|
|
|
2019-12-02 14:14:35 -06:00
|
|
|
|
CoalescingQueue.standard.performCallsImmediately()
|
2025-02-01 19:55:10 -08:00
|
|
|
|
coordinator?.suspend()
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Application processing suspended.")
|
2019-12-02 14:14:35 -06:00
|
|
|
|
}
|
2019-10-31 19:20:52 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 19:43:44 -08:00
|
|
|
|
// MARK: - Background Tasks
|
2019-06-19 23:26:03 +08:00
|
|
|
|
|
2019-04-27 13:54:52 -05:00
|
|
|
|
private extension AppDelegate {
|
2019-06-28 10:28:02 -05:00
|
|
|
|
|
2025-02-01 20:28:37 -08:00
|
|
|
|
static let refreshTaskIdentifier = "com.ranchero.NetNewsWire.FeedRefresh"
|
|
|
|
|
|
2019-06-19 23:26:03 +08:00
|
|
|
|
/// Register all background tasks.
|
|
|
|
|
func registerBackgroundTasks() {
|
|
|
|
|
// Register background feed refresh.
|
2025-02-01 20:28:37 -08:00
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.refreshTaskIdentifier, using: nil) { (task) in
|
2019-06-19 23:26:03 +08:00
|
|
|
|
self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
|
2019-04-29 09:29:57 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-19 23:26:03 +08:00
|
|
|
|
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
|
|
|
|
|
func scheduleBackgroundFeedRefresh() {
|
2025-02-01 20:28:37 -08:00
|
|
|
|
let request = BGAppRefreshTaskRequest(identifier: Self.refreshTaskIdentifier)
|
2019-11-13 17:13:06 -06:00
|
|
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
2019-11-05 19:14:26 -06:00
|
|
|
|
|
2019-11-13 17:13:06 -06:00
|
|
|
|
// We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the
|
2019-11-05 19:14:26 -06:00
|
|
|
|
// task scheduler can hang indefinitely.
|
2025-02-01 20:28:37 -08:00
|
|
|
|
backgroundTaskDispatchQueue.async {
|
2019-11-05 19:14:26 -06:00
|
|
|
|
do {
|
|
|
|
|
try BGTaskScheduler.shared.submit(request)
|
|
|
|
|
} catch {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
self.logger.error("Could not schedule app refresh: \(error.localizedDescription)")
|
2019-11-05 19:14:26 -06:00
|
|
|
|
}
|
2019-06-19 23:26:03 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2019-06-19 23:26:03 +08:00
|
|
|
|
/// Performs background feed refresh.
|
|
|
|
|
/// - Parameter task: `BGAppRefreshTask`
|
|
|
|
|
/// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work.
|
|
|
|
|
func performBackgroundFeedRefresh(with task: BGAppRefreshTask) {
|
2025-01-03 11:48:19 -08:00
|
|
|
|
|
2019-06-19 23:26:03 +08:00
|
|
|
|
scheduleBackgroundFeedRefresh() // schedule next refresh
|
2025-01-03 11:48:19 -08:00
|
|
|
|
|
|
|
|
|
logger.info("Woken to perform account refresh.")
|
2019-10-02 16:41:32 -05:00
|
|
|
|
|
2019-12-06 15:47:25 -07:00
|
|
|
|
DispatchQueue.main.async {
|
2019-12-01 16:51:25 -06:00
|
|
|
|
if AccountManager.shared.isSuspended {
|
|
|
|
|
AccountManager.shared.resumeAll()
|
|
|
|
|
}
|
2019-12-02 14:14:35 -06:00
|
|
|
|
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in
|
2019-12-05 17:43:38 -07:00
|
|
|
|
if !AccountManager.shared.isSuspended {
|
|
|
|
|
self.suspendApplication()
|
2025-01-03 11:48:19 -08:00
|
|
|
|
logger.info("Account refresh operation completed.")
|
2019-12-06 15:47:25 -07:00
|
|
|
|
task.setTaskCompleted(success: true)
|
2019-12-05 17:43:38 -07:00
|
|
|
|
}
|
2019-06-19 23:26:03 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-03 11:48:19 -08:00
|
|
|
|
|
2019-06-19 23:26:03 +08:00
|
|
|
|
// set expiration handler
|
2019-11-27 18:03:19 -06:00
|
|
|
|
task.expirationHandler = { [weak task] in
|
2025-01-03 11:48:19 -08:00
|
|
|
|
self.logger.info("Accounts refresh processing terminated for running too long.")
|
2022-03-01 14:53:43 -06:00
|
|
|
|
DispatchQueue.main.async {
|
2019-12-04 16:56:09 -07:00
|
|
|
|
self.suspendApplication()
|
2022-03-01 14:43:54 -06:00
|
|
|
|
task?.setTaskCompleted(success: false)
|
2019-12-04 16:56:09 -07:00
|
|
|
|
}
|
2019-06-19 23:26:03 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-23 20:15:25 +08:00
|
|
|
|
|
2025-02-01 19:43:44 -08:00
|
|
|
|
// MARK: - Handle Notification Actions
|
2020-12-23 20:15:25 +08:00
|
|
|
|
|
|
|
|
|
private extension AppDelegate {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-12-23 20:15:25 +08:00
|
|
|
|
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
|
2020-12-23 20:15:25 +08:00
|
|
|
|
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
|
|
|
|
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-02-01 13:28:49 -08:00
|
|
|
|
AccountManager.shared.resumeAllIfSuspended()
|
2020-12-23 20:15:25 +08:00
|
|
|
|
let account = AccountManager.shared.existingAccount(with: accountID)
|
|
|
|
|
guard account != nil else {
|
|
|
|
|
os_log(.debug, "No account found from notification.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
|
|
|
|
guard article != nil else {
|
|
|
|
|
os_log(.debug, "No article found from search using %@", articleID)
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-04-12 19:41:01 -05:00
|
|
|
|
account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
|
2020-12-23 20:15:25 +08:00
|
|
|
|
self.prepareAccountsForBackground()
|
2020-12-23 21:16:32 +08:00
|
|
|
|
account!.syncArticleStatus(completion: { [weak self] _ in
|
|
|
|
|
if !AccountManager.shared.isSuspended {
|
|
|
|
|
self?.prepareAccountsForBackground()
|
|
|
|
|
self?.suspendApplication()
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|
2020-12-23 21:16:32 +08:00
|
|
|
|
})
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|
2025-01-22 22:18:09 -08:00
|
|
|
|
|
2020-12-23 20:15:25 +08:00
|
|
|
|
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
2025-01-22 22:18:09 -08:00
|
|
|
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
|
2020-12-23 20:15:25 +08:00
|
|
|
|
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
|
|
|
|
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-02-01 13:28:49 -08:00
|
|
|
|
AccountManager.shared.resumeAllIfSuspended()
|
2020-12-23 20:15:25 +08:00
|
|
|
|
let account = AccountManager.shared.existingAccount(with: accountID)
|
|
|
|
|
guard account != nil else {
|
|
|
|
|
os_log(.debug, "No account found from notification.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
|
|
|
|
guard article != nil else {
|
|
|
|
|
os_log(.debug, "No article found from search using %@", articleID)
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-04-12 19:41:01 -05:00
|
|
|
|
account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
|
2020-12-23 21:16:32 +08:00
|
|
|
|
account!.syncArticleStatus(completion: { [weak self] _ in
|
|
|
|
|
if !AccountManager.shared.isSuspended {
|
|
|
|
|
self?.prepareAccountsForBackground()
|
|
|
|
|
self?.suspendApplication()
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|
2020-12-23 21:16:32 +08:00
|
|
|
|
})
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|
2025-02-01 17:48:47 -08:00
|
|
|
|
|
|
|
|
|
func handle(_ response: UNNotificationResponse) {
|
|
|
|
|
AccountManager.shared.resumeAllIfSuspended()
|
2025-02-01 19:55:10 -08:00
|
|
|
|
coordinator?.handle(response)
|
2025-02-01 17:48:47 -08:00
|
|
|
|
}
|
2020-12-23 20:15:25 +08:00
|
|
|
|
}
|