diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 3a7edd28d..98ce74c6f 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -525,7 +525,20 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; [[Environment getCurrent].contactsManager fetchSystemContactsIfAlreadyAuthorized]; // This will fetch new messages, if we're using domain fronting. [[PushManager sharedManager] applicationDidBecomeActive]; + + if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { + DDLogInfo( + @"%@ Retrying to register for remote notifications since user hasn't registered yet.", self.tag); + // Push tokens don't normally change while the app is launched, so checking once during launch is + // usually sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications" and disabled + // "Background App Refresh" will not be able to obtain an APN token. Enabling those settings does not + // restart the app, so we check every activation for users who haven't yet registered. + __unused AnyPromise *promise = + [OWSSyncPushTokensJob runWithAccountManager:[Environment getCurrent].accountManager + preferences:[Environment preferences]]; + } }); + } DDLogInfo(@"%@ applicationDidBecomeActive completed.", self.tag); diff --git a/Signal/src/Models/AccountManager.swift b/Signal/src/Models/AccountManager.swift index 485caed7e..9b9ff9e43 100644 --- a/Signal/src/Models/AccountManager.swift +++ b/Signal/src/Models/AccountManager.swift @@ -46,6 +46,17 @@ class AccountManager: NSObject { self.registerForTextSecure(verificationCode: verificationCode) }.then { self.syncPushTokens() + }.recover { (error) -> Promise in + switch error { + case PushRegistrationError.pushNotSupported(let description): + // This can happen with: + // - simulators, none of which support receiving push notifications + // - on iOS11 devices which have disabled "Allow Notifications" and disabled "Enable Background Refresh" in the system settings. + Logger.info("\(self.TAG) Recovered push registration error. Registering for manual message fetcher because push not supported: \(description)") + return self.registerForManualMessageFetching() + default: + throw error + } }.then { self.completeRegistration() } @@ -73,21 +84,9 @@ class AccountManager: NSObject { self.textSecureAccountManager.didRegister() } - // MARK: Push Tokens + // MARK: Message Delivery func updatePushTokens(pushToken: String, voipToken: String) -> Promise { - return firstly { - return self.updateTextSecurePushTokens(pushToken: pushToken, voipToken: voipToken) - }.then { - Logger.info("\(self.TAG) Successfully updated text secure push tokens.") - // TODO code cleanup - convert to `return Promise(value: nil)` and test - return Promise { fulfill, _ in - fulfill() - } - } - } - - private func updateTextSecurePushTokens(pushToken: String, voipToken: String) -> Promise { return Promise { fulfill, reject in self.textSecureAccountManager.registerForPushNotifications(pushToken:pushToken, voipToken:voipToken, @@ -96,6 +95,12 @@ class AccountManager: NSObject { } } + func registerForManualMessageFetching() -> Promise { + return Promise { fulfill, reject in + self.textSecureAccountManager.registerForManualMessageFetching(success:fulfill, failure:reject) + } + } + // MARK: Turn Server func getTurnServerInfo() -> Promise { diff --git a/Signal/src/Models/SyncPushTokensJob.swift b/Signal/src/Models/SyncPushTokensJob.swift index 0405c9385..9eadb7699 100644 --- a/Signal/src/Models/SyncPushTokensJob.swift +++ b/Signal/src/Models/SyncPushTokensJob.swift @@ -65,7 +65,7 @@ class SyncPushTokensJob: NSObject { return self.recordPushTokensLocally(pushToken:pushToken, voipToken:voipToken) } }.then { - Logger.info("\(self.TAG) in \(#function): succeeded") + Logger.info("\(self.TAG) completed successfully.") }.catch { error in Logger.error("\(self.TAG) in \(#function): Failed with error: \(error).") } diff --git a/Signal/src/environment/Migrations/OWS103EnableVideoCalling.m b/Signal/src/environment/Migrations/OWS103EnableVideoCalling.m index d0f5911f0..7411c19fd 100644 --- a/Signal/src/environment/Migrations/OWS103EnableVideoCalling.m +++ b/Signal/src/environment/Migrations/OWS103EnableVideoCalling.m @@ -23,7 +23,7 @@ static NSString *const OWS103EnableVideoCallingMigrationId = @"103"; DDLogWarn(@"%@ running migration...", self.tag); if ([TSAccountManager isRegistered]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - TSUpdateAttributesRequest *request = [[TSUpdateAttributesRequest alloc] initWithUpdatedAttributesWithVoice]; + TSUpdateAttributesRequest *request = [[TSUpdateAttributesRequest alloc] initWithManualMessageFetching:NO]; [[TSNetworkManager sharedManager] makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { DDLogInfo(@"%@ successfully ran", self.tag); diff --git a/Signal/src/environment/PushRegistrationManager.swift b/Signal/src/environment/PushRegistrationManager.swift index a276138c9..99542b84c 100644 --- a/Signal/src/environment/PushRegistrationManager.swift +++ b/Signal/src/environment/PushRegistrationManager.swift @@ -6,6 +6,12 @@ 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. */ @@ -26,15 +32,16 @@ import PushKit super.init() } - private enum PushRegistrationManagerError: Error { - case assertionError(description: String) - } + private var userNotificationSettingsPromise: Promise? + private var fulfillUserNotificationSettingsPromise: (() -> Void)? - private var voipRegistry: PKPushRegistry? + private var vanillaTokenPromise: Promise? private var fulfillVanillaTokenPromise: ((Data) -> Void)? private var rejectVanillaTokenPromise: ((Error) -> Void)? + + private var voipRegistry: PKPushRegistry? + private var voipTokenPromise: Promise? private var fulfillVoipTokenPromise: ((Data) -> Void)? - private var fulfillRegisterUserNotificationSettingsPromise: (() -> Void)? // MARK: Public interface @@ -43,8 +50,7 @@ import PushKit return self.registerUserNotificationSettings().then { guard !Platform.isSimulator else { - Logger.warn("\(self.TAG) Using fake push tokens for simulator") - return Promise(value: (pushToken: "fakePushToken", voipToken: "fakeVoipToken")) + throw PushRegistrationError.pushNotSupported(description:"Push not supported on simulators") } return self.registerForVanillaPushToken().then { vanillaPushToken in @@ -62,12 +68,12 @@ import PushKit // we register user notification settings. @objc public func didRegisterUserNotificationSettings() { - guard let fulfillRegisterUserNotificationSettingsPromise = self.fulfillRegisterUserNotificationSettingsPromise else { + guard let fulfillUserNotificationSettingsPromise = self.fulfillUserNotificationSettingsPromise else { owsFail("\(TAG) promise completion in \(#function) unexpectedly nil") return } - fulfillRegisterUserNotificationSettingsPromise() + fulfillUserNotificationSettingsPromise() } // MARK: Vanilla push token @@ -128,13 +134,15 @@ import PushKit private func registerUserNotificationSettings() -> Promise { AssertIsOnMainThread() - guard fulfillRegisterUserNotificationSettingsPromise == nil else { + guard self.userNotificationSettingsPromise == nil else { + let promise = self.userNotificationSettingsPromise! Logger.info("\(TAG) already registered user notification settings") - return Promise(value: ()) + return promise } let (promise, fulfill, _) = Promise.pending() - self.fulfillRegisterUserNotificationSettingsPromise = fulfill + self.userNotificationSettingsPromise = promise + self.fulfillUserNotificationSettingsPromise = fulfill Logger.info("\(TAG) registering user notification settings") @@ -143,18 +151,81 @@ import PushKit 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 { 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.pending() + self.vanillaTokenPromise = promise self.fulfillVanillaTokenPromise = fulfill self.rejectVanillaTokenPromise = reject UIApplication.shared.registerForRemoteNotifications() - return promise.then { (pushTokenData: Data) -> String in + let kTimeout: TimeInterval = 10 + let timeout: Promise = after(seconds: kTimeout).then { 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 + } + }.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() + return pushTokenData.hexEncodedString + }.always { + self.vanillaTokenPromise = nil } } @@ -162,8 +233,15 @@ import PushKit AssertIsOnMainThread() Logger.info("\(self.TAG) in \(#function)") - // Voip token not yet registered, assign promise. + 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.pending() + self.voipTokenPromise = promise self.fulfillVoipTokenPromise = fulfill if self.voipRegistry == nil { @@ -177,10 +255,10 @@ import PushKit guard let voipRegistry = self.voipRegistry else { owsFail("\(TAG) failed to initialize voipRegistry in \(#function)") - reject(PushRegistrationManagerError.assertionError(description: "\(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 + // 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() } } @@ -194,14 +272,16 @@ import PushKit return promise.then { (voipTokenData: Data) -> String in Logger.info("\(self.TAG) successfully registered for voip push notifications") - return voipTokenData.hexEncodedString() + return voipTokenData.hexEncodedString + }.always { + self.voipTokenPromise = nil } } } // We transmit pushToken data as hex encoded string to the server fileprivate extension Data { - func hexEncodedString() -> String { + var hexEncodedString: String { return map { String(format: "%02hhx", $0) }.joined() } } diff --git a/Signal/src/environment/VersionMigrations.m b/Signal/src/environment/VersionMigrations.m index 1122688a9..fceae5a70 100644 --- a/Signal/src/environment/VersionMigrations.m +++ b/Signal/src/environment/VersionMigrations.m @@ -142,7 +142,7 @@ __block BOOL success; - TSUpdateAttributesRequest *request = [[TSUpdateAttributesRequest alloc] initWithUpdatedAttributesWithVoice]; + TSUpdateAttributesRequest *request = [[TSUpdateAttributesRequest alloc] initWithManualMessageFetching:NO]; [[TSNetworkManager sharedManager] makeRequest:request success:^(NSURLSessionDataTask *task, id responseObject) { success = YES; diff --git a/SignalServiceKit/src/Account/TSAccountManager.h b/SignalServiceKit/src/Account/TSAccountManager.h index c978cbb90..ae7e7ed92 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.h +++ b/SignalServiceKit/src/Account/TSAccountManager.h @@ -65,6 +65,9 @@ extern NSString *const kNSNotificationName_LocalNumberDidChange; success:(void (^)())successBlock failure:(void (^)(NSError *error))failureBlock; +- (void)registerForManualMessageFetchingWithSuccess:(void (^)())successBlock + failure:(void (^)(NSError *error))failureBlock; + // Called once registration is complete - meaning the following have succeeded: // - obtained signal server credentials // - uploaded pre-keys diff --git a/SignalServiceKit/src/Account/TSAccountManager.m b/SignalServiceKit/src/Account/TSAccountManager.m index 3800485ab..dc5b61604 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.m +++ b/SignalServiceKit/src/Account/TSAccountManager.m @@ -274,6 +274,20 @@ NSString *const TSAccountManager_LocalRegistrationIdKey = @"TSStorageLocalRegist [self registerWithPhoneNumber:number success:successBlock failure:failureBlock smsVerification:NO]; } +- (void)registerForManualMessageFetchingWithSuccess:(void (^)())successBlock + failure:(void (^)(NSError *error))failureBlock +{ + TSUpdateAttributesRequest *request = [[TSUpdateAttributesRequest alloc] initWithManualMessageFetching:YES]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { + DDLogInfo(@"%@ updated server with account attributes to enableManualFetching", self.tag); + successBlock(); + } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) { + DDLogInfo(@"%@ failed to updat server with account attributes with error: %@", self.tag, error); + failureBlock(error); + }]; +} + - (void)verifyAccountWithCode:(NSString *)verificationCode success:(void (^)())successBlock failure:(void (^)(NSError *error))failureBlock diff --git a/SignalServiceKit/src/Account/TSAttributes.h b/SignalServiceKit/src/Account/TSAttributes.h index d19ab8b17..20f9d8d2f 100644 --- a/SignalServiceKit/src/Account/TSAttributes.h +++ b/SignalServiceKit/src/Account/TSAttributes.h @@ -2,11 +2,16 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // +NS_ASSUME_NONNULL_BEGIN + @interface TSAttributes : NSObject -+ (NSDictionary *)attributesFromStorageWithVoiceSupport; ++ (NSDictionary *)attributesFromStorageWithManualMessageFetching:(BOOL)isEnabled; + (NSDictionary *)attributesWithSignalingKey:(NSString *)signalingKey - serverAuthToken:(NSString *)authToken; + serverAuthToken:(NSString *)authToken + manualMessageFetching:(BOOL)isEnabled; @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Account/TSAttributes.m b/SignalServiceKit/src/Account/TSAttributes.m index 7528dfe6a..ad1053d4a 100644 --- a/SignalServiceKit/src/Account/TSAttributes.m +++ b/SignalServiceKit/src/Account/TSAttributes.m @@ -6,23 +6,31 @@ #import "TSAccountManager.h" #import "TSStorageManager+keyingMaterial.h" +NS_ASSUME_NONNULL_BEGIN + @implementation TSAttributes -+ (NSDictionary *)attributesFromStorageWithVoiceSupport { ++ (NSDictionary *)attributesFromStorageWithManualMessageFetching:(BOOL)isEnabled +{ return [self attributesWithSignalingKey:[TSStorageManager signalingKey] - serverAuthToken:[TSStorageManager serverAuthToken]]; + serverAuthToken:[TSStorageManager serverAuthToken] + manualMessageFetching:isEnabled]; } + (NSDictionary *)attributesWithSignalingKey:(NSString *)signalingKey serverAuthToken:(NSString *)authToken + manualMessageFetching:(BOOL)isEnabled { return @{ @"signalingKey" : signalingKey, @"AuthKey" : authToken, @"voice" : @(YES), // all Signal-iOS clients support voice @"video" : @(YES), // all Signal-iOS clients support WebRTC-based voice and video calls. + @"fetchesMessages" : @(isEnabled), // devices that don't support push must tell the server they fetch messages manually @"registrationId" : [NSString stringWithFormat:@"%i", [TSAccountManager getOrGenerateRegistrationId]] }; } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.h b/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.h index 1347ef864..f59f95577 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.h +++ b/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.h @@ -1,15 +1,11 @@ // -// TSUpdateAttributesRequest.h -// Signal -// -// Created by Frederic Jacobs on 22/08/15. -// Copyright (c) 2015 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSRequest.h" @interface TSUpdateAttributesRequest : TSRequest -- (instancetype)initWithUpdatedAttributesWithVoice; +- (instancetype)initWithManualMessageFetching:(BOOL)isEnabled; @end diff --git a/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.m b/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.m index c27b33457..caf6c4dd3 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSUpdateAttributesRequest.m @@ -1,27 +1,28 @@ // -// TSUpdateAttributesRequest.m -// Signal -// -// Created by Frederic Jacobs on 22/08/15. -// Copyright (c) 2015 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSAttributes.h" #import "TSConstants.h" #import "TSUpdateAttributesRequest.h" +NS_ASSUME_NONNULL_BEGIN + @implementation TSUpdateAttributesRequest -- (instancetype)initWithUpdatedAttributesWithVoice { +- (instancetype)initWithManualMessageFetching:(BOOL)enableManualMessageFetching +{ NSString *endPoint = [textSecureAccountsAPI stringByAppendingString:textSecureAttributesAPI]; - self = [super initWithURL:[NSURL URLWithString:endPoint]]; + self = [super initWithURL:[NSURL URLWithString:endPoint]]; if (self) { [self setHTTPMethod:@"PUT"]; - [self.parameters addEntriesFromDictionary:[TSAttributes attributesFromStorageWithVoiceSupport]]; + [self.parameters addEntriesFromDictionary:[TSAttributes attributesFromStorageWithManualMessageFetching:enableManualMessageFetching]]; } - + return self; } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Network/API/Requests/TSVerifyCodeRequest.m b/SignalServiceKit/src/Network/API/Requests/TSVerifyCodeRequest.m index 5c67b2df0..ed7e6bdfa 100644 --- a/SignalServiceKit/src/Network/API/Requests/TSVerifyCodeRequest.m +++ b/SignalServiceKit/src/Network/API/Requests/TSVerifyCodeRequest.m @@ -1,9 +1,5 @@ // -// TSRegisterWithTokenRequest.m -// TextSecureKit -// -// Created by Frederic Jacobs on 14/11/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "TSAccountManager.h" @@ -22,7 +18,7 @@ stringWithFormat:@"%@/code/%@", textSecureAccountsAPI, verificationCode]]]; NSDictionary *attributes = - [TSAttributes attributesWithSignalingKey:signalingKey serverAuthToken:authKey]; + [TSAttributes attributesWithSignalingKey:signalingKey serverAuthToken:authKey manualMessageFetching:NO]; _numberToValidate = phoneNumber; [self.parameters addEntriesFromDictionary:attributes];