// // 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? private var vanillaTokenResolver: Resolver? private var voipRegistry: PKPushRegistry? private var voipTokenPromise: Promise? private var voipTokenResolver: Resolver? // MARK: Public interface public func requestPushTokens() -> Promise<(pushToken: String, voipToken: String)> { return firstly { self.registerUserNotificationSettings() }.then { () -> Promise<(pushToken: String, voipToken: String)> in guard !Platform.isSimulator else { throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators") } 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 { 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 { 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.pending() self.vanillaTokenPromise = promise self.vanillaTokenResolver = resolver UIApplication.shared.registerForRemoteNotifications() let kTimeout: TimeInterval = 10 let timeout: Promise = after(seconds: kTimeout).map { throw PushRegistrationError.timeout } let promiseWithTimeout: Promise = race(promise, timeout) return promiseWithTimeout.recover { error -> Promise 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() } }