NCContactsManager.m 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. #import "NCContactsManager.h"
  6. #import <Contacts/Contacts.h>
  7. #import "NCAPIController.h"
  8. #import "NCDatabaseManager.h"
  9. #import "NCSettingsController.h"
  10. #import "ABContact.h"
  11. #import "NCContact.h"
  12. #import "NextcloudTalk-Swift.h"
  13. @interface NCContactsManager ()
  14. @property (nonatomic, strong) CNContactStore *contactStore;
  15. @end
  16. @implementation NCContactsManager
  17. NSString * const NCContactsManagerContactsUpdatedNotification = @"NCContactsManagerContactsUpdatedNotification";
  18. NSString * const NCContactsManagerContactsAccessUpdatedNotification = @"NCContactsManagerContactsAccessUpdatedNotification";
  19. + (NCContactsManager *)sharedInstance
  20. {
  21. static dispatch_once_t once;
  22. static NCContactsManager *sharedInstance;
  23. dispatch_once(&once, ^{
  24. sharedInstance = [[self alloc] init];
  25. });
  26. return sharedInstance;
  27. }
  28. - (id)init
  29. {
  30. self = [super init];
  31. if (self) {
  32. _contactStore = [[CNContactStore alloc] init];
  33. }
  34. return self;
  35. }
  36. - (void)requestContactsAccess:(void (^)(BOOL granted))completionHandler
  37. {
  38. [_contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  39. completionHandler(granted);
  40. [[NSNotificationCenter defaultCenter] postNotificationName:NCContactsManagerContactsAccessUpdatedNotification
  41. object:self
  42. userInfo:nil];
  43. }];
  44. }
  45. - (BOOL)isContactAccessDetermined
  46. {
  47. return [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusNotDetermined;
  48. }
  49. - (BOOL)isContactAccessAuthorized
  50. {
  51. return [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized;
  52. }
  53. - (BOOL)isTimeToSyncContacts
  54. {
  55. // Update address book contacts and search for matches in the server only once per day.
  56. NSDate *lastUpdate = [NSDate dateWithTimeIntervalSince1970:[[NCDatabaseManager sharedInstance] activeAccount].lastContactSync];
  57. return ![[NSCalendar currentCalendar] isDate:lastUpdate inSameDayAsDate:[NSDate date]];
  58. }
  59. - (void)searchInServerForAddressBookContacts:(BOOL)forceSync
  60. {
  61. if (![[NCSettingsController sharedInstance] isContactSyncEnabled]) {
  62. return;
  63. }
  64. if ([self isContactAccessAuthorized] && ([self isTimeToSyncContacts] || forceSync)) {
  65. NSMutableDictionary *phoneNumbersDict = [NSMutableDictionary new];
  66. NSMutableArray *contacts = [NSMutableArray new];
  67. NSInteger updateTimestamp = [[NSDate date] timeIntervalSince1970];
  68. NSError *error = nil;
  69. NSArray *keysToFetch = @[CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey];
  70. CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:keysToFetch];
  71. [_contactStore enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact * __nonnull contact, BOOL * __nonnull stop) {
  72. NSMutableArray *phoneNumbers = [NSMutableArray new];
  73. for (CNLabeledValue *phoneNumberValue in contact.phoneNumbers) {
  74. [phoneNumbers addObject:[[phoneNumberValue valueForKey:@"value"] valueForKey:@"digits"]];
  75. }
  76. if (phoneNumbers.count > 0) {
  77. NSString *identifier = [contact valueForKey:@"identifier"];
  78. NSString *givenName = [contact valueForKey:@"givenName"];
  79. NSString *familyName = [contact valueForKey:@"familyName"];
  80. NSString *name = [[NSString stringWithFormat:@"%@ %@", givenName, familyName] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  81. ABContact *abContact = [ABContact contactWithIdentifier:identifier name:name phoneNumbers:phoneNumbers lastUpdate:updateTimestamp];
  82. if (abContact) {
  83. [contacts addObject:abContact];
  84. }
  85. [phoneNumbersDict setValue:phoneNumbers forKey:identifier];
  86. }
  87. }];
  88. [self updateAddressBookCopyWithContacts:contacts andTimestamp:updateTimestamp];
  89. [self searchForPhoneNumbers:phoneNumbersDict forAccount:[[NCDatabaseManager sharedInstance] activeAccount]];
  90. } else if (![self isContactAccessDetermined]) {
  91. [self requestContactsAccess:^(BOOL granted) {
  92. if (granted) {
  93. [self searchInServerForAddressBookContacts:YES];
  94. }
  95. }];
  96. }
  97. }
  98. - (void)updateAddressBookCopyWithContacts:(NSArray *)contacts andTimestamp:(NSInteger)timestamp
  99. {
  100. RLMRealm *realm = [RLMRealm defaultRealm];
  101. [realm transactionWithBlock:^{
  102. // Add or update contacts
  103. for (ABContact *contact in contacts) {
  104. ABContact *managedABContact = [ABContact objectsWhere:@"identifier = %@", contact.identifier].firstObject;
  105. if (managedABContact) {
  106. [ABContact updateContact:managedABContact withContact:contact];
  107. } else {
  108. [realm addObject:contact];
  109. }
  110. }
  111. // Delete old contacts
  112. NSPredicate *query = [NSPredicate predicateWithFormat:@"lastUpdate != %ld", (long)timestamp];
  113. RLMResults *managedABContactsToBeDeleted = [ABContact objectsWithPredicate:query];
  114. // Delete matching nc contacts
  115. for (ABContact *managedABContact in managedABContactsToBeDeleted) {
  116. NSPredicate *query2 = [NSPredicate predicateWithFormat:@"identifier = %@", managedABContact.identifier];
  117. [realm deleteObjects:[NCContact objectsWithPredicate:query2]];
  118. }
  119. [realm deleteObjects:managedABContactsToBeDeleted];
  120. NSLog(@"Address Book Contacts updated");
  121. }];
  122. }
  123. - (void)searchForPhoneNumbers:(NSDictionary *)phoneNumbers forAccount:(TalkAccount *)account
  124. {
  125. [[NCAPIController sharedInstance] searchContactsForAccount:account withPhoneNumbers:phoneNumbers andCompletionBlock:^(NSDictionary *contacts, NSError *error) {
  126. if (!error) {
  127. BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCUpdateContacts" expirationHandler:nil];
  128. RLMRealm *realm = [RLMRealm defaultRealm];
  129. [realm transactionWithBlock:^{
  130. NSInteger updateTimestamp = [[NSDate date] timeIntervalSince1970];
  131. // Add or update matched contacts
  132. if (contacts.count > 0) {
  133. for (NSString *identifier in contacts.allKeys) {
  134. NSString *cloudId = [contacts objectForKey:identifier];
  135. NCContact *contact = [NCContact contactWithIdentifier:identifier cloudId:cloudId lastUpdate:updateTimestamp andAccountId:account.accountId];
  136. // Filter out app user (it could have its own phone number in address book)
  137. if ([contact.userId isEqualToString:account.userId]) {
  138. continue;
  139. }
  140. NCContact *managedNCContact = [NCContact objectsWhere:@"identifier = %@ AND accountId = %@", identifier, account.accountId].firstObject;
  141. if (managedNCContact) {
  142. [NCContact updateContact:managedNCContact withContact:contact];
  143. } else {
  144. [realm addObject:contact];
  145. }
  146. }
  147. }
  148. // Delete old contacts
  149. NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@ AND lastUpdate != %ld", account.accountId, (long)updateTimestamp];
  150. RLMResults *managedNCContactsToBeDeleted = [NCContact objectsWithPredicate:query];
  151. [realm deleteObjects:managedNCContactsToBeDeleted];
  152. // Update last sync for account
  153. NSPredicate *accountQuery = [NSPredicate predicateWithFormat:@"accountId = %@", account.accountId];
  154. TalkAccount *managedAccount = [TalkAccount objectsWithPredicate:accountQuery].firstObject;
  155. managedAccount.lastContactSync = updateTimestamp;
  156. NSLog(@"Matched NC Contacts updated");
  157. [[NSNotificationCenter defaultCenter] postNotificationName:NCContactsManagerContactsUpdatedNotification
  158. object:self
  159. userInfo:nil];
  160. [bgTask stopBackgroundTask];
  161. }];
  162. }
  163. }];
  164. }
  165. - (void)removeStoredContacts
  166. {
  167. RLMRealm *realm = [RLMRealm defaultRealm];
  168. [realm beginWriteTransaction];
  169. TalkAccount *account = [TalkAccount objectsWhere:(@"active = true")].firstObject;
  170. // Remove stored contacts for active account
  171. NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", account.accountId];
  172. RLMResults *managedNCContactsToBeDeleted = [NCContact objectsWithPredicate:query];
  173. [realm deleteObjects:managedNCContactsToBeDeleted];
  174. account.lastContactSync = 0;
  175. // If there are no other account with contact sync enabled -> delete address book copy
  176. TalkAccount *accountWithContactSyncEnabled = [TalkAccount objectsWhere:(@"hasContactSyncEnabled = true AND active = false")].firstObject;
  177. if (!accountWithContactSyncEnabled) {
  178. [realm deleteObjects:[ABContact allObjects]];
  179. }
  180. [realm commitWriteTransaction];
  181. }
  182. @end