Register for manual message fetching when unable to obtain push tokens

// FREEBIE
This commit is contained in:
Michael Kirk 2017-10-11 10:23:24 -04:00
parent df15c904bc
commit 9a7e3cb9d8
13 changed files with 182 additions and 61 deletions

View File

@ -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);

View File

@ -46,6 +46,17 @@ class AccountManager: NSObject {
self.registerForTextSecure(verificationCode: verificationCode)
}.then {
self.syncPushTokens()
}.recover { (error) -> Promise<Void> 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<Void> {
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<Void> {
return Promise { fulfill, reject in
self.textSecureAccountManager.registerForPushNotifications(pushToken:pushToken,
voipToken:voipToken,
@ -96,6 +95,12 @@ class AccountManager: NSObject {
}
}
func registerForManualMessageFetching() -> Promise<Void> {
return Promise { fulfill, reject in
self.textSecureAccountManager.registerForManualMessageFetching(success:fulfill, failure:reject)
}
}
// MARK: Turn Server
func getTurnServerInfo() -> Promise<TurnServerInfo> {

View File

@ -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).")
}

View File

@ -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);

View File

@ -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<Void>?
private var fulfillUserNotificationSettingsPromise: (() -> Void)?
private var voipRegistry: PKPushRegistry?
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)?
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<Void> {
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<Void>.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<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()
return promise.then { (pushTokenData: Data) -> String in
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()
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<Data>.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()
}
}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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];