// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "OWSBlockingManager.h" #import "OWSBlockedPhoneNumbersMessage.h" #import "OWSMessageSender.h" #import "TSStorageManager.h" #import "TextSecureKitEnv.h" NS_ASSUME_NONNULL_BEGIN NSString *const kNSNotificationName_BlockedPhoneNumbersDidChange = @"kNSNotificationName_BlockedPhoneNumbersDidChange"; NSString *const kOWSBlockingManager_BlockedPhoneNumbersCollection = @"kOWSBlockingManager_BlockedPhoneNumbersCollection"; // This key is used to persist the current "blocked phone numbers" state. NSString *const kOWSBlockingManager_BlockedPhoneNumbersKey = @"kOWSBlockingManager_BlockedPhoneNumbersKey"; // This key is used to persist the most recently synced "blocked phone numbers" state. NSString *const kOWSBlockingManager_SyncedBlockedPhoneNumbersKey = @"kOWSBlockingManager_SyncedBlockedPhoneNumbersKey"; @interface OWSBlockingManager () @property (nonatomic, readonly) TSStorageManager *storageManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; // We don't store the phone numbers as instances of PhoneNumber to avoid // consistency issues between clients, but these should all be valid e164 // phone numbers. @property (nonatomic, readonly) NSMutableSet *blockedPhoneNumberSet; @end #pragma mark - @implementation OWSBlockingManager + (instancetype)sharedManager { static OWSBlockingManager *sharedMyManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedMyManager = [[self alloc] initDefault]; }); return sharedMyManager; } - (instancetype)initDefault { TSStorageManager *storageManager = [TSStorageManager sharedManager]; OWSMessageSender *messageSender = [TextSecureKitEnv sharedEnv].messageSender; return [self initWithStorageManager:storageManager messageSender:messageSender]; } - (instancetype)initWithStorageManager:(TSStorageManager *)storageManager messageSender:(OWSMessageSender *)messageSender { self = [super init]; if (!self) { return self; } OWSAssert(storageManager); OWSAssert(messageSender); _storageManager = storageManager; _messageSender = messageSender; OWSSingletonAssert(); // Register this manager with the message sender. // This is a circular dependency. [messageSender setBlockingManager:self]; return self; } - (void)addBlockedPhoneNumber:(NSString *)phoneNumber { OWSAssert(phoneNumber.length > 0); DDLogInfo(@"%@ addBlockedPhoneNumber: %@", self.tag, phoneNumber); @synchronized(self) { [self lazyLoadBlockedPhoneNumbersIfNecessary]; if ([_blockedPhoneNumberSet containsObject:phoneNumber]) { // Ignore redundant changes. return; } [_blockedPhoneNumberSet addObject:phoneNumber]; } [self handleUpdate]; } - (void)removeBlockedPhoneNumber:(NSString *)phoneNumber { OWSAssert(phoneNumber.length > 0); DDLogInfo(@"%@ removeBlockedPhoneNumber: %@", self.tag, phoneNumber); @synchronized(self) { [self lazyLoadBlockedPhoneNumbersIfNecessary]; if (![_blockedPhoneNumberSet containsObject:phoneNumber]) { // Ignore redundant changes. return; } [_blockedPhoneNumberSet removeObject:phoneNumber]; } [self handleUpdate]; } - (void)setBlockedPhoneNumbers:(NSArray *)blockedPhoneNumbers sendSyncMessage:(BOOL)sendSyncMessage { OWSAssert(blockedPhoneNumbers != nil); DDLogInfo(@"%@ setBlockedPhoneNumbers: %d", self.tag, (int)blockedPhoneNumbers.count); @synchronized(self) { [self lazyLoadBlockedPhoneNumbersIfNecessary]; NSSet *newSet = [NSSet setWithArray:blockedPhoneNumbers]; if ([_blockedPhoneNumberSet isEqualToSet:newSet]) { return; } _blockedPhoneNumberSet = [newSet mutableCopy]; } [self handleUpdate:sendSyncMessage]; } - (NSArray *)blockedPhoneNumbers { @synchronized(self) { [self lazyLoadBlockedPhoneNumbersIfNecessary]; return [_blockedPhoneNumberSet.allObjects sortedArrayUsingSelector:@selector(compare:)]; } } // This should be called every time the block list changes. - (void)handleUpdate { // By default, always send a sync message when the block list changes. [self handleUpdate:YES]; } - (void)handleUpdate:(BOOL)sendSyncMessage { NSArray *blockedPhoneNumbers = [self blockedPhoneNumbers]; [_storageManager setObject:blockedPhoneNumbers forKey:kOWSBlockingManager_BlockedPhoneNumbersKey inCollection:kOWSBlockingManager_BlockedPhoneNumbersCollection]; dispatch_async(dispatch_get_main_queue(), ^{ if (sendSyncMessage) { [self sendBlockedPhoneNumbersMessage:blockedPhoneNumbers]; } else { // If this update came from an incoming block list sync message, // update the "synced blocked phone numbers" state immediately, // since we're now in sync. // // There could be data loss if both clients modify the block list // at the same time, but: // // a) Block list changes will be rare. // b) Conflicting block list changes will be even rarer. // c) It's unlikely a user will make conflicting changes on two // devices around the same time. // d) There isn't a good way to avoid this. [self saveSyncedBlockedPhoneNumbers:blockedPhoneNumbers]; } [[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_BlockedPhoneNumbersDidChange object:nil userInfo:nil]; }); } // This method should only be called from within a synchronized block. - (void)lazyLoadBlockedPhoneNumbersIfNecessary { if (_blockedPhoneNumberSet) { // _blockedPhoneNumberSet has already been loaded, abort. return; } NSArray *blockedPhoneNumbers = [_storageManager objectForKey:kOWSBlockingManager_BlockedPhoneNumbersKey inCollection:kOWSBlockingManager_BlockedPhoneNumbersCollection]; _blockedPhoneNumberSet = [[NSMutableSet alloc] initWithArray:(blockedPhoneNumbers ?: [NSArray new])]; // If we haven't yet successfully synced the current "blocked phone numbers" changes, // try again to sync now. NSArray *syncedBlockedPhoneNumbers = [_storageManager objectForKey:kOWSBlockingManager_SyncedBlockedPhoneNumbersKey inCollection:kOWSBlockingManager_BlockedPhoneNumbersCollection]; NSSet *syncedBlockedPhoneNumberSet = [[NSSet alloc] initWithArray:(syncedBlockedPhoneNumbers ?: [NSArray new])]; if (![_blockedPhoneNumberSet isEqualToSet:syncedBlockedPhoneNumberSet]) { DDLogInfo(@"%@ retrying sync of blocked phone numbers", self.tag); dispatch_async(dispatch_get_main_queue(), ^{ [self sendBlockedPhoneNumbersMessage:blockedPhoneNumbers]; }); } } - (void)sendBlockedPhoneNumbersMessage:(NSArray *)blockedPhoneNumbers { OWSAssert(blockedPhoneNumbers); OWSBlockedPhoneNumbersMessage *message = [[OWSBlockedPhoneNumbersMessage alloc] initWithPhoneNumbers:blockedPhoneNumbers]; [self.messageSender sendMessage:message success:^{ DDLogInfo(@"%@ Successfully sent blocked phone numbers sync message", self.tag); // Record the last set of "blocked phone numbers" which we successfully synced. [self saveSyncedBlockedPhoneNumbers:blockedPhoneNumbers]; } failure:^(NSError *error) { DDLogError(@"%@ Failed to send blocked phone numbers sync message with error: %@", self.tag, error); // TODO: We might want to retry more often than just app launch. }]; } - (void)saveSyncedBlockedPhoneNumbers:(NSArray *)blockedPhoneNumbers { OWSAssert(blockedPhoneNumbers); // Record the last set of "blocked phone numbers" which we successfully synced. [_storageManager setObject:blockedPhoneNumbers forKey:kOWSBlockingManager_SyncedBlockedPhoneNumbersKey inCollection:kOWSBlockingManager_BlockedPhoneNumbersCollection]; } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end NS_ASSUME_NONNULL_END