diff --git a/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj b/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj index 586b66951..271be3c8d 100644 --- a/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj +++ b/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 45C6A09A1D2F029B007D8AC0 /* TSMessageTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */; }; 45D7243F1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45D7243E1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m */; }; 51520592F83F2440F2DE4D67 /* libPods-TSKitiOSTestApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B8362AB8E280E0F64352F08A /* libPods-TSKitiOSTestApp.a */; }; + 6323E339D5B8F4CB77EB3ED4 /* SignalRecipientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */; }; B6273DD61C13A2E500738558 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = B6273DD51C13A2E500738558 /* main.m */; }; B6273DD91C13A2E500738558 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = B6273DD81C13A2E500738558 /* AppDelegate.m */; }; B6273DDC1C13A2E500738558 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B6273DDB1C13A2E500738558 /* ViewController.m */; }; @@ -60,6 +61,7 @@ 45A856AB1D220BFF0056CD4D /* TSAttributesTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttributesTest.m; sourceTree = ""; }; 45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessageTest.m; path = ../../../tests/Messages/Interactions/TSMessageTest.m; sourceTree = ""; }; 45D7243E1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDeviceProvisionerTest.m; path = ../../../tests/Devices/OWSDeviceProvisionerTest.m; sourceTree = ""; }; + 6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SignalRecipientTest.m; path = ../../tests/Contacts/SignalRecipientTest.m; sourceTree = ""; }; B6273DD11C13A2E500738558 /* TSKitiOSTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TSKitiOSTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; B6273DD51C13A2E500738558 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; B6273DD71C13A2E500738558 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -200,6 +202,7 @@ B6273DD21C13A2E500738558 /* Products */, 5183572EFCE99F6F1791272A /* Pods */, AD6EE8912464E5C2A7FF5BA1 /* Frameworks */, + 6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */, ); sourceTree = ""; }; @@ -475,6 +478,7 @@ 45458B771CC342B600A02153 /* TSMessageStorageTests.m in Sources */, 45458B7C1CC342B600A02153 /* MessagePaddingTests.m in Sources */, 45A856AC1D220BFF0056CD4D /* TSAttributesTest.m in Sources */, + 6323E339D5B8F4CB77EB3ED4 /* SignalRecipientTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/Contacts/SignalRecipient.h b/src/Contacts/SignalRecipient.h index 9f8e5b28f..3da562447 100644 --- a/src/Contacts/SignalRecipient.h +++ b/src/Contacts/SignalRecipient.h @@ -11,9 +11,10 @@ NS_ASSUME_NONNULL_BEGIN relay:(nullable NSString *)relay supportsVoice:(BOOL)voiceCapable; -+ (instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier; -+ (instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier - withTransaction:(YapDatabaseReadTransaction *)transaction; ++ (instancetype)selfRecipient; ++ (nullable instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier; ++ (nullable instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier + withTransaction:(YapDatabaseReadTransaction *)transaction; - (void)addDevices:(NSSet *)set; diff --git a/src/Contacts/SignalRecipient.m b/src/Contacts/SignalRecipient.m index f78e2ac6a..3a8ea35d3 100644 --- a/src/Contacts/SignalRecipient.m +++ b/src/Contacts/SignalRecipient.m @@ -2,6 +2,7 @@ // Copyright (c) 2014 Open Whisper Systems. All rights reserved. #import "SignalRecipient.h" +#import "TSStorageHeaders.h" #import "TSStorageManager+IdentityKeyStore.h" NS_ASSUME_NONNULL_BEGIN @@ -28,12 +29,13 @@ NS_ASSUME_NONNULL_BEGIN return self; } -+ (instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier - withTransaction:(YapDatabaseReadTransaction *)transaction { ++ (nullable instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier + withTransaction:(YapDatabaseReadTransaction *)transaction +{ return [self fetchObjectWithUniqueID:textSecureIdentifier transaction:transaction]; } -+ (instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier ++ (nullable instancetype)recipientWithTextSecureIdentifier:(NSString *)textSecureIdentifier { __block SignalRecipient *recipient; [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { @@ -42,6 +44,15 @@ NS_ASSUME_NONNULL_BEGIN return recipient; } ++ (instancetype)selfRecipient +{ + SignalRecipient *myself = [self recipientWithTextSecureIdentifier:[TSStorageManager localNumber]]; + if (!myself) { + myself = [[self alloc] initWithTextSecureIdentifier:[TSStorageManager localNumber] relay:nil supportsVoice:YES]; + } + return myself; +} + - (NSMutableOrderedSet *)devices { return [_devices copy]; } diff --git a/src/Devices/OWSDevice.h b/src/Devices/OWSDevice.h index 6c3566bbf..774faa521 100644 --- a/src/Devices/OWSDevice.h +++ b/src/Devices/OWSDevice.h @@ -8,14 +8,41 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSDevice : TSYapDatabaseObject @property (nonatomic, readonly) NSInteger deviceId; -@property (nullable, nonatomic, readonly) NSString *name; -@property (nonatomic, readonly) NSDate *createdAt; -@property (nonatomic, readonly) NSDate *lastSeenAt; +@property (nullable, readonly) NSString *name; +@property (readonly) NSDate *createdAt; +@property (readonly) NSDate *lastSeenAt; + (instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error; -+ (NSArray *)secondaryDevices; + +/** + * Set local database of devices to `devices`. + * + * This will create missing devices, update existing devices, and delete stale devices. + * @param devices + */ + (void)replaceAll:(NSArray *)devices; +/** + * + * @param transaction + * @return + * If the user has any linked devices (apart from the device this app is running on). + */ ++ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction; + +- (NSString *)displayName; +- (BOOL)isPrimaryDevice; + +/** + * Assign attributes to this device from another. + * + * @param other + * OWSDevice whose attributes to copy to this device + * @return + * YES if any values on self changed, else NO + */ +- (BOOL)updateAttributesWithDevice:(OWSDevice *)other; + @end NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSDevice.m b/src/Devices/OWSDevice.m index ca7741750..a8f710ff1 100644 --- a/src/Devices/OWSDevice.m +++ b/src/Devices/OWSDevice.m @@ -12,6 +12,14 @@ NS_ASSUME_NONNULL_BEGIN static MTLValueTransformer *_millisecondTimestampToDateTransformer; static int const OWSDevicePrimaryDeviceId = 1; +@interface OWSDevice () + +@property NSString *name; +@property NSDate *createdAt; +@property NSDate *lastSeenAt; + +@end + @implementation OWSDevice @synthesize name = _name; @@ -41,14 +49,27 @@ static int const OWSDevicePrimaryDeviceId = 1; return self.millisecondTimestampToDateTransformer; } -+ (void)replaceAll:(NSArray *)devices ++ (void)replaceAll:(NSArray *)currentDevices { - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [transaction removeAllObjectsInCollection:[self collection]]; - for (OWSDevice *device in devices) { - [device saveWithTransaction:transaction]; + NSMutableArray *existingDevices = [[self allObjectsInCollection] mutableCopy]; + for (OWSDevice *currentDevice in currentDevices) { + NSUInteger existingDeviceIndex = [existingDevices indexOfObject:currentDevice]; + if (existingDeviceIndex == NSNotFound) { + // New Device + [currentDevice save]; + } else { + OWSDevice *existingDevice = existingDevices[existingDeviceIndex]; + if ([existingDevice updateAttributesWithDevice:currentDevice]) { + [existingDevice save]; + } + [existingDevices removeObjectAtIndex:existingDeviceIndex]; } - }]; + } + + // Since we removed existing devices as we went, only stale devices remain + for (OWSDevice *staleDevice in existingDevices) { + [staleDevice remove]; + } } + (MTLValueTransformer *)millisecondTimestampToDateTransformer @@ -87,26 +108,15 @@ static int const OWSDevicePrimaryDeviceId = 1; return _millisecondTimestampToDateTransformer; } -+ (NSArray *)secondaryDevices +- (BOOL)isPrimaryDevice { - NSMutableArray *devices = [NSMutableArray new]; - - [self enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) { - if ([obj isKindOfClass:[OWSDevice class]]) { - OWSDevice *device = (OWSDevice *)obj; - if (device.deviceId != OWSDevicePrimaryDeviceId) { - [devices addObject:device]; - } - } - }]; - - return [devices copy]; + return self.deviceId == OWSDevicePrimaryDeviceId; } -- (nullable NSString *)name +- (NSString *)displayName { - if (_name) { - return _name; + if (self.name) { + return self.name; } if (self.deviceId == OWSDevicePrimaryDeviceId) { @@ -115,6 +125,50 @@ static int const OWSDevicePrimaryDeviceId = 1; return NSLocalizedString(@"UNNAMED_DEVICE", @"Label text in device manager for a device with no name"); } +- (BOOL)updateAttributesWithDevice:(OWSDevice *)other +{ + BOOL changed = NO; + if (![self.lastSeenAt isEqual:other.lastSeenAt]) { + self.lastSeenAt = other.lastSeenAt; + changed = YES; + } + + if (![self.createdAt isEqual:other.createdAt]) { + self.createdAt = other.createdAt; + changed = YES; + } + + if (![self.name isEqual:other.name]) { + self.name = other.name; + changed = YES; + } + + return changed; +} + ++ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self numberOfKeysInCollectionWithTransaction:transaction] > 1; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[OWSDevice class]]) { + return NO; + } + + return [self isEqualToDevice:(OWSDevice *)object]; +} + +- (BOOL)isEqualToDevice:(OWSDevice *)device +{ + return self.deviceId == device.deviceId; +} + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/Interactions/TSOutgoingMessage.m b/src/Messages/Interactions/TSOutgoingMessage.m index a9d34bcbc..04d3cce1a 100644 --- a/src/Messages/Interactions/TSOutgoingMessage.m +++ b/src/Messages/Interactions/TSOutgoingMessage.m @@ -100,7 +100,6 @@ NS_ASSUME_NONNULL_BEGIN return !self.hasSyncedTranscript; } - - (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId { TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId]; diff --git a/src/Messages/TSMessagesManager+sendMessages.m b/src/Messages/TSMessagesManager+sendMessages.m index 5d9abc310..2a6691f08 100644 --- a/src/Messages/TSMessagesManager+sendMessages.m +++ b/src/Messages/TSMessagesManager+sendMessages.m @@ -372,9 +372,8 @@ dispatch_queue_t sendingQueue() { OWSOutgoingSentMessageTranscript *sentMessageTranscript = [[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message]; - SignalRecipient *localUser = [SignalRecipient recipientWithTextSecureIdentifier:[TSStorageManager localNumber]]; [self sendMessage:sentMessageTranscript - toRecipient:localUser + toRecipient:[SignalRecipient selfRecipient] inThread:message.thread withAttemps:RETRY_ATTEMPTS success:^{ diff --git a/src/Storage/TSDatabaseView.h b/src/Storage/TSDatabaseView.h index 73871c4d3..584483ded 100644 --- a/src/Storage/TSDatabaseView.h +++ b/src/Storage/TSDatabaseView.h @@ -14,14 +14,16 @@ extern NSString *TSInboxGroup; extern NSString *TSArchiveGroup; extern NSString *TSUnreadIncomingMessagesGroup; +extern NSString *TSSecondaryDevicesGroup; extern NSString *TSThreadDatabaseViewExtensionName; extern NSString *TSMessageDatabaseViewExtensionName; extern NSString *TSUnreadDatabaseViewExtensionName; +extern NSString *TSSecondaryDevicesDatabaseViewExtensionName; + (BOOL)registerThreadDatabaseView; + (BOOL)registerBuddyConversationDatabaseView; + (BOOL)registerUnreadDatabaseView; - ++ (BOOL)registerSecondaryDevicesDatabaseView; @end diff --git a/src/Storage/TSDatabaseView.m b/src/Storage/TSDatabaseView.m index affebb5c7..40b533259 100644 --- a/src/Storage/TSDatabaseView.m +++ b/src/Storage/TSDatabaseView.m @@ -10,6 +10,7 @@ #import +#import "OWSDevice.h" #import "TSIncomingMessage.h" #import "TSStorageManager.h" #import "TSThread.h" @@ -18,10 +19,12 @@ NSString *TSInboxGroup = @"TSInboxGroup"; NSString *TSArchiveGroup = @"TSArchiveGroup"; NSString *TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup"; +NSString *TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup"; NSString *TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName"; NSString *TSMessageDatabaseViewExtensionName = @"TSMessageDatabaseViewExtensionName"; NSString *TSUnreadDatabaseViewExtensionName = @"TSUnreadDatabaseViewExtensionName"; +NSString *TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesDatabaseViewExtensionName"; @implementation TSDatabaseView @@ -206,6 +209,61 @@ NSString *TSUnreadDatabaseViewExtensionName = @"TSUnreadDatabaseViewExtensionNa }]; } ++ (BOOL)registerSecondaryDevicesDatabaseView +{ + YapDatabaseView *existingView = + [[TSStorageManager sharedManager].database registeredExtension:TSSecondaryDevicesDatabaseViewExtensionName]; + if (existingView) { + return YES; + } + + YapDatabaseViewGrouping *viewGrouping = + [YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(YapDatabaseReadTransaction *_Nonnull transaction, + NSString *_Nonnull collection, + NSString *_Nonnull key, + id _Nonnull object) { + if ([object isKindOfClass:[OWSDevice class]]) { + OWSDevice *device = (OWSDevice *)object; + if (![device isPrimaryDevice]) { + return TSSecondaryDevicesGroup; + } + } + return nil; + }]; + + YapDatabaseViewSorting *viewSorting = + [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *_Nonnull transaction, + NSString *_Nonnull group, + NSString *_Nonnull collection1, + NSString *_Nonnull key1, + id _Nonnull object1, + NSString *_Nonnull collection2, + NSString *_Nonnull key2, + id _Nonnull object2) { + + if ([object1 isKindOfClass:[OWSDevice class]] && [object2 isKindOfClass:[OWSDevice class]]) { + OWSDevice *device1 = (OWSDevice *)object1; + OWSDevice *device2 = (OWSDevice *)object2; + + return [device2.createdAt compare:device1.createdAt]; + } + + return NSOrderedSame; + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.isPersistent = YES; + + NSSet *deviceCollection = [NSSet setWithObject:[OWSDevice collection]]; + options.allowedCollections = [[YapWhitelistBlacklist alloc] initWithWhitelist:deviceCollection]; + + YapDatabaseView *view = + [[YapDatabaseView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"3" options:options]; + + return [[TSStorageManager sharedManager].database registerExtension:view + withName:TSSecondaryDevicesDatabaseViewExtensionName]; +} + + (NSDate *)localTimeReceiveDateForInteraction:(TSInteraction *)interaction { NSDate *interactionDate = interaction.date; diff --git a/src/Storage/TSStorageManager.m b/src/Storage/TSStorageManager.m index ec2d5b017..873c96bf1 100644 --- a/src/Storage/TSStorageManager.m +++ b/src/Storage/TSStorageManager.m @@ -58,6 +58,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; [TSDatabaseView registerThreadDatabaseView]; [TSDatabaseView registerBuddyConversationDatabaseView]; [TSDatabaseView registerUnreadDatabaseView]; + [TSDatabaseView registerSecondaryDevicesDatabaseView]; [self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"]; } diff --git a/src/Storage/TSYapDatabaseObject.h b/src/Storage/TSYapDatabaseObject.h index f4315a50e..aba5bfbe1 100644 --- a/src/Storage/TSYapDatabaseObject.h +++ b/src/Storage/TSYapDatabaseObject.h @@ -33,6 +33,7 @@ * @return The number of keys in the classes collection. */ + (NSUInteger)numberOfKeysInCollection; ++ (NSUInteger)numberOfKeysInCollectionWithTransaction:(YapDatabaseReadTransaction *)transaction; /** * Removes all objects in the classes collection. diff --git a/src/Storage/TSYapDatabaseObject.m b/src/Storage/TSYapDatabaseObject.m index 3c17079ae..36f64a06e 100644 --- a/src/Storage/TSYapDatabaseObject.m +++ b/src/Storage/TSYapDatabaseObject.m @@ -69,11 +69,16 @@ { __block NSUInteger count; [[self dbConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) { - count = [transaction numberOfKeysInCollection:[self collection]]; + count = [self numberOfKeysInCollectionWithTransaction:transaction]; }]; return count; } ++ (NSUInteger)numberOfKeysInCollectionWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [transaction numberOfKeysInCollection:[self collection]]; +} + + (NSArray *)allObjectsInCollection { __block NSMutableArray *all = [[NSMutableArray alloc] initWithCapacity:[self numberOfKeysInCollection]]; diff --git a/tests/Contacts/SignalRecipientTest.m b/tests/Contacts/SignalRecipientTest.m new file mode 100644 index 000000000..32c96470e --- /dev/null +++ b/tests/Contacts/SignalRecipientTest.m @@ -0,0 +1,45 @@ +// Copyright (c) 2016 Open Whisper Systems. All rights reserved. + +#import "SignalRecipient.h" +#import "TSStorageManager+keyingMaterial.h" +#import "TSStorageManager.h" +#import + +@interface SignalRecipientTest : XCTestCase + +@end + +@implementation SignalRecipientTest + +- (void)setUp { + [super setUp]; + [TSStorageManager storePhoneNumber:@"+13231231234"]; +} + +- (void)testSelfRecipientWithExistingRecord +{ + // Sanity Check + NSString *localNumber = @"+13231231234"; + XCTAssertNotNil(localNumber); + [[[SignalRecipient alloc] initWithTextSecureIdentifier:localNumber relay:nil supportsVoice:YES] save]; + XCTAssertNotNil([SignalRecipient recipientWithTextSecureIdentifier:localNumber]); + + SignalRecipient *me = [SignalRecipient selfRecipient]; + XCTAssert(me); + XCTAssertEqualObjects(localNumber, me.uniqueId); +} + +- (void)testSelfRecipientWithoutExistingRecord +{ + NSString *localNumber = @"+13231231234"; + XCTAssertNotNil(localNumber); + [[SignalRecipient fetchObjectWithUniqueID:localNumber] remove]; + // Sanity Check that there's no existing user. + XCTAssertNil([SignalRecipient recipientWithTextSecureIdentifier:localNumber]); + + SignalRecipient *me = [SignalRecipient selfRecipient]; + XCTAssert(me); + XCTAssertEqualObjects(localNumber, me.uniqueId); +} + +@end