Added support for CloudKit push notifications (subscriptions).

This commit is contained in:
Maurice Parker
2020-03-30 02:48:25 -05:00
parent e2d8db6f26
commit 187121298e
14 changed files with 135 additions and 30 deletions

View File

@@ -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, Error>) -> Void) {
self.delegate.refreshAll(for: self, completion: completion)
delegate.refreshAll(for: self, completion: completion)
}
public func syncArticleStatus(completion: ((Result<Void, Error>) -> Void)? = nil) {

View File

@@ -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, Error>) -> Void)
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))

View File

@@ -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) {

View File

@@ -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, Error>) -> 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<Credentials?, Error>) -> Void) {

View File

@@ -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, Error>) -> 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")
}
}

View File

@@ -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, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6)

View File

@@ -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, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(5)

View File

@@ -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, Error>) -> Void) {
assert(Thread.isMainThread)

View File

@@ -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, Error>) -> Void) {
refresher.refreshFeeds(account.flattenedWebFeeds()) {
account.metadata.lastArticleFetchEndTime = Date()

View File

@@ -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<Void, Error>) -> ()) {
self.refreshProgress.addToNumberOfTasksAndRemaining(5)

View File

@@ -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, Error>) -> Void) {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -92,6 +92,8 @@
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreenPhone</string>