Don't cache CNContact.

pull/1/head
Matthew Chen 8 years ago
parent e2de8f6ffa
commit 12295bd8c5

@ -55,17 +55,34 @@ class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPi
navigationController.popViewController(animated: true)
}
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
func contactsPicker(_: ContactsPicker, didSelectContact newContact: Contact) {
Logger.debug("\(self.logTag) in \(#function)")
guard let mergedContact: CNContact = self.contactShare.cnContact(mergedWithExistingContact: contact) else {
owsFail("\(logTag) in \(#function) mergedContact was unexpectedly nil")
return
}
let contactsManager = Environment.current().contactsManager
contactsManager?.cnContact(withId: self.contactShare.cnContactId,
success: { [weak self] oldCNContact in
contactsManager?.cnContact(withId: newContact.cnContactId,
success: { newCNContact in
guard let strongSelf = weakSelf else {
return
}
strongSelf.merge(oldCNContact: oldCNContact, newCNContact: newCNContact)
}, failure: {
// TODO: Alert?
})
}, failure: {
// TODO: Alert?
})
}
func merge(oldCNContact: CNContact, newCNContact: CNContact) {
Logger.debug("\(self.logTag) in \(#function)")
let mergedCNContact: CNContact = Contact.merge(cnContact: oldCNContact, newCNContact: newCNContact)
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
// saving our users a tap in some cases when we already know they want to edit.
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedContact)
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedCNContact)
// Default title is "New Contact". We could give a more descriptive title, but anything
// seems redundant - the context is sufficiently clear.

