diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 84500eefb..c9ef0a139 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -76,6 +76,69 @@ static NSTimeInterval launchStartedAt; @synthesize window = _window; +#pragma mark - Dependencies + +- (OWSProfileManager *)profileManager +{ + return [OWSProfileManager sharedManager]; +} + +- (OWSReadReceiptManager *)readReceiptManager +{ + return [OWSReadReceiptManager sharedManager]; +} + +- (id)udManager +{ + OWSAssertDebug(SSKEnvironment.shared.udManager); + + return SSKEnvironment.shared.udManager; +} + +- (OWSPrimaryStorage *)primaryStorage +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + +- (PushRegistrationManager *)pushRegistrationManager +{ + OWSAssertDebug(AppEnvironment.shared.pushRegistrationManager); + + return AppEnvironment.shared.pushRegistrationManager; +} + +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +- (OWSDisappearingMessagesJob *)disappearingMessagesJob +{ + OWSAssertDebug(SSKEnvironment.shared.disappearingMessagesJob); + + return SSKEnvironment.shared.disappearingMessagesJob; +} + +- (TSSocketManager *)socketManager +{ + OWSAssertDebug(SSKEnvironment.shared.socketManager); + + return SSKEnvironment.shared.socketManager; +} + +- (OWSMessageManager *)messageManager +{ + OWSAssertDebug(SSKEnvironment.shared.messageManager); + + return SSKEnvironment.shared.messageManager; +} + +#pragma mark - + - (void)applicationDidEnterBackground:(UIApplication *)application { OWSLogWarn(@"applicationDidEnterBackground."); @@ -425,7 +488,7 @@ static NSTimeInterval launchStartedAt; } OWSLogInfo(@"registered vanilla push token: %@", deviceToken); - [PushRegistrationManager.shared didReceiveVanillaPushToken:deviceToken]; + [self.pushRegistrationManager didReceiveVanillaPushToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error @@ -440,10 +503,10 @@ static NSTimeInterval launchStartedAt; OWSLogError(@"failed to register vanilla push token with error: %@", error); #ifdef DEBUG OWSLogWarn(@"We're in debug mode. Faking success for remote registration with a fake push identifier"); - [PushRegistrationManager.shared didReceiveVanillaPushToken:[[NSMutableData dataWithLength:32] copy]]; + [self.pushRegistrationManager didReceiveVanillaPushToken:[[NSMutableData dataWithLength:32] copy]]; #else OWSProdError([OWSAnalyticsEvents appDelegateErrorFailedToRegisterForRemoteNotifications]); - [PushRegistrationManager.shared didFailToReceiveVanillaPushTokenWithError:error]; + [self.pushRegistrationManager didFailToReceiveVanillaPushTokenWithError:error]; #endif } @@ -458,7 +521,7 @@ static NSTimeInterval launchStartedAt; } OWSLogInfo(@"registered user notification settings"); - [PushRegistrationManager.shared didRegisterUserNotificationSettings]; + [self.pushRegistrationManager didRegisterUserNotificationSettings]; } - (BOOL)application:(UIApplication *)application @@ -482,7 +545,7 @@ static NSTimeInterval launchStartedAt; } if ([url.scheme isEqualToString:kURLSchemeSGNLKey]) { - if ([url.host hasPrefix:kURLHostVerifyPrefix] && ![TSAccountManager isRegistered]) { + if ([url.host hasPrefix:kURLHostVerifyPrefix] && ![self.tsAccountManager isRegistered]) { id signupController = SignalApp.sharedApp.signUpFlowNavigationController; if ([signupController isKindOfClass:[OWSNavigationController class]]) { OWSNavigationController *navController = (OWSNavigationController *)signupController; @@ -542,7 +605,7 @@ static NSTimeInterval launchStartedAt; - (void)enableBackgroundRefreshIfNecessary { [AppReadiness runNowOrWhenAppIsReady:^{ - if (OWS2FAManager.sharedManager.is2FAEnabled && [TSAccountManager isRegistered]) { + if (OWS2FAManager.sharedManager.is2FAEnabled && [self.tsAccountManager isRegistered]) { // Ping server once a day to keep-alive 2FA clients. const NSTimeInterval kBackgroundRefreshInterval = 24 * 60 * 60; [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:kBackgroundRefreshInterval]; @@ -566,26 +629,25 @@ static NSTimeInterval launchStartedAt; dispatch_once(&onceToken, ^{ RTCInitializeSSL(); - if ([TSAccountManager isRegistered]) { + if ([self.tsAccountManager isRegistered]) { // At this point, potentially lengthy DB locking migrations could be running. // Avoid blocking app launch by putting all further possible DB access in async block dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - OWSLogInfo(@"running post launch block for registered user: %@", [TSAccountManager localNumber]); + OWSLogInfo(@"running post launch block for registered user: %@", [self.tsAccountManager localNumber]); // Clean up any messages that expired since last launch immediately // and continue cleaning in the background. - [[OWSDisappearingMessagesJob sharedJob] startIfNecessary]; + [self.disappearingMessagesJob startIfNecessary]; [self enableBackgroundRefreshIfNecessary]; // Mark all "attempting out" messages as "unsent", i.e. any messages that were not successfully // sent before the app exited should be marked as failures. - [[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; + [[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:self.primaryStorage] run]; // Mark all "incomplete" calls as missed, e.g. any incoming or outgoing calls that were not // connected, failed or hung up before the app existed should be marked as missed. - [[[OWSIncompleteCallsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; - [[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] - run]; + [[[OWSIncompleteCallsJob alloc] initWithPrimaryStorage:self.primaryStorage] run]; + [[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:self.primaryStorage] run]; }); } else { OWSLogInfo(@"running post launch block for unregistered user."); @@ -593,7 +655,7 @@ static NSTimeInterval launchStartedAt; // Unregistered user should have no unread messages. e.g. if you delete your account. [SignalApp clearAllNotifications]; - [TSSocketManager.shared requestSocketOpen]; + [self.socketManager requestSocketOpen]; UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:[Pastelog class] action:@selector(submitLogs)]; @@ -603,11 +665,11 @@ static NSTimeInterval launchStartedAt; }); // end dispatchOnce for first time we become active // Every time we become active... - if ([TSAccountManager isRegistered]) { + if ([self.tsAccountManager isRegistered]) { // At this point, potentially lengthy DB locking migrations could be running. // Avoid blocking app launch by putting all further possible DB access in async block dispatch_async(dispatch_get_main_queue(), ^{ - [TSSocketManager.shared requestSocketOpen]; + [self.socketManager requestSocketOpen]; [Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized]; // This will fetch new messages, if we're using domain fronting. [[PushManager sharedManager] applicationDidBecomeActive]; @@ -674,7 +736,7 @@ static NSTimeInterval launchStartedAt; } [AppReadiness runNowOrWhenAppIsReady:^{ - if (![TSAccountManager isRegistered]) { + if (![self.tsAccountManager isRegistered]) { UIAlertController *controller = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"REGISTER_CONTACTS_WELCOME", nil) message:NSLocalizedString(@"REGISTRATION_RESTRICTED_MESSAGE", nil) @@ -747,7 +809,7 @@ static NSTimeInterval launchStartedAt; [AppReadiness runNowOrWhenAppIsReady:^{ NSString *_Nullable phoneNumber = handle; if ([handle hasPrefix:CallKitCallManager.kAnonymousCallHandlePrefix]) { - phoneNumber = [[OWSPrimaryStorage sharedManager] phoneNumberForCallKitId:handle]; + phoneNumber = [self.primaryStorage phoneNumberForCallKitId:handle]; if (phoneNumber.length < 1) { OWSLogWarn(@"ignoring attempt to initiate video call to unknown anonymous signal user."); return; @@ -804,7 +866,7 @@ static NSTimeInterval launchStartedAt; [AppReadiness runNowOrWhenAppIsReady:^{ NSString *_Nullable phoneNumber = handle; if ([handle hasPrefix:CallKitCallManager.kAnonymousCallHandlePrefix]) { - phoneNumber = [[OWSPrimaryStorage sharedManager] phoneNumberForCallKitId:handle]; + phoneNumber = [self.primaryStorage phoneNumberForCallKitId:handle]; if (phoneNumber.length < 1) { OWSLogWarn(@"ignoring attempt to initiate audio call to unknown anonymous signal user."); return; @@ -1010,8 +1072,8 @@ static NSTimeInterval launchStartedAt; OWSLogInfo(@"checkIfAppIsReady"); // TODO: Once "app ready" logic is moved into AppSetup, move this line there. - [[OWSProfileManager sharedManager] ensureLocalProfileCached]; - + [self.profileManager ensureLocalProfileCached]; + // Note that this does much more than set a flag; // it will also run all deferred blocks. [AppReadiness setAppIsReady]; @@ -1021,8 +1083,8 @@ static NSTimeInterval launchStartedAt; return; } - if ([TSAccountManager isRegistered]) { - OWSLogInfo(@"localNumber: %@", [TSAccountManager localNumber]); + if ([self.tsAccountManager isRegistered]) { + OWSLogVerbose(@"localNumber: %@", [self.tsAccountManager localNumber]); // Fetch messages as soon as possible after launching. In particular, when // launching from the background, without this, we end up waiting some extra @@ -1047,7 +1109,7 @@ static NSTimeInterval launchStartedAt; [SSKEnvironment.shared.batchMessageProcessor handleAnyUnprocessedEnvelopesAsync]; if (!Environment.shared.preferences.hasGeneratedThumbnails) { - [OWSPrimaryStorage.sharedManager.newDatabaseConnection + [self.primaryStorage.newDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { [TSAttachmentStream enumerateCollectionObjectsUsingBlock:^(id _Nonnull obj, BOOL *_Nonnull stop){ // no-op. It's sufficient to initWithCoder: each object. @@ -1069,8 +1131,8 @@ static NSTimeInterval launchStartedAt; [OWSOrphanDataCleaner auditOnLaunchIfNecessary]; #endif - [OWSProfileManager.sharedManager fetchLocalUsersProfile]; - [[OWSReadReceiptManager sharedManager] prepareCachedValues]; + [self.profileManager fetchLocalUsersProfile]; + [self.readReceiptManager prepareCachedValues]; // Disable the SAE until the main app has successfully completed launch process // at least once in the post-SAE world. @@ -1080,14 +1142,14 @@ static NSTimeInterval launchStartedAt; [OWSBackup.sharedManager setup]; - [SSKEnvironment.shared.messageManager startObserving]; + [self.messageManager startObserving]; #ifdef DEBUG // Resume lazy restore. [OWSBackupLazyRestoreJob runAsync]; #endif - [SSKEnvironment.shared.udManager setup]; + [self.udManager setup]; } - (void)registrationStateDidChange @@ -1098,20 +1160,20 @@ static NSTimeInterval launchStartedAt; [self enableBackgroundRefreshIfNecessary]; - if ([TSAccountManager isRegistered]) { - OWSLogInfo(@"localNumber: %@", [TSAccountManager localNumber]); + if ([self.tsAccountManager isRegistered]) { + OWSLogInfo(@"localNumber: %@", [self.tsAccountManager localNumber]); - [[OWSPrimaryStorage sharedManager].newDatabaseConnection + [self.primaryStorage.newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [ExperienceUpgradeFinder.sharedManager markAllAsSeenWithTransaction:transaction]; }]; // Start running the disappearing messages job in case the newly registered user // enables this feature - [[OWSDisappearingMessagesJob sharedJob] startIfNecessary]; - [[OWSProfileManager sharedManager] ensureLocalProfileCached]; + [self.disappearingMessagesJob startIfNecessary]; + [self.profileManager ensureLocalProfileCached]; // For non-legacy users, read receipts are on by default. - [OWSReadReceiptManager.sharedManager setAreReadReceiptsEnabled:YES]; + [self.readReceiptManager setAreReadReceiptsEnabled:YES]; } } @@ -1134,7 +1196,7 @@ static NSTimeInterval launchStartedAt; NSTimeInterval startupDuration = CACurrentMediaTime() - launchStartedAt; OWSLogInfo(@"Presenting app %.2f seconds after launch started.", startupDuration); - if ([TSAccountManager isRegistered]) { + if ([self.tsAccountManager isRegistered]) { HomeViewController *homeView = [HomeViewController new]; SignalsNavigationController *navigationController = [[SignalsNavigationController alloc] initWithRootViewController:homeView]; diff --git a/Signal/src/ViewControllers/AppSettings/OWSLinkedDevicesTableViewController.m b/Signal/src/ViewControllers/AppSettings/OWSLinkedDevicesTableViewController.m index f89483fff..304e2df80 100644 --- a/Signal/src/ViewControllers/AppSettings/OWSLinkedDevicesTableViewController.m +++ b/Signal/src/ViewControllers/AppSettings/OWSLinkedDevicesTableViewController.m @@ -68,6 +68,18 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1; selector:@selector(yapDatabaseModifiedExternally:) name:YapDatabaseModifiedExternallyNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceListUpdateSucceeded:) + name:NSNotificationName_DeviceListUpdateSucceeded + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceListUpdateFailed:) + name:NSNotificationName_DeviceListUpdateFailed + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceListUpdateModifiedDeviceList:) + name:NSNotificationName_DeviceListUpdateModifiedDeviceList + object:nil]; self.refreshControl = [UIRefreshControl new]; [self.refreshControl addTarget:self action:@selector(refreshDevices) forControlEvents:UIControlEventValueChanged]; @@ -141,61 +153,54 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1; - (void)refreshDevices { - __weak typeof(self) wself = self; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [[OWSDevicesService new] - getDevicesWithSuccess:^(NSArray *devices) { - // If we have more than one device; we may have a linked device. - if (devices.count > 1) { - // Setting this flag here shouldn't be necessary, but we do so - // because the "cost" is low and it will improve robustness. - [OWSDeviceManager.sharedManager setMayHaveLinkedDevices]; - } - - if (devices.count > [OWSDevice numberOfKeysInCollection]) { - // Got our new device, we can stop refreshing. - wself.isExpectingMoreDevices = NO; - [wself.pollingRefreshTimer invalidate]; - dispatch_async(dispatch_get_main_queue(), ^{ - wself.refreshControl.attributedTitle = nil; - }); - } - [OWSDevice replaceAll:devices]; - - if (!self.isExpectingMoreDevices) { - dispatch_async(dispatch_get_main_queue(), ^{ - [wself.refreshControl endRefreshing]; - }); - } - } - failure:^(NSError *error) { - OWSLogError(@"Failed to fetch devices in linkedDevices controller with error: %@", error); - - NSString *alertTitle = NSLocalizedString( - @"DEVICE_LIST_UPDATE_FAILED_TITLE", @"Alert title that can occur when viewing device manager."); - - UIAlertController *alertController = - [UIAlertController alertControllerWithTitle:alertTitle - message:error.localizedDescription - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *retryAction = [UIAlertAction actionWithTitle:[CommonStrings retryButton] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [wself refreshDevices]; - }]; - [alertController addAction:retryAction]; - - UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton - style:UIAlertActionStyleCancel - handler:nil]; - [alertController addAction:dismissAction]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [wself.refreshControl endRefreshing]; - [wself presentViewController:alertController animated:YES completion:nil]; - }); - }]; + [OWSDevicesService refreshDevices]; +} + +- (void)deviceListUpdateSucceeded:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self.refreshControl endRefreshing]; +} + +- (void)deviceListUpdateFailed:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + NSError *error = notification.object; + OWSAssertDebug(error); + + NSString *alertTitle = NSLocalizedString( + @"DEVICE_LIST_UPDATE_FAILED_TITLE", @"Alert title that can occur when viewing device manager."); + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:alertTitle + message:error.localizedDescription + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *retryAction = [UIAlertAction actionWithTitle:[CommonStrings retryButton] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self refreshDevices]; + }]; + [alertController addAction:retryAction]; + + UIAlertAction *dismissAction = + [UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil]; + [alertController addAction:dismissAction]; + + [self.refreshControl endRefreshing]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void)deviceListUpdateModifiedDeviceList:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + // Got our new device, we can stop refreshing. + self.isExpectingMoreDevices = NO; + [self.pollingRefreshTimer invalidate]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.refreshControl.attributedTitle = nil; }); } @@ -397,29 +402,29 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1; - (void)unlinkDevice:(OWSDevice *)device success:(void (^)(void))successCallback { - [[OWSDevicesService new] unlinkDevice:device - success:successCallback - failure:^(NSError *error) { - NSString *title = NSLocalizedString( - @"UNLINKING_FAILED_ALERT_TITLE", @"Alert title when unlinking device fails"); - UIAlertController *alertController = - [UIAlertController alertControllerWithTitle:title - message:error.localizedDescription - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *retryAction = - [UIAlertAction actionWithTitle:[CommonStrings retryButton] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *aaction) { - [self unlinkDevice:device success:successCallback]; - }]; - [alertController addAction:retryAction]; - [alertController addAction:[OWSAlerts cancelAction]]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self presentViewController:alertController animated:YES completion:nil]; - }); - }]; + [OWSDevicesService unlinkDevice:device + success:successCallback + failure:^(NSError *error) { + NSString *title = NSLocalizedString( + @"UNLINKING_FAILED_ALERT_TITLE", @"Alert title when unlinking device fails"); + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:title + message:error.localizedDescription + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *retryAction = + [UIAlertAction actionWithTitle:[CommonStrings retryButton] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *aaction) { + [self unlinkDevice:device success:successCallback]; + }]; + [alertController addAction:retryAction]; + [alertController addAction:[OWSAlerts cancelAction]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self presentViewController:alertController animated:YES completion:nil]; + }); + }]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(nullable id)sender diff --git a/SignalMessaging/profiles/ProfileFetcherJob.swift b/SignalMessaging/profiles/ProfileFetcherJob.swift index 98d291777..cfd2da60d 100644 --- a/SignalMessaging/profiles/ProfileFetcherJob.swift +++ b/SignalMessaging/profiles/ProfileFetcherJob.swift @@ -142,7 +142,8 @@ public class ProfileFetcherJob: NSObject { }, websocketFailureBlock: { // Do nothing }, recipientId: recipientId, - unidentifiedAccess: unidentifiedAccess) + unidentifiedAccess: unidentifiedAccess, + canFailoverUDAuth: true) return requestMaker.makeRequest() .map { (result: RequestMakerResult) -> SignalServiceProfile in try SignalServiceProfile(recipientId: recipientId, responseObject: result.responseObject) diff --git a/SignalServiceKit/src/Devices/OWSDevice.h b/SignalServiceKit/src/Devices/OWSDevice.h index 32c0fdee9..90bdfb65c 100644 --- a/SignalServiceKit/src/Devices/OWSDevice.h +++ b/SignalServiceKit/src/Devices/OWSDevice.h @@ -35,13 +35,17 @@ extern uint32_t const OWSDevicePrimaryDeviceId; + (nullable instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error; ++ (NSArray *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction; + /** * Set local database of devices to `devices`. * * This will create missing devices, update existing devices, and delete stale devices. * @param devices Removes any existing devices, replacing them with `devices` + * + * Returns YET if any devices were added or removed. */ -+ (void)replaceAll:(NSArray *)devices; ++ (BOOL)replaceAll:(NSArray *)devices; /** * The id of the device currently running this application diff --git a/SignalServiceKit/src/Devices/OWSDevice.m b/SignalServiceKit/src/Devices/OWSDevice.m index b831ad55a..8fec95a7f 100644 --- a/SignalServiceKit/src/Devices/OWSDevice.m +++ b/SignalServiceKit/src/Devices/OWSDevice.m @@ -171,21 +171,37 @@ NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_Ma return self.millisecondTimestampToDateTransformer; } -+ (void)replaceAll:(NSArray *)currentDevices ++ (NSArray *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction { - BOOL didChange = NO; + OWSAssertDebug(transaction); + + NSMutableArray *result = [NSMutableArray new]; + [transaction enumerateKeysAndObjectsInCollection:OWSDevice.collection + usingBlock:^(NSString *key, OWSDevice *object, BOOL *stop) { + if (![object isKindOfClass:[OWSDevice class]]) { + OWSFailDebug(@"Unexpected object in collection: %@", object.class); + return; + } + [result addObject:object]; + }]; + return result; +} + ++ (BOOL)replaceAll:(NSArray *)currentDevices +{ + BOOL didAddOrRemove = NO; NSMutableArray *existingDevices = [[self allObjectsInCollection] mutableCopy]; for (OWSDevice *currentDevice in currentDevices) { NSUInteger existingDeviceIndex = [existingDevices indexOfObject:currentDevice]; if (existingDeviceIndex == NSNotFound) { // New Device + OWSLogInfo(@"Adding device: %@", currentDevice); [currentDevice save]; - didChange = YES; + didAddOrRemove = YES; } else { OWSDevice *existingDevice = existingDevices[existingDeviceIndex]; if ([existingDevice updateAttributesWithDevice:currentDevice]) { [existingDevice save]; - didChange = YES; } [existingDevices removeObjectAtIndex:existingDeviceIndex]; } @@ -193,11 +209,12 @@ NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_Ma // Since we removed existing devices as we went, only stale devices remain for (OWSDevice *staleDevice in existingDevices) { + OWSLogVerbose(@"Removing device: %@", staleDevice); [staleDevice remove]; - didChange = YES; + didAddOrRemove = YES; } - if (didChange) { + if (didAddOrRemove) { dispatch_async(dispatch_get_main_queue(), ^{ // Device changes can affect the UD access mode for a recipient, // so we need to: @@ -208,6 +225,9 @@ NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_Ma recipientId:self.tsAccountManager.localNumber]; [self.profileManager fetchLocalUsersProfile]; }); + return YES; + } else { + return NO; } } diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 97230fe21..5c6c9fbea 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -14,6 +14,7 @@ #import "OWSCallMessageHandler.h" #import "OWSContact.h" #import "OWSDevice.h" +#import "OWSDevicesService.h" #import "OWSDisappearingConfigurationUpdateInfoMessage.h" #import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesJob.h" @@ -47,6 +48,7 @@ #import #import #import +#import #import #import @@ -89,6 +91,8 @@ NS_ASSUME_NONNULL_BEGIN return self; } +#pragma mark - Dependencies + - (id)callMessageHandler { OWSAssertDebug(SSKEnvironment.shared.callMessageHandler); @@ -145,6 +149,13 @@ NS_ASSUME_NONNULL_BEGIN return SSKEnvironment.shared.syncManager; } +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + #pragma mark - - (void)startObserving @@ -235,6 +246,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(![self isEnvelopeSenderBlocked:envelope]); + [self checkForUnknownLinkedDevice:envelope transaction:transaction]; + switch (envelope.type) { case SSKProtoEnvelopeTypeCiphertext: case SSKProtoEnvelopeTypePrekeyBundle: @@ -471,7 +484,7 @@ NS_ASSUME_NONNULL_BEGIN if (groupThread) { if (dataMessage.group.type != SSKProtoGroupContextTypeUpdate) { - if (![groupThread.groupModel.groupMemberIds containsObject:[TSAccountManager localNumber]]) { + if (![groupThread.groupModel.groupMemberIds containsObject:self.tsAccountManager.localNumber]) { OWSLogInfo(@"Ignoring messages for left group."); return; } @@ -749,7 +762,7 @@ NS_ASSUME_NONNULL_BEGIN return; } - NSString *localNumber = [TSAccountManager localNumber]; + NSString *localNumber = self.tsAccountManager.localNumber; if (![localNumber isEqualToString:envelope.source]) { // Sync messages should only come from linked devices. OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorSyncMessageFromUnknownSource], envelope); @@ -1062,7 +1075,7 @@ NS_ASSUME_NONNULL_BEGIN } // Ensure we are in the group. - NSString *localNumber = [TSAccountManager localNumber]; + NSString *localNumber = self.tsAccountManager.localNumber; if (![gThread.groupModel.groupMemberIds containsObject:localNumber]) { OWSLogWarn(@"Ignoring 'Request Group Info' message for group we no longer belong to."); return; @@ -1298,7 +1311,7 @@ NS_ASSUME_NONNULL_BEGIN [incomingMessage saveWithTransaction:transaction]; // Any messages sent from the current user - from this device or another - should be automatically marked as read. - if ([envelope.source isEqualToString:TSAccountManager.localNumber]) { + if ([envelope.source isEqualToString:self.tsAccountManager.localNumber]) { // Don't send a read receipt for messages sent by ourselves. [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; } @@ -1424,6 +1437,48 @@ NS_ASSUME_NONNULL_BEGIN } } +#pragma mark - + +- (void)checkForUnknownLinkedDevice:(SSKProtoEnvelope *)envelope + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(envelope); + OWSAssertDebug(transaction); + + NSString *localNumber = self.tsAccountManager.localNumber; + if (![localNumber isEqualToString:envelope.source]) { + return; + } + + NSMutableSet *deviceIdSet = [NSMutableSet new]; + for (OWSDevice *device in [OWSDevice currentDevicesWithTransaction:transaction]) { + [deviceIdSet addObject:@(device.deviceId)]; + } + SignalRecipient *_Nullable recipient = + [SignalRecipient registeredRecipientForRecipientId:localNumber transaction:transaction]; + if (!recipient) { + OWSFailDebug(@"No local SignalRecipient."); + } else { + BOOL isRecipientDevice = [recipient.devices containsObject:@(envelope.sourceDevice)]; + if (!isRecipientDevice) { + OWSLogInfo(@"Message received from unknown linked device; adding to local SignalRecipient: %lu.", + (unsigned long) envelope.sourceDevice); + + [recipient updateRegisteredRecipientWithDevicesToAdd:@[ @(envelope.sourceDevice) ] + devicesToRemove:nil + transaction:transaction]; + } + } + + BOOL isInDeviceList = [deviceIdSet containsObject:@(envelope.sourceDevice)]; + if (!isInDeviceList) { + OWSLogInfo(@"Message received from unknown linked device; refreshing device list: %lu.", + (unsigned long) envelope.sourceDevice); + + [OWSDevicesService refreshDevices]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index ab1969ca0..c7edf9682 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -933,6 +933,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // Consume an attempt. messageSend.remainingAttempts = messageSend.remainingAttempts - 1; + if ([message isKindOfClass:[OWSOutgoingSyncMessage class]] + && ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) { + [messageSend disableUD]; + } + NSError *deviceMessagesError; NSArray *_Nullable deviceMessages = [self deviceMessagesForMessageSendSafe:messageSend error:&deviceMessagesError]; @@ -1028,11 +1033,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSLogWarn(@"Sending a message with no device messages."); } - if ([message isKindOfClass:[OWSOutgoingSyncMessage class]] - && ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) { - [messageSend disableUD]; - } - + // NOTE: canFailoverUDAuth is NO because UD-auth and Non-UD-auth requests + // use different device lists. OWSRequestMaker *requestMaker = [[OWSRequestMaker alloc] initWithRequestFactoryBlock:^(SSKUnidentifiedAccess *_Nullable unidentifiedAccess) { return [OWSRequestFactory submitMessageRequestWithRecipient:recipient.recipientId @@ -1047,7 +1049,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; messageSend.hasWebsocketSendFailed = YES; } recipientId:recipient.recipientId - unidentifiedAccess:messageSend.unidentifiedAccess]; + unidentifiedAccess:messageSend.unidentifiedAccess + canFailoverUDAuth:NO]; [[requestMaker makeRequestObjc] .then(^(OWSRequestMakerResult *result) { dispatch_async([OWSDispatch sendingQueue], ^{ @@ -1059,7 +1062,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; dispatch_async([OWSDispatch sendingQueue], ^{ NSUInteger statusCode = 0; NSData *_Nullable responseData = nil; - if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + if ([error.domain isEqualToString:@"SignalServiceKit.RequestMakerUDAuthError"]) { + // Try again. + OWSLogInfo(@"UD request auth failed; failing over to non-UD request."); + [error setIsRetryable:YES]; + } else if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { statusCode = error.code; NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; @@ -1508,7 +1515,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; messageSend.hasWebsocketSendFailed = YES; } recipientId:recipientId - unidentifiedAccess:messageSend.unidentifiedAccess]; + unidentifiedAccess:messageSend.unidentifiedAccess + canFailoverUDAuth:YES]; [[requestMaker makeRequestObjc] .then(^(OWSRequestMakerResult *result) { // We _do not_ want to dispatch to the sendingQueue here; we're diff --git a/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift index cc5f99265..edd8b5e98 100644 --- a/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift +++ b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift @@ -5,6 +5,11 @@ import Foundation import PromiseKit +@objc +public enum RequestMakerUDAuthError: Int, Error { + case udAuthFailure +} + public enum RequestMakerError: Error { case websocketRequestError(statusCode : Int, responseData : Data?, underlyingError : Error) } @@ -40,18 +45,21 @@ public class RequestMaker: NSObject { private let websocketFailureBlock: WebsocketFailureBlock private let recipientId: String private let unidentifiedAccess: SSKUnidentifiedAccess? + private let canFailoverUDAuth: Bool @objc public init(requestFactoryBlock : @escaping RequestFactoryBlock, udAuthFailureBlock : @escaping UDAuthFailureBlock, websocketFailureBlock : @escaping WebsocketFailureBlock, recipientId: String, - unidentifiedAccess: SSKUnidentifiedAccess?) { + unidentifiedAccess: SSKUnidentifiedAccess?, + canFailoverUDAuth: Bool) { self.requestFactoryBlock = requestFactoryBlock self.udAuthFailureBlock = udAuthFailureBlock self.websocketFailureBlock = websocketFailureBlock self.recipientId = recipientId self.unidentifiedAccess = unidentifiedAccess + self.canFailoverUDAuth = canFailoverUDAuth } // MARK: - Dependencies @@ -115,8 +123,13 @@ public class RequestMaker: NSObject { // failure), mark recipient as _not_ in UD mode, then retry. self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) self.udAuthFailureBlock() - Logger.info("UD websocket request failed; failing over to non-UD websocket request.") - return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + if self.canFailoverUDAuth { + Logger.info("UD websocket request auth failed; failing over to non-UD websocket request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } else { + Logger.info("UD websocket request auth failed; aborting.") + throw RequestMakerUDAuthError.udAuthFailure + } } break default: @@ -141,8 +154,13 @@ public class RequestMaker: NSObject { // failure), mark recipient as _not_ in UD mode, then retry. self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) self.udAuthFailureBlock() - Logger.info("UD REST request failed; failing over to non-UD REST request.") - return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + if self.canFailoverUDAuth { + Logger.info("UD REST request auth failed; failing over to non-UD REST request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } else { + Logger.info("UD REST request auth failed; aborting.") + throw RequestMakerUDAuthError.udAuthFailure + } } break default: diff --git a/SignalServiceKit/src/Network/API/OWSDevicesService.h b/SignalServiceKit/src/Network/API/OWSDevicesService.h index 761494efa..fcdb36848 100644 --- a/SignalServiceKit/src/Network/API/OWSDevicesService.h +++ b/SignalServiceKit/src/Network/API/OWSDevicesService.h @@ -1,17 +1,20 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN +extern NSString *const NSNotificationName_DeviceListUpdateSucceeded; +extern NSString *const NSNotificationName_DeviceListUpdateFailed; +extern NSString *const NSNotificationName_DeviceListUpdateModifiedDeviceList; + @class OWSDevice; @interface OWSDevicesService : NSObject -- (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback - failure:(void (^)(NSError *))failureCallback; ++ (void)refreshDevices; -- (void)unlinkDevice:(OWSDevice *)device ++ (void)unlinkDevice:(OWSDevice *)device success:(void (^)(void))successCallback failure:(void (^)(NSError *))failureCallback; diff --git a/SignalServiceKit/src/Network/API/OWSDevicesService.m b/SignalServiceKit/src/Network/API/OWSDevicesService.m index 2cae90742..9ce96728d 100644 --- a/SignalServiceKit/src/Network/API/OWSDevicesService.m +++ b/SignalServiceKit/src/Network/API/OWSDevicesService.m @@ -3,6 +3,7 @@ // #import "OWSDevicesService.h" +#import "NSNotificationCenter+OWS.h" #import "OWSDevice.h" #import "OWSError.h" #import "OWSRequestFactory.h" @@ -11,9 +12,47 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const NSNotificationName_DeviceListUpdateSucceeded = @"NSNotificationName_DeviceListUpdateSucceeded"; +NSString *const NSNotificationName_DeviceListUpdateFailed = @"NSNotificationName_DeviceListUpdateFailed"; +NSString *const NSNotificationName_DeviceListUpdateModifiedDeviceList + = @"NSNotificationName_DeviceListUpdateModifiedDeviceList"; + @implementation OWSDevicesService -- (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback ++ (void)refreshDevices +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self + getDevicesWithSuccess:^(NSArray *devices) { + // If we have more than one device; we may have a linked device. + if (devices.count > 1) { + // Setting this flag here shouldn't be necessary, but we do so + // because the "cost" is low and it will improve robustness. + [OWSDeviceManager.sharedManager setMayHaveLinkedDevices]; + } + + BOOL didAddOrRemove = [OWSDevice replaceAll:devices]; + + [NSNotificationCenter.defaultCenter + postNotificationNameAsync:NSNotificationName_DeviceListUpdateSucceeded + object:nil]; + + if (didAddOrRemove) { + [NSNotificationCenter.defaultCenter + postNotificationNameAsync:NSNotificationName_DeviceListUpdateModifiedDeviceList + object:nil]; + } + } + failure:^(NSError *error) { + OWSLogError(@"Request device list failed with error: %@", error); + + [NSNotificationCenter.defaultCenter postNotificationNameAsync:NSNotificationName_DeviceListUpdateFailed + object:error]; + }]; + }); +} + ++ (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback failure:(void (^)(NSError *))failureCallback { TSRequest *request = [OWSRequestFactory getDevicesRequest]; @@ -39,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (void)unlinkDevice:(OWSDevice *)device ++ (void)unlinkDevice:(OWSDevice *)device success:(void (^)(void))successCallback failure:(void (^)(NSError *))failureCallback { @@ -59,7 +98,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (NSArray *)parseResponse:(id)responseObject ++ (NSArray *)parseResponse:(id)responseObject { if (![responseObject isKindOfClass:[NSDictionary class]]) { OWSLogError(@"Device response was not a dictionary.");