Fixup DevicesManager

* By providing a view extension for secondary devices we can use that in
  a view mapping to power our devices view controller, and avoid any race
  conditions with uncommitted transactions.

* Fix crash when you're not in your own contacts

* New device appears on top

* Don't show "edit" button unless there are devices, or rather, the helpers to do so.

* Fix glitchy refresh

  Saving unchanged records was causing the tableview to redraw, which was
  mostly invisible, except that if the refresh indicator were running, it
  would twitch.

// FREEBIE
pull/1/head
Michael Kirk 9 years ago
parent c39e8b0bc6
commit c8a5f50763

@ -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 = "<group>"; };
45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessageTest.m; path = ../../../tests/Messages/Interactions/TSMessageTest.m; sourceTree = "<group>"; };
45D7243E1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDeviceProvisionerTest.m; path = ../../../tests/Devices/OWSDeviceProvisionerTest.m; sourceTree = "<group>"; };
6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SignalRecipientTest.m; path = ../../tests/Contacts/SignalRecipientTest.m; sourceTree = "<group>"; };
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 = "<group>"; };
B6273DD71C13A2E500738558 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
@ -200,6 +202,7 @@
B6273DD21C13A2E500738558 /* Products */,
5183572EFCE99F6F1791272A /* Pods */,
AD6EE8912464E5C2A7FF5BA1 /* Frameworks */,
6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */,
);
sourceTree = "<group>";
};
@ -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;
};

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

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

@ -8,14 +8,41 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSDevice : TSYapDatabaseObject <MTLJSONSerializing>
@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<OWSDevice *> *)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<OWSDevice *> *)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

@ -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<OWSDevice *> *)devices
+ (void)replaceAll:(NSArray<OWSDevice *> *)currentDevices
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeAllObjectsInCollection:[self collection]];
for (OWSDevice *device in devices) {
[device saveWithTransaction:transaction];
NSMutableArray<OWSDevice *> *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<OWSDevice *> *)secondaryDevices
- (BOOL)isPrimaryDevice
{
NSMutableArray<OWSDevice *> *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

@ -100,7 +100,6 @@ NS_ASSUME_NONNULL_BEGIN
return !self.hasSyncedTranscript;
}
- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId
{
TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId];

@ -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:^{

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

@ -10,6 +10,7 @@
#import <YapDatabase/YapDatabaseView.h>
#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;

@ -58,6 +58,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[TSDatabaseView registerThreadDatabaseView];
[TSDatabaseView registerBuddyConversationDatabaseView];
[TSDatabaseView registerUnreadDatabaseView];
[TSDatabaseView registerSecondaryDevicesDatabaseView];
[self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"];
}

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

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

@ -0,0 +1,45 @@
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
#import "SignalRecipient.h"
#import "TSStorageManager+keyingMaterial.h"
#import "TSStorageManager.h"
#import <XCTest/XCTest.h>
@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
Loading…
Cancel
Save