diff --git a/Signal/src/ViewControllers/OWSLinkDeviceViewController.m b/Signal/src/ViewControllers/OWSLinkDeviceViewController.m index 52da44807..81385f76e 100644 --- a/Signal/src/ViewControllers/OWSLinkDeviceViewController.m +++ b/Signal/src/ViewControllers/OWSLinkDeviceViewController.m @@ -9,6 +9,7 @@ #import "OWSProfileManager.h" #import "Signal-Swift.h" #import +#import #import #import #import @@ -18,9 +19,10 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSLinkDeviceViewController () -@property (strong, nonatomic) IBOutlet UIView *qrScanningView; -@property (strong, nonatomic) IBOutlet UILabel *scanningInstructionsLabel; -@property (strong, nonatomic) OWSQRCodeScanningViewController *qrScanningController; +@property (nonatomic) YapDatabaseConnection *dbConnection; +@property (nonatomic) IBOutlet UIView *qrScanningView; +@property (nonatomic) IBOutlet UILabel *scanningInstructionsLabel; +@property (nonatomic) OWSQRCodeScanningViewController *qrScanningController; @property (nonatomic, readonly) OWSReadReceiptManager *readReceiptManager; @end @@ -31,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN { [super viewDidLoad]; + self.dbConnection = [[TSStorageManager sharedManager] newDatabaseConnection]; + // HACK to get full width preview layer CGRect oldFrame = self.qrScanningView.frame; self.qrScanningView.frame = CGRectMake( @@ -142,6 +146,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)provisionWithParser:(OWSDeviceProvisioningURLParser *)parser { + // Optimistically set this flag. + [OWSDeviceManager.sharedManager setMayHaveLinkedDevices:YES dbConnection:self.dbConnection]; + ECKeyPair *_Nullable identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair]; OWSAssert(identityKeyPair); NSData *myPublicKey = identityKeyPair.publicKey; diff --git a/Signal/src/ViewControllers/OWSLinkedDevicesTableViewController.m b/Signal/src/ViewControllers/OWSLinkedDevicesTableViewController.m index ebd4490e5..9b5efc150 100644 --- a/Signal/src/ViewControllers/OWSLinkedDevicesTableViewController.m +++ b/Signal/src/ViewControllers/OWSLinkedDevicesTableViewController.m @@ -20,10 +20,10 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSLinkedDevicesTableViewController () -@property YapDatabaseConnection *dbConnection; -@property YapDatabaseViewMappings *deviceMappings; -@property NSTimer *pollingRefreshTimer; -@property BOOL isExpectingMoreDevices; +@property (nonatomic) YapDatabaseConnection *dbConnection; +@property (nonatomic) YapDatabaseViewMappings *deviceMappings; +@property (nonatomic) NSTimer *pollingRefreshTimer; +@property (nonatomic) BOOL isExpectingMoreDevices; @end @@ -131,6 +131,14 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1; __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. + [OWSDevice setMayHaveLinkedDevices:YES + dbConnection:[[TSStorageManager sharedManager] newDatabaseConnection]]; + } + if (devices.count > [OWSDevice numberOfKeysInCollection]) { // Got our new device, we can stop refreshing. wself.isExpectingMoreDevices = NO; @@ -243,7 +251,6 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1; return (NSInteger)[self.deviceMappings numberOfItemsInSection:(NSUInteger)section]; case OWSLinkedDevicesTableViewControllerSectionAddDevice: return 1; - default: DDLogError(@"Unknown section: %ld", (long)section); return 0; diff --git a/SignalServiceKit/src/Contacts/SignalRecipient.h b/SignalServiceKit/src/Contacts/SignalRecipient.h index 39b3bd44f..2151829d9 100644 --- a/SignalServiceKit/src/Contacts/SignalRecipient.h +++ b/SignalServiceKit/src/Contacts/SignalRecipient.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeDevices:(NSSet *)set; @property (nonatomic, nullable) NSString *relay; -@property (nonatomic, retain) NSMutableOrderedSet *devices; +@property (nonatomic) NSMutableOrderedSet *devices; - (BOOL)supportsVoice; // This property indicates support for both WebRTC audio and video calls. diff --git a/SignalServiceKit/src/Contacts/SignalRecipient.m b/SignalServiceKit/src/Contacts/SignalRecipient.m index dc71f3a01..3e08e4558 100644 --- a/SignalServiceKit/src/Contacts/SignalRecipient.m +++ b/SignalServiceKit/src/Contacts/SignalRecipient.m @@ -23,7 +23,23 @@ NS_ASSUME_NONNULL_BEGIN return self; } - _devices = [NSMutableOrderedSet orderedSetWithObject:[NSNumber numberWithInt:1]]; + OWSAssert([TSAccountManager localNumber].length > 0); + if ([[TSAccountManager localNumber] isEqualToString:textSecureIdentifier]) { + // Default to no devices. + // + // This instance represents our own account and is used for sending + // sync message to linked devices. We shouldn't have any linked devices + // yet when we create the "self" SignalRecipient, and we don't need to + // send sync messages to the primary - we ARE the primary. + _devices = [NSMutableOrderedSet new]; + } else { + // Default to sending to just primary device. + // + // OWSMessageSender will correct this if it is wrong the next time + // we send a message to this recipient. + _devices = [NSMutableOrderedSet orderedSetWithObject:@(1)]; + } + _relay = [relay isEqualToString:@""] ? nil : relay; return self; diff --git a/SignalServiceKit/src/Devices/OWSDevice.h b/SignalServiceKit/src/Devices/OWSDevice.h index ce3230a57..eba4af55c 100644 --- a/SignalServiceKit/src/Devices/OWSDevice.h +++ b/SignalServiceKit/src/Devices/OWSDevice.h @@ -9,12 +9,25 @@ NS_ASSUME_NONNULL_BEGIN extern uint32_t const OWSDevicePrimaryDeviceId; +@interface OWSDeviceManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedManager; + +- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection; +- (void)setMayHaveLinkedDevices:(BOOL)value dbConnection:(YapDatabaseConnection *)dbConnection; + +@end + +#pragma mark - + @interface OWSDevice : TSYapDatabaseObject @property (nonatomic, readonly) NSInteger deviceId; -@property (nullable, readonly) NSString *name; -@property (readonly) NSDate *createdAt; -@property (readonly) NSDate *lastSeenAt; +@property (nonatomic, readonly, nullable) NSString *name; +@property (nonatomic, readonly) NSDate *createdAt; +@property (nonatomic, readonly) NSDate *lastSeenAt; + (instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error; diff --git a/SignalServiceKit/src/Devices/OWSDevice.m b/SignalServiceKit/src/Devices/OWSDevice.m index b6a7be014..2cccb3c69 100644 --- a/SignalServiceKit/src/Devices/OWSDevice.m +++ b/SignalServiceKit/src/Devices/OWSDevice.m @@ -5,26 +5,90 @@ #import "OWSDevice.h" #import "NSDate+OWS.h" #import "OWSError.h" +#import "TSStorageManager.h" #import "YapDatabaseConnection.h" #import "YapDatabaseTransaction.h" #import NS_ASSUME_NONNULL_BEGIN -static MTLValueTransformer *_millisecondTimestampToDateTransformer; uint32_t const OWSDevicePrimaryDeviceId = 1; +NSString *const kTSStorageManager_OWSDeviceCollection = @"kTSStorageManager_OWSDeviceCollection"; +NSString *const kTSStorageManager_MayHaveLinkedDevices = @"kTSStorageManager_MayHaveLinkedDevices"; + +@interface OWSDeviceManager () + +@property (atomic, nullable) NSNumber *mayHaveLinkedDevicesCached; + +@end + +#pragma mark - + +@implementation OWSDeviceManager + ++ (instancetype)sharedManager +{ + static OWSDeviceManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] initDefault]; + }); + return instance; +} + +- (instancetype)initDefault +{ + return [super init]; +} + +- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection +{ + OWSAssert(dbConnection); + + @synchronized(self) + { + if (!self.mayHaveLinkedDevicesCached) { + self.mayHaveLinkedDevicesCached = @([dbConnection boolForKey:kTSStorageManager_MayHaveLinkedDevices + inCollection:kTSStorageManager_OWSDeviceCollection + defaultValue:YES]); + } + + return [self.mayHaveLinkedDevicesCached boolValue]; + } +} + +- (void)setMayHaveLinkedDevices:(BOOL)value dbConnection:(YapDatabaseConnection *)dbConnection +{ + OWSAssert(dbConnection); + + @synchronized(self) + { + self.mayHaveLinkedDevicesCached = @(value); + + [dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction setObject:@(value) + forKey:kTSStorageManager_MayHaveLinkedDevices + inCollection:kTSStorageManager_OWSDeviceCollection]; + }]; + } +} + +@end + +#pragma mark - @interface OWSDevice () -@property NSString *name; -@property NSDate *createdAt; -@property NSDate *lastSeenAt; +@property (nonatomic) NSInteger deviceId; +@property (nonatomic, nullable) NSString *name; +@property (nonatomic) NSDate *createdAt; +@property (nonatomic) NSDate *lastSeenAt; @end -@implementation OWSDevice +#pragma mark - -@synthesize name = _name; +@implementation OWSDevice + (instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error { @@ -76,38 +140,39 @@ uint32_t const OWSDevicePrimaryDeviceId = 1; + (MTLValueTransformer *)millisecondTimestampToDateTransformer { - if (!_millisecondTimestampToDateTransformer) { - _millisecondTimestampToDateTransformer = - [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) { - if ([value isKindOfClass:[NSNumber class]]) { - NSNumber *number = (NSNumber *)value; - NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]]; + static MTLValueTransformer *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *number = (NSNumber *)value; + NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]]; + if (result) { + *success = YES; + return result; + } + } + *success = NO; + DDLogError(@"%@ unable to decode date from %@", self.logTag, value); + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Unable to decode date from %@"); + return nil; + } + reverseBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSDate class]]) { + NSDate *date = (NSDate *)value; + NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]]; if (result) { *success = YES; return result; } } + DDLogError(@"%@ unable to encode date from %@", self.logTag, value); + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, @"Unable to encode date"); *success = NO; - DDLogError(@"%@ unable to decode date from %@", self.logTag, value); - *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Unable to decode date from %@"); return nil; - } - reverseBlock:^id(id value, BOOL *success, NSError **error) { - if ([value isKindOfClass:[NSDate class]]) { - NSDate *date = (NSDate *)value; - NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]]; - if (result) { - *success = YES; - return result; - } - } - DDLogError(@"%@ unable to encode date from %@", self.logTag, value); - *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, @"Unable to encode date"); - *success = NO; - return nil; - }]; - } - return _millisecondTimestampToDateTransformer; + }]; + }); + return instance; } + (uint32_t)currentDeviceId diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 7ba8024a5..ad5d71c40 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -954,14 +954,29 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; NSString *localNumber = [TSAccountManager localNumber]; if ([localNumber isEqualToString:recipient.uniqueId]) { - __block BOOL hasSecondaryDevices; - [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - hasSecondaryDevices = [OWSDevice hasSecondaryDevicesWithTransaction:transaction]; - }]; + OWSAssert([message isKindOfClass:[OWSOutgoingSyncMessage class]]); + // Messages send to the "local number" should be sync messages. + // + // We can skip sending sync messages if we know that we have no linked + // devices. However, we need to be sure to handle the case where the + // linked device list has just changed. + // + // The linked device list is reflected in two separate pieces of state: + // + // * OWSDevice's state is updated when you link or unlink a device. + // * SignalRecipient's state is updated by 409 "Mismatched devices" + // responses from the service. + // + // If _both_ of these pieces of state agree that there are no linked + // devices, then can safely skip sending sync message. + // 1. Check OWSDevice's state. + BOOL mayHaveLinkedDevices = [OWSDeviceManager.sharedManager mayHaveLinkedDevices:self.dbConnection]; + + // 2. Check SignalRecipient's state. BOOL hasDeviceMessages = deviceMessages.count > 0; - if (!hasSecondaryDevices && !hasDeviceMessages) { + if (!mayHaveLinkedDevices && !hasDeviceMessages) { DDLogInfo(@"%@ Ignoring sync message without secondary devices: %@", self.logTag, [message class]); OWSAssert([message isKindOfClass:[OWSOutgoingSyncMessage class]]); @@ -972,10 +987,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }); return; - } else if (hasSecondaryDevices) { + } else if (mayHaveLinkedDevices) { // We may have just linked a new secondary device which is not yet reflected in // the SignalRecipient that corresponds to ourself. Proceed. Client should learn - // of new secondary devices when this message send fails. + // of new secondary devices via 409 "Mismatched devices" response. DDLogWarn(@"%@ sync message has no device messages but account has secondary devices.", self.logTag); } else if (hasDeviceMessages) { OWSFail(@"%@ sync message has device messages for unknown secondary devices.", self.logTag); @@ -1095,6 +1110,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; NSArray *extraDevices = [dictionary objectForKey:@"extraDevices"]; NSArray *missingDevices = [dictionary objectForKey:@"missingDevices"]; + if (missingDevices.count > 0) { + NSString *localNumber = [TSAccountManager localNumber]; + if ([localNumber isEqualToString:recipient.uniqueId]) { + [OWSDeviceManager.sharedManager setMayHaveLinkedDevices:YES dbConnection:self.dbConnection]; + } + } + dispatch_async([OWSDispatch sessionStoreQueue], ^{ if (extraDevices.count < 1 && missingDevices.count < 1) { OWSProdFail([OWSAnalyticsEvents messageSenderErrorNoMissingOrExtraDevices]); diff --git a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h index d6aac59cc..79f8e6018 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h +++ b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)hasObjectForKey:(NSString *)key inCollection:(NSString *)collection; - (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection; +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue; - (int)intForKey:(NSString *)key inCollection:(NSString *)collection; - (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection; - (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection; diff --git a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m index 957d0d4c8..0e31b973b 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m +++ b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m @@ -52,8 +52,13 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection { - NSNumber *_Nullable number = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; - return [number boolValue]; + return [self boolForKey:key inCollection:collection defaultValue:NO]; +} + +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue +{ + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return value ? [value boolValue] : defaultValue; } - (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection