diff --git a/SignalMessaging/contacts/OWSContactsManager.m b/SignalMessaging/contacts/OWSContactsManager.m index 8c0ac14a7..da1d9864b 100644 --- a/SignalMessaging/contacts/OWSContactsManager.m +++ b/SignalMessaging/contacts/OWSContactsManager.m @@ -22,9 +22,16 @@ @import Contacts; +NS_ASSUME_NONNULL_BEGIN + NSString *const OWSContactsManagerSignalAccountsDidChangeNotification = @"OWSContactsManagerSignalAccountsDidChangeNotification"; +NSString *const OWSContactsManagerCollection = @"OWSContactsManagerCollection"; +NSString *const OWSContactsManagerKeyLastKnownContactPhoneNumbers + = @"OWSContactsManagerKeyLastKnownContactPhoneNumbers"; +NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsManagerKeyNextFullIntersectionDate2"; + @interface OWSContactsManager () @property (nonatomic) BOOL isContactsUpdateInFlight; @@ -42,6 +49,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification @end +#pragma mark - + @implementation OWSContactsManager - (id)init @@ -216,7 +225,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification } else { shouldClearStaleCache = YES; } - [self updateWithContacts:contacts shouldClearStaleCache:shouldClearStaleCache]; + [self updateWithContacts:contacts isUserRequested:isUserRequested shouldClearStaleCache:shouldClearStaleCache]; } - (void)systemContactsFetcher:(SystemContactsFetcher *)systemContactsFetcher @@ -225,29 +234,142 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification if (authorizationStatus == ContactStoreAuthorizationStatusRestricted || authorizationStatus == ContactStoreAuthorizationStatusDenied) { // Clear the contacts cache if access to the system contacts is revoked. - [self updateWithContacts:@[] shouldClearStaleCache:YES]; + [self updateWithContacts:@[] isUserRequested:NO shouldClearStaleCache:YES]; } } #pragma mark - Intersection -- (void)intersectContactsWithCompletion:(void (^)(NSError *_Nullable error))completionBlock +- (NSSet *)recipientIdsForIntersectionWithContacts:(NSArray *)contacts { - [self intersectContactsWithRetryDelay:1 completion:completionBlock]; + OWSAssert(contacts); + + NSMutableSet *recipientIds = [NSMutableSet set]; + + for (Contact *contact in contacts) { + for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { + [recipientIds addObject:phoneNumber.toE164]; + } + } + + return recipientIds; } -- (void)intersectContactsWithRetryDelay:(double)retryDelaySeconds - completion:(void (^)(NSError *_Nullable error))completionBlock +- (void)intersectContacts:(NSArray *)contacts + isUserRequested:(BOOL)isUserRequested + completion:(void (^)(NSError *_Nullable error))completion { - void (^success)(void) = ^{ + OWSAssert(contacts); + OWSAssert(completion); + + dispatch_async(self.serialQueue, ^{ + __block BOOL isFullIntersection = YES; + __block NSSet *allContactRecipientIds; + __block NSSet *recipientIdsForIntersection; + [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + // Contact updates initiated by the user should always do a full intersection. + if (!isUserRequested) { + NSDate *_Nullable nextFullIntersectionDate = + [transaction dateForKey:OWSContactsManagerKeyNextFullIntersectionDate + inCollection:OWSContactsManagerCollection]; + if (nextFullIntersectionDate && [nextFullIntersectionDate isAfterNow]) { + isFullIntersection = NO; + } + } + + allContactRecipientIds = [self recipientIdsForIntersectionWithContacts:contacts]; + recipientIdsForIntersection = allContactRecipientIds; + + if (!isFullIntersection) { + // Do a "delta" intersection instead of a "full" intersection: + // only intersect new contacts which were not in the last successful + // "full" intersection. + NSSet *_Nullable lastKnownContactPhoneNumbers = + [transaction objectForKey:OWSContactsManagerKeyLastKnownContactPhoneNumbers + inCollection:OWSContactsManagerCollection]; + if (lastKnownContactPhoneNumbers) { + // Do a "delta" sync which only intersects recipient ids not included + // in the last full intersection. + NSMutableSet *newRecipientIds = [allContactRecipientIds mutableCopy]; + [newRecipientIds minusSet:lastKnownContactPhoneNumbers]; + recipientIdsForIntersection = newRecipientIds; + } else { + // Without a list of "last known" contact phone numbers, we'll have to do a full intersection. + isFullIntersection = YES; + } + } + }]; + OWSAssert(recipientIdsForIntersection); + + if (recipientIdsForIntersection.count < 1) { + DDLogInfo(@"%@ Skipping intersection; no contacts to intersect.", self.logTag); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completion(nil); + }); + return; + } else if (isFullIntersection) { + DDLogInfo(@"%@ Doing full intersection with %zu contacts.", self.logTag, recipientIdsForIntersection.count); + } else { + DDLogInfo( + @"%@ Doing delta intersection with %zu contacts.", self.logTag, recipientIdsForIntersection.count); + } + + [self intersectContacts:recipientIdsForIntersection + retryDelaySeconds:1.0 + success:^(NSSet *registeredRecipients) { + [self markIntersectionAsComplete:allContactRecipientIds isFullIntersection:isFullIntersection]; + + completion(nil); + } + failure:^(NSError *error) { + completion(error); + }]; + }); +} + +- (void)markIntersectionAsComplete:(NSSet *)recipientIdsForIntersection + isFullIntersection:(BOOL)isFullIntersection +{ + OWSAssert(recipientIdsForIntersection.count > 0); + + dispatch_async(self.serialQueue, ^{ + [self.dbReadConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:recipientIdsForIntersection + forKey:OWSContactsManagerKeyLastKnownContactPhoneNumbers + inCollection:OWSContactsManagerCollection]; + + if (isFullIntersection) { + // Don't do a full intersection more often than once every 6 hours. + const NSTimeInterval kMinFullIntersectionInterval = 6 * kHourInterval; + NSDate *nextFullIntersectionDate = [NSDate + dateWithTimeIntervalSince1970:[NSDate new].timeIntervalSince1970 + kMinFullIntersectionInterval]; + [transaction setDate:nextFullIntersectionDate + forKey:OWSContactsManagerKeyNextFullIntersectionDate + inCollection:OWSContactsManagerCollection]; + } + }]; + }); +} + +- (void)intersectContacts:(NSSet *)recipientIds + retryDelaySeconds:(double)retryDelaySeconds + success:(void (^)(NSSet *))successParameter + failure:(void (^)(NSError *))failureParameter +{ + OWSAssert(recipientIds.count > 0); + OWSAssert(retryDelaySeconds > 0); + OWSAssert(successParameter); + OWSAssert(failureParameter); + + void (^success)(NSArray *) = ^(NSArray *registeredRecipientIds) { DDLogInfo(@"%@ Successfully intersected contacts.", self.logTag); - completionBlock(nil); + successParameter([NSSet setWithArray:registeredRecipientIds]); }; - void (^failure)(NSError *error) = ^(NSError *error) { + void (^failure)(NSError *) = ^(NSError *error) { if ([error.domain isEqualToString:OWSSignalServiceKitErrorDomain] && error.code == OWSErrorCodeContactsUpdaterRateLimit) { DDLogError(@"Contact intersection hit rate limit with error: %@", error); - completionBlock(error); + failureParameter(error); return; } @@ -258,12 +380,13 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification // TODO: Abort if another contact intersection succeeds in the meantime. dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryDelaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self intersectContactsWithRetryDelay:retryDelaySeconds * 2 completion:completionBlock]; + [self intersectContacts:recipientIds + retryDelaySeconds:retryDelaySeconds * 2.0 + success:successParameter + failure:failureParameter]; }); }; - [[ContactsUpdater sharedUpdater] updateSignalContactIntersectionWithABContacts:self.allContacts - success:success - failure:failure]; + [[ContactsUpdater sharedUpdater] lookupIdentifiers:recipientIds.allObjects success:success failure:failure]; } - (void)startObserving @@ -284,7 +407,9 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification [self.avatarCache removeAllImagesForKey:recipientId]; } -- (void)updateWithContacts:(NSArray *)contacts shouldClearStaleCache:(BOOL)shouldClearStaleCache +- (void)updateWithContacts:(NSArray *)contacts + isUserRequested:(BOOL)isUserRequested + shouldClearStaleCache:(BOOL)shouldClearStaleCache { dispatch_async(self.serialQueue, ^{ NSMutableDictionary *allContactsMap = [NSMutableDictionary new]; @@ -305,9 +430,12 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification [self.avatarCache removeAllImages]; - [self intersectContactsWithCompletion:^(NSError *_Nullable error) { - [self buildSignalAccountsAndClearStaleCache:shouldClearStaleCache]; - }]; + [self intersectContacts:contacts + isUserRequested:isUserRequested + completion:^(NSError *_Nullable error) { + // TODO: Should we do this on error? + [self buildSignalAccountsAndClearStaleCache:shouldClearStaleCache]; + }]; }); }); } @@ -927,4 +1055,6 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification return name; } +NS_ASSUME_NONNULL_END + @end diff --git a/SignalServiceKit/src/Contacts/ContactsUpdater.h b/SignalServiceKit/src/Contacts/ContactsUpdater.h index a686db178..714f9c04b 100644 --- a/SignalServiceKit/src/Contacts/ContactsUpdater.h +++ b/SignalServiceKit/src/Contacts/ContactsUpdater.h @@ -6,8 +6,6 @@ NS_ASSUME_NONNULL_BEGIN -@class Contact; - @interface ContactsUpdater : NSObject + (instancetype)sharedUpdater; @@ -31,10 +29,6 @@ NS_ASSUME_NONNULL_BEGIN success:(void (^)(NSArray *recipients))success failure:(void (^)(NSError *error))failure; -- (void)updateSignalContactIntersectionWithABContacts:(NSArray *)abContacts - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure; - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Contacts/ContactsUpdater.m b/SignalServiceKit/src/Contacts/ContactsUpdater.m index cdd883651..dfd0d69c0 100644 --- a/SignalServiceKit/src/Contacts/ContactsUpdater.m +++ b/SignalServiceKit/src/Contacts/ContactsUpdater.m @@ -3,7 +3,6 @@ // #import "ContactsUpdater.h" -#import "Contact.h" #import "Cryptography.h" #import "OWSError.h" #import "OWSPrimaryStorage.h" @@ -79,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN [self contactIntersectionWithSet:[NSSet setWithArray:identifiers] success:^(NSSet *recipients) { if (recipients.count > 0) { - success([recipients copy]); + success(recipients.allObjects); } else { failure(OWSErrorMakeNoSuchSignalRecipientError()); } @@ -87,36 +86,6 @@ NS_ASSUME_NONNULL_BEGIN failure:failure]; } -// TODO: Modify this to support delta lookups. -- (void)updateSignalContactIntersectionWithABContacts:(NSArray *)abContacts - success:(void (^)(void))success - failure:(void (^)(NSError *error))failure -{ - NSMutableSet *abPhoneNumbers = [NSMutableSet set]; - - for (Contact *contact in abContacts) { - for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { - [abPhoneNumbers addObject:phoneNumber.toE164]; - } - } - - NSMutableSet *recipientIds = [NSMutableSet set]; - [OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction * transaction) { - // TODO: Don't do this. - NSArray *allRecipientKeys = [transaction allKeysInCollection:[SignalRecipient collection]]; - [recipientIds addObjectsFromArray:allRecipientKeys]; - }]; - - NSMutableSet *allContacts = [[abPhoneNumbers setByAddingObjectsFromSet:recipientIds] mutableCopy]; - - [self contactIntersectionWithSet:allContacts - success:^(NSSet *recipients) { - DDLogInfo(@"%@ successfully intersected contacts.", self.logTag); - success(); - } - failure:failure]; -} - - (void)contactIntersectionWithSet:(NSSet *)recipientIdsToLookup success:(void (^)(NSSet *recipients))success failure:(void (^)(NSError *error))failure { diff --git a/SignalServiceKit/src/Contacts/SignalRecipient.m b/SignalServiceKit/src/Contacts/SignalRecipient.m index 2790e59ab..db163a015 100644 --- a/SignalServiceKit/src/Contacts/SignalRecipient.m +++ b/SignalServiceKit/src/Contacts/SignalRecipient.m @@ -159,7 +159,7 @@ NS_ASSUME_NONNULL_BEGIN SignalRecipient *latest = [SignalRecipient markRecipientAsRegisteredAndGet:self.recipientId transaction:transaction]; - if (![devices isSubsetOfSet:latest.devices.set]) { + if (![devices intersectsSet:latest.devices.set]) { return; } DDLogDebug(@"%@ removing devices: %@, from recipient: %@", self.logTag, devices, latest.recipientId); diff --git a/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.h b/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.h index db9581683..3a98dad0f 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.h +++ b/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.h @@ -36,6 +36,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)restoreSnapshotOfCollection:(NSString *)collection snapshotFilePath:(NSString *)snapshotFilePath; #endif +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.m b/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.m index 63fdeeaaa..e3e638e61 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.m +++ b/SignalServiceKit/src/Storage/YapDatabaseTransaction+OWS.m @@ -149,6 +149,11 @@ NS_ASSUME_NONNULL_BEGIN } #endif +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + [self setObject:@(value.timeIntervalSince1970) forKey:key inCollection:collection]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/NSDate+OWS.h b/SignalServiceKit/src/Util/NSDate+OWS.h index 024924a9b..10167abad 100755 --- a/SignalServiceKit/src/Util/NSDate+OWS.h +++ b/SignalServiceKit/src/Util/NSDate+OWS.h @@ -34,6 +34,9 @@ extern const NSTimeInterval kYearInterval; - (BOOL)isAfterDate:(NSDate *)otherDate; - (BOOL)isBeforeDate:(NSDate *)otherDate; +- (BOOL)isAfterNow; +- (BOOL)isBeforeNow; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/NSDate+OWS.mm b/SignalServiceKit/src/Util/NSDate+OWS.mm index f6238a7e0..2e81a44e0 100644 --- a/SignalServiceKit/src/Util/NSDate+OWS.mm +++ b/SignalServiceKit/src/Util/NSDate+OWS.mm @@ -45,6 +45,16 @@ const NSTimeInterval kYearInterval = 365 * kDayInterval; return [self compare:otherDate] == NSOrderedAscending; } +- (BOOL)isAfterNow +{ + return [self isAfterDate:[NSDate new]]; +} + +- (BOOL)isBeforeNow +{ + return [self isBeforeDate:[NSDate new]]; +} + @end NS_ASSUME_NONNULL_END