mirror of https://github.com/oxen-io/session-ios
Merge branch 'mkirk/push-notification-fixups'
commit
97d99e5c2d
@ -0,0 +1,287 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import PromiseKit
|
||||||
|
import PushKit
|
||||||
|
|
||||||
|
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 class PushRegistrationManager: NSObject, PKPushRegistryDelegate {
|
||||||
|
|
||||||
|
let TAG = "[PushRegistrationManager]"
|
||||||
|
|
||||||
|
// MARK - Dependencies
|
||||||
|
private var pushManager: PushManager {
|
||||||
|
return PushManager.shared()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK - Singleton class
|
||||||
|
|
||||||
|
@objc(sharedManager)
|
||||||
|
static let shared = PushRegistrationManager()
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var userNotificationSettingsPromise: Promise<Void>?
|
||||||
|
private var fulfillUserNotificationSettingsPromise: (() -> Void)?
|
||||||
|
|
||||||
|
private var vanillaTokenPromise: Promise<Data>?
|
||||||
|
private var fulfillVanillaTokenPromise: ((Data) -> Void)?
|
||||||
|
private var rejectVanillaTokenPromise: ((Error) -> Void)?
|
||||||
|
|
||||||
|
private var voipRegistry: PKPushRegistry?
|
||||||
|
private var voipTokenPromise: Promise<Data>?
|
||||||
|
private var fulfillVoipTokenPromise: ((Data) -> Void)?
|
||||||
|
|
||||||
|
// MARK: Public interface
|
||||||
|
|
||||||
|
public func requestPushTokens() -> Promise<(pushToken: String, voipToken: String)> {
|
||||||
|
Logger.info("\(self.TAG) in \(#function)")
|
||||||
|
|
||||||
|
return self.registerUserNotificationSettings().then {
|
||||||
|
guard !Platform.isSimulator else {
|
||||||
|
throw PushRegistrationError.pushNotSupported(description:"Push not supported on simulators")
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.registerForVanillaPushToken().then { vanillaPushToken in
|
||||||
|
self.registerForVoipPushToken().then { voipPushToken in
|
||||||
|
(pushToken: vanillaPushToken, voipToken: voipPushToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification registration is confirmed via AppDelegate
|
||||||
|
// Before this occurs, it is not safe to assume push token requests will be acknowledged.
|
||||||
|
//
|
||||||
|
// e.g. in the case that Background Fetch is disabled, token requests will be ignored until
|
||||||
|
// we register user notification settings.
|
||||||
|
@objc
|
||||||
|
public func didRegisterUserNotificationSettings() {
|
||||||
|
guard let fulfillUserNotificationSettingsPromise = self.fulfillUserNotificationSettingsPromise else {
|
||||||
|
owsFail("\(TAG) promise completion in \(#function) unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfillUserNotificationSettingsPromise()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Vanilla push token
|
||||||
|
|
||||||
|
// Vanilla push token is obtained from the system via AppDelegate
|
||||||
|
@objc
|
||||||
|
public func didReceiveVanillaPushToken(_ tokenData: Data) {
|
||||||
|
guard let fulfillVanillaTokenPromise = self.fulfillVanillaTokenPromise else {
|
||||||
|
owsFail("\(TAG) promise completion in \(#function) unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfillVanillaTokenPromise(tokenData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vanilla push token is obtained from the system via AppDelegate
|
||||||
|
@objc
|
||||||
|
public func didFailToReceiveVanillaPushToken(error: Error) {
|
||||||
|
guard let rejectVanillaTokenPromise = self.rejectVanillaTokenPromise else {
|
||||||
|
owsFail("\(TAG) promise completion in \(#function) unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectVanillaTokenPromise(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: PKPushRegistryDelegate - voIP Push Token
|
||||||
|
|
||||||
|
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) {
|
||||||
|
Logger.info("\(self.TAG) in \(#function)")
|
||||||
|
assert(type == .voIP)
|
||||||
|
self.pushManager.application(UIApplication.shared, didReceiveRemoteNotification: payload.dictionaryPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType) {
|
||||||
|
Logger.info("\(self.TAG) in \(#function)")
|
||||||
|
assert(type == .voIP)
|
||||||
|
assert(credentials.type == .voIP)
|
||||||
|
guard let fulfillVoipTokenPromise = self.fulfillVoipTokenPromise else {
|
||||||
|
owsFail("\(TAG) fulfillVoipTokenPromise was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfillVoipTokenPromise(credentials.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenForType type: PKPushType) {
|
||||||
|
// It's not clear when this would happen. We've never previously handled it, but we should at
|
||||||
|
// least start learning if it happens.
|
||||||
|
owsFail("\(TAG) in \(#function)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: helpers
|
||||||
|
|
||||||
|
// User notification settings must be registered *before* AppDelegate will
|
||||||
|
// return any requested push tokens. We don't consider the notifications settings registration
|
||||||
|
// *complete* until AppDelegate#didRegisterUserNotificationSettings is called.
|
||||||
|
private func registerUserNotificationSettings() -> Promise<Void> {
|
||||||
|
AssertIsOnMainThread()
|
||||||
|
|
||||||
|
guard self.userNotificationSettingsPromise == nil else {
|
||||||
|
let promise = self.userNotificationSettingsPromise!
|
||||||
|
Logger.info("\(TAG) already registered user notification settings")
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
let (promise, fulfill, _) = Promise<Void>.pending()
|
||||||
|
self.userNotificationSettingsPromise = promise
|
||||||
|
self.fulfillUserNotificationSettingsPromise = fulfill
|
||||||
|
|
||||||
|
Logger.info("\(TAG) registering user notification settings")
|
||||||
|
|
||||||
|
UIApplication.shared.registerUserNotificationSettings(self.pushManager.userNotificationSettings)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* work around for iOS11 bug, wherein for users who have disabled notifications
|
||||||
|
* and background fetch, the AppDelegate will neither succeed nor fail at registering
|
||||||
|
* for a vanilla push token.
|
||||||
|
*/
|
||||||
|
private var isSusceptibleToFailedPushRegistration: Bool {
|
||||||
|
// Only affects iOS11 users
|
||||||
|
guard #available(iOS 11.0, *) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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> {
|
||||||
|
Logger.info("\(self.TAG) in \(#function)")
|
||||||
|
AssertIsOnMainThread()
|
||||||
|
|
||||||
|
guard self.vanillaTokenPromise == nil else {
|
||||||
|
let promise = vanillaTokenPromise!
|
||||||
|
assert(promise.isPending)
|
||||||
|
Logger.info("\(TAG) alreay pending promise for vanilla push token")
|
||||||
|
return promise.then { $0.hexEncodedString }
|
||||||
|
}
|
||||||
|
|
||||||
|
// No pending vanilla token yet. Create a new promise
|
||||||
|
let (promise, fulfill, reject) = Promise<Data>.pending()
|
||||||
|
self.vanillaTokenPromise = promise
|
||||||
|
self.fulfillVanillaTokenPromise = fulfill
|
||||||
|
self.rejectVanillaTokenPromise = reject
|
||||||
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
|
||||||
|
let kTimeout: TimeInterval = 10
|
||||||
|
let timeout: Promise<Data> = after(seconds: kTimeout).then { 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
|
||||||
|
}
|
||||||
|
}.then { (pushTokenData: Data) -> String in
|
||||||
|
if self.isSusceptibleToFailedPushRegistration {
|
||||||
|
// Sentinal in case this bug is fixed.
|
||||||
|
owsFail("Device was unexpectedly able to complete push registration even though it was susceptible to failure.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info("\(self.TAG) successfully registered for vanilla push notifications")
|
||||||
|
return pushTokenData.hexEncodedString
|
||||||
|
}.always {
|
||||||
|
self.vanillaTokenPromise = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerForVoipPushToken() -> Promise<String> {
|
||||||
|
AssertIsOnMainThread()
|
||||||
|
Logger.info("\(self.TAG) in \(#function)")
|
||||||
|
|
||||||
|
guard self.voipTokenPromise == nil else {
|
||||||
|
let promise = self.voipTokenPromise!
|
||||||
|
assert(promise.isPending)
|
||||||
|
return promise.then { $0.hexEncodedString }
|
||||||
|
}
|
||||||
|
|
||||||
|
// No pending voip token yet. Create a new promise
|
||||||
|
let (promise, fulfill, reject) = Promise<Data>.pending()
|
||||||
|
self.voipTokenPromise = promise
|
||||||
|
self.fulfillVoipTokenPromise = fulfill
|
||||||
|
|
||||||
|
if self.voipRegistry == nil {
|
||||||
|
// We don't create the voip registry in init, because it immediately requests the voip token,
|
||||||
|
// potentially before we're ready to handle it.
|
||||||
|
let voipRegistry = PKPushRegistry(queue: nil)
|
||||||
|
self.voipRegistry = voipRegistry
|
||||||
|
voipRegistry.desiredPushTypes = [.voIP]
|
||||||
|
voipRegistry.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let voipRegistry = self.voipRegistry else {
|
||||||
|
owsFail("\(TAG) failed to initialize voipRegistry in \(#function)")
|
||||||
|
reject(PushRegistrationError.assertionError(description: "\(TAG) failed to initialize voipRegistry in \(#function)"))
|
||||||
|
return promise.then { _ in
|
||||||
|
// coerce expected type of returned promise - we don't really care about the value,
|
||||||
|
// since this promise has been rejected. In practice this shouldn't happen
|
||||||
|
String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've already completed registering for a voip token, resolve it immediately,
|
||||||
|
// rather than waiting for the delegate method to be called.
|
||||||
|
if let voipTokenData = voipRegistry.pushToken(forType: .voIP) {
|
||||||
|
Logger.info("\(self.TAG) using pre-registered voIP token")
|
||||||
|
fulfill(voipTokenData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then { (voipTokenData: Data) -> String in
|
||||||
|
Logger.info("\(self.TAG) successfully registered for voip push notifications")
|
||||||
|
return voipTokenData.hexEncodedString
|
||||||
|
}.always {
|
||||||
|
self.voipTokenPromise = 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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
@interface NSData (ows_StripToken)
|
|
||||||
|
|
||||||
- (NSString *)ows_tripToken;
|
|
||||||
|
|
||||||
@end
|
|
@ -1,20 +0,0 @@
|
|||||||
//
|
|
||||||
// NSData+ows_StripToken.m
|
|
||||||
// Signal
|
|
||||||
//
|
|
||||||
// Created by Frederic Jacobs on 14/04/15.
|
|
||||||
// Copyright (c) 2015 Open Whisper Systems. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "NSData+ows_StripToken.h"
|
|
||||||
|
|
||||||
@implementation NSData (ows_StripToken)
|
|
||||||
|
|
||||||
- (NSString *)ows_tripToken {
|
|
||||||
return [[[NSString stringWithFormat:@"%@", self]
|
|
||||||
stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]
|
|
||||||
stringByReplacingOccurrencesOfString:@" "
|
|
||||||
withString:@""];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
@ -1,15 +1,11 @@
|
|||||||
//
|
//
|
||||||
// TSUpdateAttributesRequest.h
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||||
// Signal
|
|
||||||
//
|
|
||||||
// Created by Frederic Jacobs on 22/08/15.
|
|
||||||
// Copyright (c) 2015 Open Whisper Systems. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "TSRequest.h"
|
#import "TSRequest.h"
|
||||||
|
|
||||||
@interface TSUpdateAttributesRequest : TSRequest
|
@interface TSUpdateAttributesRequest : TSRequest
|
||||||
|
|
||||||
- (instancetype)initWithUpdatedAttributesWithVoice;
|
- (instancetype)initWithManualMessageFetching:(BOOL)isEnabled;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
//
|
//
|
||||||
// TSUpdateAttributesRequest.m
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||||
// Signal
|
|
||||||
//
|
|
||||||
// Created by Frederic Jacobs on 22/08/15.
|
|
||||||
// Copyright (c) 2015 Open Whisper Systems. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "TSAttributes.h"
|
#import "TSAttributes.h"
|
||||||
#import "TSConstants.h"
|
#import "TSConstants.h"
|
||||||
#import "TSUpdateAttributesRequest.h"
|
#import "TSUpdateAttributesRequest.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@implementation TSUpdateAttributesRequest
|
@implementation TSUpdateAttributesRequest
|
||||||
|
|
||||||
- (instancetype)initWithUpdatedAttributesWithVoice {
|
- (instancetype)initWithManualMessageFetching:(BOOL)enableManualMessageFetching
|
||||||
|
{
|
||||||
NSString *endPoint = [textSecureAccountsAPI stringByAppendingString:textSecureAttributesAPI];
|
NSString *endPoint = [textSecureAccountsAPI stringByAppendingString:textSecureAttributesAPI];
|
||||||
self = [super initWithURL:[NSURL URLWithString:endPoint]];
|
self = [super initWithURL:[NSURL URLWithString:endPoint]];
|
||||||
|
|
||||||
if (self) {
|
if (self) {
|
||||||
[self setHTTPMethod:@"PUT"];
|
[self setHTTPMethod:@"PUT"];
|
||||||
[self.parameters addEntriesFromDictionary:[TSAttributes attributesFromStorageWithVoiceSupport]];
|
[self.parameters addEntriesFromDictionary:[TSAttributes attributesFromStorageWithManualMessageFetching:enableManualMessageFetching]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
Loading…
Reference in New Issue