mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			179 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			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
 | 
						|
            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<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()
 | 
						|
    }
 | 
						|
}
 |