@ -148,16 +148,20 @@ public class ContactShareViewModel: NSObject {
return dbRecord.isProfileAvatar
}
@objc
public func cnContact(mergedWithExistingContact existingContact: Contact) -> CNContact? {
guard let newCNContact = OWSContacts.systemContact(for: self.dbRecord, imageData: self.avatarImageData) else {
owsFail("\(logTag) in \(#function) newCNContact was unexpectedly nil")
return nil
}
return existingContact.buildCNContact(mergedWithNewContact: newCNContact)
}
// @objc
// public func cnContact(mergedWithExistingContact existingContact: Contact) -> CNContact? {
// public func cnContact(mergedWithExistingContact existingContact: Contact) -> CNContact? {
//// success successParam: @escaping (CNContact) -> Void,
//// failure failureParam: @escaping () -> Void) {
//
// guard let newCNContact = OWSContacts.systemContact(for: self.dbRecord, imageData: self.avatarImageData) else {
// owsFail("\(logTag) in \(#function) newCNContact was unexpectedly nil")
// return nil
// }
//
// let mergedCNContact = Contact.merge(cnContact: <#T##CNContact#>, newCNContact: <#T##CNContact#>)
// return existingContact.buildCNContact(mergedWithNewContact: newCNContact)
// }
@objc
public func copy(withName name: OWSContactName) -> ContactShareViewModel {

@ -149,37 +149,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
self.thread = thread;
if (self.attachment.isConvertibleToContactShare) {
NSData *data = self.attachment.data;
Contact *_Nullable contact = [Contact contactWithVCardData:data];
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact];
if (!contactShareRecord) {
DDLogError(@"%@ Could not convert system contact.", self.logTag);
return;
}
BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData;
for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) {
break;
}
avatarImageData = [self.contactsManager profileImageDataForPhoneIdentifier:recipientId];
if (avatarImageData) {
isProfileAvatar = YES;
}
}
contactShareRecord.isProfileAvatar = isProfileAvatar;
ContactShareViewModel *contactShare =
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord
avatarImageData:avatarImageData];
ContactShareApprovalViewController *approvalVC =
[[ContactShareApprovalViewController alloc] initWithContactShare:contactShare
contactsManager:self.contactsManager
delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES];
[self showContactShareApproval];
return;
}
@ -200,6 +170,45 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
}
}
- (void)showContactShareApproval
{
OWSAssert(self.attachment);
OWSAssert(self.thread);
OWSAssert(self.attachment.isConvertibleToContactShare);
NSData *data = self.attachment.data;
CNContact *_Nullable cnContact = [Contact cnContactWithVCardData:data];
Contact *_Nullable contact = [[Contact alloc] initWithSystemContact:cnContact];
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:cnContact];
if (!contactShareRecord) {
DDLogError(@"%@ Could not convert system contact.", self.logTag);
return;
}
BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData;
for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) {
break;
}
avatarImageData = [self.contactsManager profileImageDataForPhoneIdentifier:recipientId];
if (avatarImageData) {
isProfileAvatar = YES;
}
}
contactShareRecord.isProfileAvatar = isProfileAvatar;
ContactShareViewModel *contactShare =
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord avatarImageData:avatarImageData];
ContactShareApprovalViewController *approvalVC =
[[ContactShareApprovalViewController alloc] initWithContactShare:contactShare
contactsManager:self.contactsManager
delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES];
}
// override
- (void)dismissPressed:(id)sender
{

@ -9,6 +9,9 @@ NS_ASSUME_NONNULL_BEGIN
extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
typedef void (^CNContactFetchSuccess)(CNContact *cnContact);
typedef void (^CNContactFetchFailure)(void);
@class ImageCache;
@class SignalAccount;
@class UIFont;
@ -59,6 +62,10 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
// contacts haven't changed, and will clear out any stale cached SignalAccounts
- (void)userRequestedSystemContactsRefreshWithCompletion:(void (^)(NSError *_Nullable error))completionHandler;
- (void)cnContactWithId:(NSString *)contactId
success:(CNContactFetchSuccess)success
failure:(CNContactFetchFailure)failure;
#pragma mark - Util
- (BOOL)isSystemContact:(NSString *)recipientId;

@ -133,6 +133,17 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
return self.systemContactsFetcher.supportsContactEditing;
}
- (void)cnContactWithId:(NSString *)contactId
success:(CNContactFetchSuccess)success
failure:(CNContactFetchFailure)failure
{
OWSAssert(contactId.length > 0);
OWSAssert(success);
OWSAssert(failure);
return [self.systemContactsFetcher fetchCNContactWithContactId:contactId success:success failure:failure];
}
#pragma mark - SystemContactsFetcherDelegate
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
@ -255,13 +266,13 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
for (Contact *contact in contacts) {
NSArray<SignalRecipient *> *signalRecipients = [contact signalRecipientsWithTransaction:transaction];
contactIdToSignalRecipientsMap[contact.uniqueId] = signalRecipients;
contactIdToSignalRecipientsMap[contact.cnContactId] = signalRecipients;
}
}];
NSMutableSet<NSString *> *seenRecipientIds = [NSMutableSet new];
for (Contact *contact in contacts) {
NSArray<SignalRecipient *> *signalRecipients = contactIdToSignalRecipientsMap[contact.uniqueId];
NSArray<SignalRecipient *> *signalRecipients = contactIdToSignalRecipientsMap[contact.cnContactId];
for (SignalRecipient *signalRecipient in [signalRecipients sortedArrayUsingSelector:@selector(compare:)]) {
if ([seenRecipientIds containsObject:signalRecipient.recipientId]) {
DDLogDebug(@"Ignoring duplicate contact: %@, %@", signalRecipient.recipientId, contact.fullName);
@ -584,7 +595,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
NSAttributedString *lastName =
[[NSAttributedString alloc] initWithString:cachedLastName attributes:lastNameAttributes];
CNContact *_Nullable cnContact = self.allContactsMap[recipientId].cnContact;
CNContact *_Nullable cnContact = self.allContactsMap[recipientId].cnContactForFormatting;
if (!cnContact) {
// If we don't have a CNContact for this recipient id, make one.
// Presumably [CNContactFormatter nameOrderForContact:] tries

@ -17,12 +17,12 @@ protocol ContactStoreAdaptee {
var supportsContactEditing: Bool { get }
func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void)
func fetchContacts() -> Result<[Contact], Error>
func fetchCNContact(contactId: String) -> CNContact?
func startObservingChanges(changeHandler: @escaping () -> Void)
}
public
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
class ContactsFrameworkContactStoreAdaptee: NSObject, ContactStoreAdaptee {
private let contactStore = CNContactStore()
private var changeHandler: (() -> Void)?
private var initializedObserver = false
@ -72,7 +72,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
return
}
Logger.info("\(self.TAG) sort order changed: \(String(describing: self.lastSortOrder)) -> \(String(describing: currentSortOrder))")
Logger.info("\(self.logTag) sort order changed: \(String(describing: self.lastSortOrder)) -> \(String(describing: currentSortOrder))")
self.lastSortOrder = currentSortOrder
self.runChangeHandler()
}
@ -81,7 +81,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
@objc
func runChangeHandler() {
guard let changeHandler = self.changeHandler else {
owsFail("\(TAG) trying to run change handler before it was registered")
owsFail("\(self.logTag) trying to run change handler before it was registered")
return
}
changeHandler()
@ -100,13 +100,35 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
systemContacts.append(contact)
}
} catch let error as NSError {
owsFail("\(self.TAG) Failed to fetch contacts with error:\(error)")
owsFail("\(self.logTag) Failed to fetch contacts with error:\(error)")
return .error(error)
}
let contacts = systemContacts.map { Contact(systemContact: $0) }
return .success(contacts)
}
func fetchCNContact(contactId: String) -> CNContact? {
var result: CNContact?
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: ContactsFrameworkContactStoreAdaptee.allowedContactKeys)
contactFetchRequest.sortOrder = .userDefault
contactFetchRequest.predicate = CNContact.predicateForContacts(withIdentifiers: [contactId])
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
guard result == nil else {
owsFail("\(self.logTag) More than one contact with contact id.")
return
}
result = contact
}
} catch let error as NSError {
owsFail("\(self.logTag) Failed to fetch contact with error:\(error)")
return nil
}
return result
}
}
@objc
@ -125,8 +147,6 @@ public enum ContactStoreAuthorizationStatus: UInt {
@objc
public class SystemContactsFetcher: NSObject {
private let TAG = "[SystemContactsFetcher]"
private let serialQueue = DispatchQueue(label: "SystemContactsFetcherQueue")
var lastContactUpdateHash: Int?
@ -207,20 +227,20 @@ public class SystemContactsFetcher: NSObject {
switch authorizationStatus {
case .notDetermined:
if CurrentAppContext().isInBackground() {
Logger.error("\(self.TAG) do not request contacts permission when app is in background")
Logger.error("\(self.logTag) do not request contacts permission when app is in background")
completion(nil)
return
}
self.contactStoreAdapter.requestAccess { (granted, error) in
if let error = error {
Logger.error("\(self.TAG) error fetching contacts: \(error)")
Logger.error("\(self.logTag) error fetching contacts: \(error)")
completion(error)
return
}
guard granted else {
// This case should have been caught by the error guard a few lines up.
owsFail("\(self.TAG) declined contact access.")
owsFail("\(self.logTag) declined contact access.")
completion(nil)
return
}
@ -232,7 +252,7 @@ public class SystemContactsFetcher: NSObject {
case .authorized:
self.updateContacts(completion: completion)
case .denied, .restricted:
Logger.debug("\(TAG) contacts were \(self.authorizationStatus)")
Logger.debug("\(logTag) contacts were \(self.authorizationStatus)")
self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus)
completion(nil)
}
@ -292,7 +312,7 @@ public class SystemContactsFetcher: NSObject {
guard let _ = self else {
return
}
Logger.error("background task time ran out contacts fetch completed.")
Logger.error("background task time ran out before contacts fetch completed.")
})
// Ensure completion is invoked on main thread.
@ -310,7 +330,7 @@ public class SystemContactsFetcher: NSObject {
serialQueue.async {
Logger.info("\(self.TAG) fetching contacts")
Logger.info("\(self.logTag) fetching contacts")
var fetchedContacts: [Contact]?
switch self.contactStoreAdapter.fetchContacts() {
@ -322,21 +342,21 @@ public class SystemContactsFetcher: NSObject {
}
guard let contacts = fetchedContacts else {
owsFail("\(self.TAG) contacts was unexpectedly not set.")
owsFail("\(self.logTag) contacts was unexpectedly not set.")
completion(nil)
}
Logger.info("\(self.TAG) fetched \(contacts.count) contacts.")
Logger.info("\(self.logTag) fetched \(contacts.count) contacts.")
let contactsHash = HashableArray(contacts).hashValue
DispatchQueue.main.async {
var shouldNotifyDelegate = false
if self.lastContactUpdateHash != contactsHash {
Logger.info("\(self.TAG) contact hash changed. new contactsHash: \(contactsHash)")
Logger.info("\(self.logTag) contact hash changed. new contactsHash: \(contactsHash)")
shouldNotifyDelegate = true
} else if isUserRequested {
Logger.info("\(self.TAG) ignoring debounce due to user request")
Logger.info("\(self.logTag) ignoring debounce due to user request")
shouldNotifyDelegate = true
} else {
@ -346,19 +366,19 @@ public class SystemContactsFetcher: NSObject {
let expiresAtDate = Date(timeInterval: kDebounceInterval, since: lastDelegateNotificationDate)
if Date() > expiresAtDate {
Logger.info("\(self.TAG) debounce interval expired at: \(expiresAtDate)")
Logger.info("\(self.logTag) debounce interval expired at: \(expiresAtDate)")
shouldNotifyDelegate = true
} else {
Logger.info("\(self.TAG) ignoring since debounce interval hasn't expired")
Logger.info("\(self.logTag) ignoring since debounce interval hasn't expired")
}
} else {
Logger.info("\(self.TAG) first contact fetch. contactsHash: \(contactsHash)")
Logger.info("\(self.logTag) first contact fetch. contactsHash: \(contactsHash)")
shouldNotifyDelegate = true
}
}
guard shouldNotifyDelegate else {
Logger.info("\(self.TAG) no reason to notify delegate.")
Logger.info("\(self.logTag) no reason to notify delegate.")
completion(nil)
@ -373,6 +393,66 @@ public class SystemContactsFetcher: NSObject {
}
}
}
@objc
public func fetchCNContact(contactId: String,
success successParam: @escaping (CNContact) -> Void,
failure failureParam: @escaping () -> Void) {
SwiftAssertIsOnMainThread(#function)
guard authorizationStatus == .authorized else {
Logger.error("\(logTag) contact fetch failed; no access.")
failureParam()
return
}
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in
SwiftAssertIsOnMainThread(#function)
guard status == .expired else {
return
}
guard let _ = self else {
return
}
Logger.error("background task time ran out before contact fetch completed.")
})
// Ensure success is invoked on main thread.
let success: (CNContact) -> Void = { contact in
DispatchMainThreadSafe({
successParam(contact)
assert(backgroundTask != nil)
backgroundTask = nil
})
}
// Ensure success is invoked on main thread.
let failure: () -> Void = {
DispatchMainThreadSafe({
failureParam()
assert(backgroundTask != nil)
backgroundTask = nil
})
}
// Don't use the serial queue.
DispatchQueue.global().async {
Logger.info("\(self.logTag) fetching contact")
if let cnContact = self.contactStoreAdapter.fetchCNContact(contactId: contactId) {
Logger.info("\(self.logTag) contact found")
success(cnContact)
} else {
Logger.info("\(self.logTag) contact not found")
failure()
}
}
}
}
struct HashableArray<Element: Hashable>: Hashable {

@ -26,12 +26,11 @@ NS_ASSUME_NONNULL_BEGIN
@property (readonly, nonatomic) NSArray<PhoneNumber *> *parsedPhoneNumbers;
@property (readonly, nonatomic) NSArray<NSString *> *userTextPhoneNumbers;
@property (readonly, nonatomic) NSArray<NSString *> *emails;
@property (readonly, nonatomic) NSString *uniqueId;
@property (nonatomic, readonly) BOOL isSignalContact;
@property (nonatomic, readonly) NSString *cnContactId;
#if TARGET_OS_IOS
@property (nullable, readonly, nonatomic) UIImage *image;
@property (nullable, readonly, nonatomic) NSData *imageData;
@property (nullable, nonatomic, readonly) CNContact *cnContact;
#endif // TARGET_OS_IOS
- (NSArray<SignalRecipient *> *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction;
@ -41,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_IOS
- (instancetype)initWithSystemContact:(CNContact *)contact NS_AVAILABLE_IOS(9_0);
+ (nullable Contact *)contactWithVCardData:(NSData *)data;
+ (nullable CNContact *)cnContactWithVCardData:(NSData *)data;
- (NSString *)nameForPhoneNumber:(NSString *)recipientId;
@ -51,7 +50,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact NS_SWIFT_NAME(formattedFullName(cnContact:));
+ (nullable NSString *)localizedStringForCNLabel:(nullable NSString *)cnLabel;
- (CNContact *)buildCNContactMergedWithNewContact:(CNContact *)newCNContact NS_SWIFT_NAME(buildCNContact(mergedWithNewContact:));
+ (CNContact *)mergeCNContact:(CNContact *)oldCNContact
newCNContact:(CNContact *)newCNContact NS_SWIFT_NAME(merge(cnContact:newCNContact:));
- (CNContact *)cnContactForFormatting;
@end

@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface Contact ()
@property (readonly, nonatomic) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
@end
@ -37,11 +37,10 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
_cnContact = contact;
_cnContactId = contact.identifier;
_firstName = contact.givenName.ows_stripped;
_lastName = contact.familyName.ows_stripped;
_fullName = [Contact formattedFullNameWithCNContact:contact];
_uniqueId = contact.identifier;
NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new];
NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap = [NSMutableDictionary new];
@ -114,17 +113,6 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
+ (nullable Contact *)contactWithVCardData:(NSData *)data
{
CNContact *_Nullable cnContact = [self cnContactWithVCardData:data];
if (!cnContact) {
return nil;
}
return [[self alloc] initWithSystemContact:cnContact];
}
- (nullable UIImage *)image
{
if (_image) {
@ -326,9 +314,14 @@ NS_ASSUME_NONNULL_BEGIN
return contacts.firstObject;
}
- (CNContact *)buildCNContactMergedWithNewContact:(CNContact *)newCNContact
+ (CNContact *)mergeCNContact:(CNContact *)oldCNContact newCNContact:(CNContact *)newCNContact
{
CNMutableContact *_Nullable mergedCNContact = [self.cnContact mutableCopy];
OWSAssert(oldCNContact);
OWSAssert(newCNContact);
Contact *oldContact = [[Contact alloc] initWithSystemContact:oldCNContact];
CNMutableContact *_Nullable mergedCNContact = [oldCNContact mutableCopy];
if (!mergedCNContact) {
OWSFail(@"%@ in %s mergedCNContact was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__);
return [CNContact new];
@ -351,8 +344,8 @@ NS_ASSUME_NONNULL_BEGIN
}
// Phone Numbers
NSSet<PhoneNumber *> *existingParsedPhoneNumberSet = [NSSet setWithArray:self.parsedPhoneNumbers];
NSSet<NSString *> *existingUnparsedPhoneNumberSet = [NSSet setWithArray:self.userTextPhoneNumbers];
NSSet<PhoneNumber *> *existingParsedPhoneNumberSet = [NSSet setWithArray:oldContact.parsedPhoneNumbers];
NSSet<NSString *> *existingUnparsedPhoneNumberSet = [NSSet setWithArray:oldContact.userTextPhoneNumbers];
NSMutableArray<CNLabeledValue<CNPhoneNumber *> *> *mergedPhoneNumbers = [mergedCNContact.phoneNumbers mutableCopy];
for (CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber in newCNContact.phoneNumbers) {
@ -371,7 +364,7 @@ NS_ASSUME_NONNULL_BEGIN
mergedCNContact.phoneNumbers = mergedPhoneNumbers;
// Emails
NSSet<NSString *> *existingEmailSet = [NSSet setWithArray:self.emails];
NSSet<NSString *> *existingEmailSet = [NSSet setWithArray:oldContact.emails];
NSMutableArray<CNLabeledValue<NSString *> *> *mergedEmailAddresses = [mergedCNContact.emailAddresses mutableCopy];
for (CNLabeledValue<NSString *> *labeledEmail in newCNContact.emailAddresses) {
NSString *normalizedValue = labeledEmail.value.ows_stripped;
@ -416,6 +409,14 @@ NS_ASSUME_NONNULL_BEGIN
return localizedLabel;
}
- (CNContact *)cnContactForFormatting
{
CNMutableContact *cnContact = [CNMutableContact new];
cnContact.givenName = self.firstName;
cnContact.familyName = self.lastName;
return cnContact;
}
@end
NS_ASSUME_NONNULL_END

Loading…
Cancel
Save