session-ios/Session/Notifications/PushRegistrationManager.swift
Morgan Pretty 47314bd639 Added a notification to indicate the user has a new message request
Fixed a bug where the notification count could be increased for message requests
Fixed a bug where an approved contact could be 'unapproved' due to an order of execution issue when generating the config sync message
Fixed a check to avoid registering for push notifications when on the simulator (old check didn't cater for M1 Macs)
Moved the 'hasHiddenMessageRequests' into the group user defaults so it can be accessed within the notification extension
Added code to handle an edge case where an old client could incorrectly un-approve a contact via a legacy configuration message
2022-02-21 14:48:53 +11:00

179 lines
6.3 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import PushKit
import SignalUtilitiesKit
import SignalUtilitiesKit
public enum PushRegistrationError: Error {
case assertionError(description: String)
case pushNotSupported(description: String)
case timeout
}
/**
* Singleton used to integrate with push notification services - registration and routing received remote notifications.
*/
@objc public class PushRegistrationManager: NSObject {
// MARK: - Dependencies
private var notificationPresenter: NotificationPresenter {
return AppEnvironment.shared.notificationPresenter
}
// MARK: - Singleton class
@objc
public static var shared: PushRegistrationManager {
get {
return AppEnvironment.shared.pushRegistrationManager
}
}
override init() {
super.init()
SwiftSingletons.register(self)
}
private var vanillaTokenPromise: Promise<Data>?
private var vanillaTokenResolver: Resolver<Data>?
private var voipRegistry: PKPushRegistry?
private var voipTokenPromise: Promise<Data>?
private var voipTokenResolver: Resolver<Data>?
// MARK: Public interface
public func requestPushTokens() -> Promise<(pushToken: String, voipToken: String)> {
return firstly {
self.registerUserNotificationSettings()
}.then { () -> Promise<(pushToken: String, voipToken: String)> in
#if targetEnvironment(simulator)
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
#endif
return self.registerForVanillaPushToken().map { vanillaPushToken -> (pushToken: String, voipToken: String) in
return (pushToken: vanillaPushToken, voipToken: "")
}
}
}
// MARK: Vanilla push token
// Vanilla push token is obtained from the system via AppDelegate
@objc
public func didReceiveVanillaPushToken(_ tokenData: Data) {
guard let vanillaTokenResolver = self.vanillaTokenResolver else {
owsFailDebug("promise completion in \(#function) unexpectedly nil")
return
}
vanillaTokenResolver.fulfill(tokenData)
}
// Vanilla push token is obtained from the system via AppDelegate
@objc
public func didFailToReceiveVanillaPushToken(error: Error) {
guard let vanillaTokenResolver = self.vanillaTokenResolver else {
owsFailDebug("promise completion in \(#function) unexpectedly nil")
return
}
vanillaTokenResolver.reject(error)
}
// MARK: helpers
// User notification settings must be registered *before* AppDelegate will
// return any requested push tokens.
public func registerUserNotificationSettings() -> Promise<Void> {
AssertIsOnMainThread()
return notificationPresenter.registerNotificationSettings()
}
/**
* When users have disabled notifications and background fetch, the system hangs when returning a push token.
* More specifically, after registering for remote notification, the app delegate calls neither
* `didFailToRegisterForRemoteNotificationsWithError` nor `didRegisterForRemoteNotificationsWithDeviceToken`
* This behavior is identical to what you'd see if we hadn't previously registered for user notification settings, though
* in this case we've verified that we *have* properly registered notification settings.
*/
private var isSusceptibleToFailedPushRegistration: Bool {
// Only affects users who have disabled both: background refresh *and* notifications
guard UIApplication.shared.backgroundRefreshStatus == .denied else {
return false
}
guard let notificationSettings = UIApplication.shared.currentUserNotificationSettings else {
return false
}
guard notificationSettings.types == [] else {
return false
}
return true
}
private func registerForVanillaPushToken() -> Promise<String> {
AssertIsOnMainThread()
guard self.vanillaTokenPromise == nil else {
let promise = vanillaTokenPromise!
assert(promise.isPending)
return promise.map { $0.hexEncodedString }
}
// No pending vanilla token yet; create a new promise
let (promise, resolver) = Promise<Data>.pending()
self.vanillaTokenPromise = promise
self.vanillaTokenResolver = resolver
UIApplication.shared.registerForRemoteNotifications()
let kTimeout: TimeInterval = 10
let timeout: Promise<Data> = after(seconds: kTimeout).map { throw PushRegistrationError.timeout }
let promiseWithTimeout: Promise<Data> = race(promise, timeout)
return promiseWithTimeout.recover { error -> Promise<Data> in
switch error {
case PushRegistrationError.timeout:
if self.isSusceptibleToFailedPushRegistration {
// If we've timed out on a device known to be susceptible to failures, quit trying
// so the user doesn't remain indefinitely hung for no good reason.
throw PushRegistrationError.pushNotSupported(description: "Device configuration disallows push notifications")
} else {
// Sometimes registration can just take a while.
// If we're not on a device known to be susceptible to push registration failure,
// just return the original promise.
return promise
}
default:
throw error
}
}.map { (pushTokenData: Data) -> String in
if self.isSusceptibleToFailedPushRegistration {
// Sentinal in case this bug is fixed
OWSLogger.debug("Device was unexpectedly able to complete push registration even though it was susceptible to failure.")
}
return pushTokenData.hexEncodedString
}.ensure {
self.vanillaTokenPromise = nil
}
}
}
// We transmit pushToken data as hex encoded string to the server
fileprivate extension Data {
var hexEncodedString: String {
return map { String(format: "%02hhx", $0) }.joined()
}
}