diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index a5834223c..0fc726544 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -377,8 +377,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, grantingType.requestOAuthAccessToken(with: response, transport: transport, completion: completion) } + public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + delegate.receiveRemoteNotification(for: self, userInfo: userInfo, completion: completion) + } + public func refreshAll(completion: @escaping (Result) -> Void) { - self.delegate.refreshAll(for: self, completion: completion) + delegate.refreshAll(for: self, completion: completion) } public func syncArticleStatus(completion: ((Result) -> Void)? = nil) { diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index edef2fb1c..0f0025287 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -22,6 +22,8 @@ protocol AccountDelegate { var refreshProgress: DownloadProgress { get } + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) diff --git a/Frameworks/Account/AccountManager.swift b/Frameworks/Account/AccountManager.swift index bcaace17f..c2b1a5da0 100644 --- a/Frameworks/Account/AccountManager.swift +++ b/Frameworks/Account/AccountManager.swift @@ -184,7 +184,22 @@ public final class AccountManager: UnreadCountProvider { accounts.forEach { $0.resume() } } - public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() ->Void)? = nil) { + public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: (() -> Void)? = nil) { + let group = DispatchGroup() + + activeAccounts.forEach { account in + group.enter() + account.receiveRemoteNotification(userInfo: userInfo) { + group.leave() + } + } + + group.notify(queue: DispatchQueue.main) { + completion?() + } + } + + public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() -> Void)? = nil) { let group = DispatchGroup() activeAccounts.forEach { account in @@ -203,7 +218,6 @@ public final class AccountManager: UnreadCountProvider { group.notify(queue: DispatchQueue.main) { completion?() } - } public func syncArticleStatusAll(completion: (() -> Void)? = nil) { diff --git a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift index 45b49173b..950d4cd40 100644 --- a/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Frameworks/Account/CloudKit/CloudKitAccountDelegate.swift @@ -30,6 +30,7 @@ final class CloudKitAccountDelegate: AccountDelegate { return CKContainer(identifier: "iCloud.\(orgID).NetNewsWire") }() + private lazy var zones = [accountZone] private let accountZone: CloudKitAccountZone private let refresher = LocalAccountRefresher() @@ -52,6 +53,21 @@ final class CloudKitAccountDelegate: AccountDelegate { accountZone.refreshProgress = refreshProgress } + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + let group = DispatchGroup() + + zones.forEach { zone in + group.enter() + zone.receiveRemoteNotification(userInfo: userInfo) { + group.leave() + } + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + } + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { accountZone.fetchChangesInZone() { result in switch result { @@ -247,11 +263,16 @@ final class CloudKitAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { accountZone.delegate = CloudKitAcountZoneDelegate(account: account, refreshProgress: refreshProgress) - accountZone.resumeLongLivedOperationIfPossible() + zones.forEach { zone in + zone.resumeLongLivedOperationIfPossible() + zone.subscribe() + } } func accountWillBeDeleted(_ account: Account) { - accountZone.resetChangeToken() + zones.forEach { zone in + zone.resetChangeToken() + } } static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result) -> Void) { diff --git a/Frameworks/Account/CloudKit/CloudKitZone.swift b/Frameworks/Account/CloudKit/CloudKitZone.swift index 83417d527..062f24607 100644 --- a/Frameworks/Account/CloudKit/CloudKitZone.swift +++ b/Frameworks/Account/CloudKit/CloudKitZone.swift @@ -58,14 +58,43 @@ extension CloudKitZone { } } - // func startObservingRemoteChanges() { - // NotificationCenter.default.addObserver(forName: Notifications.cloudKitDataDidChangeRemotely.name, object: nil, queue: nil, using: { [weak self](_) in - // guard let self = self else { return } - // DispatchQueue.global(qos: .utility).async { - // self.fetchChangesInDatabase(nil) - // } - // }) - // } + func subscribe() { + + let subscription = CKRecordZoneSubscription(zoneID: Self.zoneID) + + let info = CKSubscription.NotificationInfo() + info.shouldSendContentAvailable = true + subscription.notificationInfo = info + + database?.save(subscription) { _, error in + switch CloudKitZoneResult.resolve(error) { + case .success: + break + case .retry(let timeToWait): + self.retryOperationIfPossible(retryAfter: timeToWait) { + self.subscribe() + } + default: + os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", Self.zoneID.zoneName, error?.localizedDescription ?? "Unknown") + } + } + + } + + func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + let note = CKRecordZoneNotification(fromRemoteNotificationDictionary: userInfo) + guard note?.recordZoneID?.zoneName == Self.zoneID.zoneName else { + completion() + return + } + + fetchChangesInZone() { result in + if case .failure(let error) = result { + os_log(.error, log: self.log, "%@ zone remote notification fetch error: %@.", Self.zoneID.zoneName, error.localizedDescription) + } + completion() + } + } func save(record: CKRecord, completion: @escaping (Result) -> Void) { modify(recordsToSave: [record], recordIDsToDelete: [], completion: completion) @@ -142,7 +171,7 @@ extension CloudKitZone { let op = CKFetchRecordZoneChangesOperation(recordZoneIDs: [Self.zoneID], configurationsByRecordZoneID: [Self.zoneID: zoneConfig]) op.fetchAllChanges = true - op.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneId, token, _ in + op.recordZoneChangeTokensUpdatedBlock = { [weak self] zoneID, token, _ in guard let self = self else { return } DispatchQueue.main.async { self.changeToken = token @@ -156,14 +185,14 @@ extension CloudKitZone { } } - op.recordWithIDWasDeletedBlock = { [weak self] recordId, recordType in + op.recordWithIDWasDeletedBlock = { [weak self] recordID, recordType in guard let self = self else { return } DispatchQueue.main.async { - self.delegate?.cloudKitDidDelete(recordType: recordType, recordID: recordId) + self.delegate?.cloudKitDidDelete(recordType: recordType, recordID: recordID) } } - op.recordZoneFetchCompletionBlock = { [weak self] zoneId ,token, _, _, error in + op.recordZoneFetchCompletionBlock = { [weak self] zoneID ,token, _, _, error in guard let self = self else { return } switch CloudKitZoneResult.resolve(error) { @@ -176,7 +205,7 @@ extension CloudKitZone { self.fetchChangesInZone(completion: completion) } default: - os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", zoneId.zoneName, error?.localizedDescription ?? "Unknown") + os_log(.error, log: self.log, "%@ zone fetch changes error: %@.", zoneID.zoneName, error?.localizedDescription ?? "Unknown") } } diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift index 9c760fd88..23fe144d7 100644 --- a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift +++ b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift @@ -60,6 +60,10 @@ final class FeedWranglerAccountDelegate: AccountDelegate { caller.logout() { _ in } } + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { refreshProgress.addToNumberOfTasksAndRemaining(6) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index af2cda43a..512fd3d1e 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -41,6 +41,8 @@ final class FeedbinAccountDelegate: AccountDelegate { caller.accountMetadata = accountMetadata } } + + var refreshProgress = DownloadProgress(numberOfTasks: 0) init(dataFolder: String, transport: Transport?) { @@ -71,9 +73,11 @@ final class FeedbinAccountDelegate: AccountDelegate { } } - - var refreshProgress = DownloadProgress(numberOfTasks: 0) - + + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { refreshProgress.addToNumberOfTasksAndRemaining(5) diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 369e237f8..09438c871 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -95,6 +95,10 @@ final class FeedlyAccountDelegate: AccountDelegate { // MARK: Account API + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { assert(Thread.isMainThread) diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 4fcec7b45..a92ec95ae 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -31,6 +31,10 @@ final class LocalAccountDelegate: AccountDelegate { return refresher.progress } + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { refresher.refreshFeeds(account.flattenedWebFeeds()) { account.metadata.lastArticleFetchEndTime = Date() diff --git a/Frameworks/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Frameworks/Account/NewsBlur/NewsBlurAccountDelegate.swift index 716ee1870..6e6cc5c0d 100644 --- a/Frameworks/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Frameworks/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -57,6 +57,10 @@ final class NewsBlurAccountDelegate: AccountDelegate { database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3")) } + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } + func refreshAll(for account: Account, completion: @escaping (Result) -> ()) { self.refreshProgress.addToNumberOfTasksAndRemaining(5) diff --git a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index ab36e98d6..dcc29b442 100644 --- a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -47,6 +47,8 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } } + var refreshProgress = DownloadProgress(numberOfTasks: 0) + init(dataFolder: String, transport: Transport?) { let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") @@ -77,7 +79,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } - var refreshProgress = DownloadProgress(numberOfTasks: 0) + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + completion() + } func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index b771a5530..3edb0d1a6 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -267,6 +267,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, CrashReporter.check(appName: "NetNewsWire") } #endif + + NSApplication.shared.registerForRemoteNotifications() } func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { @@ -302,6 +304,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, saveState() } + func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { + AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) + } + func applicationWillTerminate(_ notification: Notification) { shuttingDown = true saveState() diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index e6f25a9ce..965e34917 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -91,15 +91,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.unreadCount = AccountManager.shared.unreadCount } - UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - UNUserNotificationCenter.current().delegate = self + UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { _, _ in } + UIApplication.shared.registerForRemoteNotifications() + userNotificationManager = UserNotificationManager() extensionContainersFile = ExtensionContainersFile() @@ -115,6 +110,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + DispatchQueue.main.async { + AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { + completionHandler(.newData) + } + } + } + func applicationWillTerminate(_ application: UIApplication) { shuttingDown = true } diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index 9251a4909..2baef99bc 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -92,6 +92,8 @@ UIBackgroundModes fetch + processing + remote-notification UILaunchStoryboardName LaunchScreenPhone