Merge branch 'charlesmchen/firstSendVsLinkedDevices'

This commit is contained in:
Matthew Chen 2017-11-10 12:58:17 -05:00
commit 64938b7824
9 changed files with 190 additions and 54 deletions

View file

@ -9,6 +9,7 @@
#import "OWSProfileManager.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/ECKeyPair+OWSPrivateKey.h>
#import <SignalServiceKit/OWSDevice.h>
#import <SignalServiceKit/OWSDeviceProvisioner.h>
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
@ -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;

View file

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

View file

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

View file

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

View file

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

View file

@ -5,26 +5,90 @@
#import "OWSDevice.h"
#import "NSDate+OWS.h"
#import "OWSError.h"
#import "TSStorageManager.h"
#import "YapDatabaseConnection.h"
#import "YapDatabaseTransaction.h"
#import <Mantle/MTLValueTransformer.h>
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 OWSDevice ()
@interface OWSDeviceManager ()
@property NSString *name;
@property NSDate *createdAt;
@property NSDate *lastSeenAt;
@property (atomic, nullable) NSNumber *mayHaveLinkedDevicesCached;
@end
@implementation OWSDevice
#pragma mark -
@synthesize name = _name;
@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 (nonatomic) NSInteger deviceId;
@property (nonatomic, nullable) NSString *name;
@property (nonatomic) NSDate *createdAt;
@property (nonatomic) NSDate *lastSeenAt;
@end
#pragma mark -
@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

View file

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

View file

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

View file

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