Merge branch 'charlesmchen/lazyContact'

pull/1/head
Matthew Chen 7 years ago
commit 8c6eb791a2

@ -55,17 +55,29 @@ class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPi
navigationController.popViewController(animated: true) navigationController.popViewController(animated: true)
} }
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) { func contactsPicker(_: ContactsPicker, didSelectContact oldContact: Contact) {
Logger.debug("\(self.logTag) in \(#function)") Logger.debug("\(self.logTag) in \(#function)")
guard let mergedContact: CNContact = self.contactShare.cnContact(mergedWithExistingContact: contact) else { let contactsManager = Environment.current().contactsManager
owsFail("\(logTag) in \(#function) mergedContact was unexpectedly nil") guard let oldCNContact = contactsManager?.cnContact(withId: oldContact.cnContactId) else {
owsFail("\(logTag) could not load old CNContact.")
return return
} }
guard let newCNContact = OWSContacts.systemContact(for: self.contactShare.dbRecord, imageData: self.contactShare.avatarImageData) else {
owsFail("\(logTag) could not load new CNContact.")
return
}
merge(oldCNContact: oldCNContact, newCNContact: newCNContact)
}
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 // 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. // 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 // Default title is "New Contact". We could give a more descriptive title, but anything
// seems redundant - the context is sufficiently clear. // seems redundant - the context is sufficiently clear.

@ -5118,18 +5118,23 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact - (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
{ {
OWSAssert(contact); OWSAssert(contact);
OWSAssert(contact.cnContact);
CNContact *_Nullable cnContact = [self.contactsManager cnContactWithId:contact.cnContactId];
if (!cnContact) {
OWSFail(@"%@ Could not load system contact.", self.logTag);
return;
}
DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact); DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact);
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact]; OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:cnContact];
if (!contactShareRecord) { if (!contactShareRecord) {
DDLogError(@"%@ Could not convert system contact.", self.logTag); OWSFail(@"%@ Could not convert system contact.", self.logTag);
return; return;
} }
BOOL isProfileAvatar = NO; BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData; NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:cnContact.identifier];
for (NSString *recipientId in contact.textSecureIdentifiers) { for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) { if (avatarImageData) {
break; break;

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "DebugUIContacts.h" #import "DebugUIContacts.h"
@ -1164,7 +1164,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(count > 0); OWSAssert(count > 0);
OWSAssert(batchCompletionHandler); OWSAssert(batchCompletionHandler);
DDLogDebug(@"createRandomContactsBatch: %zd", count); DDLogDebug(@"createRandomContactsBatch: %zu", count);
CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts]; CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
if (status == CNAuthorizationStatusDenied || status == CNAuthorizationStatusRestricted) { if (status == CNAuthorizationStatusDenied || status == CNAuthorizationStatusRestricted) {
@ -1219,7 +1219,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
DDLogError(@"Saving fake contacts: %zd", contacts.count); DDLogError(@"Saving fake contacts: %zu", contacts.count);
NSError *saveError = nil; NSError *saveError = nil;
if (![store executeSaveRequest:request error:&saveError]) { if (![store executeSaveRequest:request error:&saveError]) {

@ -133,6 +133,11 @@ NS_ASSUME_NONNULL_BEGIN
} }
}]]; }]];
[items addObject:[OWSTableItem itemWithTitle:@"Fetch system contacts"
actionBlock:^() {
[Environment.current.contactsManager requestSystemContactsOnce];
}]];
return [OWSTableSection sectionWithTitle:self.name items:items]; return [OWSTableSection sectionWithTitle:self.name items:items];
} }

@ -161,8 +161,6 @@ NS_ASSUME_NONNULL_BEGIN
@"EDIT_GROUP_CONTACTS_SECTION_TITLE", @"a title for the contacts section of the 'new/update group' view."); @"EDIT_GROUP_CONTACTS_SECTION_TITLE", @"a title for the contacts section of the 'new/update group' view.");
for (Contact *contact in self.contactsViewHelper.contactsManager.allContacts) { for (Contact *contact in self.contactsViewHelper.contactsManager.allContacts) {
OWSAssert(contact.cnContact);
NSString *_Nullable displayName = [self displayNameForContact:contact]; NSString *_Nullable displayName = [self displayNameForContact:contact];
if (displayName.length < 1) { if (displayName.length < 1) {
continue; continue;
@ -198,10 +196,15 @@ NS_ASSUME_NONNULL_BEGIN
OWSFail(@"%@ Contact editing not supported", self.logTag); OWSFail(@"%@ Contact editing not supported", self.logTag);
return; return;
} }
CNContact *_Nullable cnContact = [self.contactsManager cnContactWithId:contact.cnContactId];
if (!cnContact) {
OWSFail(@"%@ Could not load system contact.", self.logTag);
return;
}
[self.contactsViewHelper presentContactViewControllerForRecipientId:self.recipientId [self.contactsViewHelper presentContactViewControllerForRecipientId:self.recipientId
fromViewController:self fromViewController:self
editImmediately:YES editImmediately:YES
addToExistingCnContact:contact.cnContact]; addToExistingCnContact:cnContact];
} }
@end @end

@ -74,10 +74,11 @@ class ContactCell: UITableViewCell {
self.contact = contact self.contact = contact
self.showsWhenSelected = showsWhenSelected self.showsWhenSelected = showsWhenSelected
titleLabel.attributedText = contact.cnContact?.formattedFullName(font: titleLabel.font) let cnContact = contactsManager.cnContact(withId: contact.cnContactId)
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
updateSubtitle(subtitleType: subtitleType, contact: contact) updateSubtitle(subtitleType: subtitleType, contact: contact)
if let contactImage = contact.image { if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
contactImageView.image = contactImage contactImageView.image = contactImage
} else { } else {
let contactIdForDeterminingBackgroundColor: String let contactIdForDeterminingBackgroundColor: String

@ -148,17 +148,6 @@ public class ContactShareViewModel: NSObject {
return dbRecord.isProfileAvatar 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 @objc
public func copy(withName name: OWSContactName) -> ContactShareViewModel { public func copy(withName name: OWSContactName) -> ContactShareViewModel {
@ -177,5 +166,4 @@ public class ContactShareViewModel: NSObject {
// If we want to keep the avatar image, the caller will need to re-apply it. // If we want to keep the avatar image, the caller will need to re-apply it.
return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImageData: nil) return ContactShareViewModel(contactShareRecord: newDbRecord, avatarImageData: nil)
} }
} }

@ -355,7 +355,7 @@ NS_ASSUME_NONNULL_BEGIN
cnContact = updatedContact; cnContact = updatedContact;
} }
if (signalAccount && !cnContact) { if (signalAccount && !cnContact) {
cnContact = signalAccount.contact.cnContact; cnContact = [self.contactsManager cnContactWithId:signalAccount.contact.cnContactId];
} }
if (cnContact) { if (cnContact) {
if (shouldEditImmediately) { if (shouldEditImmediately) {

@ -149,17 +149,45 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
self.thread = thread; self.thread = thread;
if (self.attachment.isConvertibleToContactShare) { if (self.attachment.isConvertibleToContactShare) {
[self showContactShareApproval];
return;
}
NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible];
if (messageText) {
MessageApprovalViewController *approvalVC =
[[MessageApprovalViewController alloc] initWithMessageText:messageText
thread:thread
contactsManager:self.contactsManager
delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES];
} else {
OWSNavigationController *approvalModal =
[AttachmentApprovalViewController wrappedInNavControllerWithAttachment:self.attachment delegate:self];
[self presentViewController:approvalModal animated:YES completion:nil];
}
}
- (void)showContactShareApproval
{
OWSAssert(self.attachment);
OWSAssert(self.thread);
OWSAssert(self.attachment.isConvertibleToContactShare);
NSData *data = self.attachment.data; NSData *data = self.attachment.data;
Contact *_Nullable contact = [Contact contactWithVCardData:data]; CNContact *_Nullable cnContact = [Contact cnContactWithVCardData:data];
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact]; Contact *_Nullable contact = [[Contact alloc] initWithSystemContact:cnContact];
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:cnContact];
if (!contactShareRecord) { if (!contactShareRecord) {
DDLogError(@"%@ Could not convert system contact.", self.logTag); DDLogError(@"%@ Could not convert system contact.", self.logTag);
return; return;
} }
BOOL isProfileAvatar = NO; BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData; NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:contact.cnContactId];
for (NSString *recipientId in contact.textSecureIdentifiers) { for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) { if (avatarImageData) {
break; break;
@ -172,32 +200,13 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
contactShareRecord.isProfileAvatar = isProfileAvatar; contactShareRecord.isProfileAvatar = isProfileAvatar;
ContactShareViewModel *contactShare = ContactShareViewModel *contactShare =
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord [[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord avatarImageData:avatarImageData];
avatarImageData:avatarImageData];
ContactShareApprovalViewController *approvalVC = ContactShareApprovalViewController *approvalVC =
[[ContactShareApprovalViewController alloc] initWithContactShare:contactShare [[ContactShareApprovalViewController alloc] initWithContactShare:contactShare
contactsManager:self.contactsManager contactsManager:self.contactsManager
delegate:self]; delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES]; [self.navigationController pushViewController:approvalVC animated:YES];
return;
}
NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible];
if (messageText) {
MessageApprovalViewController *approvalVC =
[[MessageApprovalViewController alloc] initWithMessageText:messageText
thread:thread
contactsManager:self.contactsManager
delegate:self];
[self.navigationController pushViewController:approvalVC animated:YES];
} else {
OWSNavigationController *approvalModal =
[AttachmentApprovalViewController wrappedInNavControllerWithAttachment:self.attachment delegate:self];
[self presentViewController:approvalModal animated:YES completion:nil];
}
} }
// override // override

@ -36,6 +36,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher; @property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
@property (nonatomic, readonly) YapDatabaseConnection *dbReadConnection; @property (nonatomic, readonly) YapDatabaseConnection *dbReadConnection;
@property (nonatomic, readonly) YapDatabaseConnection *dbWriteConnection; @property (nonatomic, readonly) YapDatabaseConnection *dbWriteConnection;
@property (nonatomic, readonly) NSCache<NSString *, CNContact *> *cnContactCache;
@property (nonatomic, readonly) NSCache<NSString *, UIImage *> *cnContactAvatarCache;
@end @end
@ -60,6 +62,10 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
_signalAccounts = @[]; _signalAccounts = @[];
_systemContactsFetcher = [SystemContactsFetcher new]; _systemContactsFetcher = [SystemContactsFetcher new];
_systemContactsFetcher.delegate = self; _systemContactsFetcher.delegate = self;
_cnContactCache = [NSCache new];
_cnContactCache.countLimit = 50;
_cnContactAvatarCache = [NSCache new];
_cnContactAvatarCache.countLimit = 25;
OWSSingletonAssert(); OWSSingletonAssert();
@ -133,6 +139,63 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
return self.systemContactsFetcher.supportsContactEditing; return self.systemContactsFetcher.supportsContactEditing;
} }
#pragma mark - CNContacts
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId
{
OWSAssert(contactId.length > 0);
OWSAssert(self.cnContactCache);
if (!contactId) {
return nil;
}
CNContact *_Nullable cnContact;
@synchronized(self.cnContactCache) {
cnContact = [self.cnContactCache objectForKey:contactId];
if (!cnContact) {
cnContact = [self.systemContactsFetcher fetchCNContactWithContactId:contactId];
if (cnContact) {
[self.cnContactCache setObject:cnContact forKey:contactId];
}
}
}
return cnContact;
}
- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId
{
// Don't bother to cache avatar data.
CNContact *_Nullable cnContact = [self cnContactWithId:contactId];
return [Contact avatarDataForCNContact:cnContact];
}
- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId
{
OWSAssert(self.cnContactAvatarCache);
if (!contactId) {
return nil;
}
UIImage *_Nullable avatarImage;
@synchronized(self.cnContactAvatarCache) {
avatarImage = [self.cnContactAvatarCache objectForKey:contactId];
if (!avatarImage) {
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
if (avatarData) {
avatarImage = [UIImage imageWithData:avatarData];
}
if (avatarImage) {
[self.cnContactAvatarCache setObject:avatarImage forKey:contactId];
}
}
}
return avatarImage;
}
#pragma mark - SystemContactsFetcherDelegate #pragma mark - SystemContactsFetcherDelegate
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher - (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
@ -232,6 +295,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
self.allContacts = contacts; self.allContacts = contacts;
self.allContactsMap = [allContactsMap copy]; self.allContactsMap = [allContactsMap copy];
[self.cnContactCache removeAllObjects];
[self.cnContactAvatarCache removeAllObjects];
[self.avatarCache removeAllImages]; [self.avatarCache removeAllImages];
@ -584,7 +649,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
NSAttributedString *lastName = NSAttributedString *lastName =
[[NSAttributedString alloc] initWithString:cachedLastName attributes:lastNameAttributes]; [[NSAttributedString alloc] initWithString:cachedLastName attributes:lastNameAttributes];
CNContact *_Nullable cnContact = self.allContactsMap[recipientId].cnContact; NSString *_Nullable cnContactId = self.allContactsMap[recipientId].cnContactId;
CNContact *_Nullable cnContact = [self cnContactWithId:cnContactId];
if (!cnContact) { if (!cnContact) {
// If we don't have a CNContact for this recipient id, make one. // If we don't have a CNContact for this recipient id, make one.
// Presumably [CNContactFormatter nameOrderForContact:] tries // Presumably [CNContactFormatter nameOrderForContact:] tries
@ -765,7 +831,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
contact = [self signalAccountForRecipientId:identifier].contact; contact = [self signalAccountForRecipientId:identifier].contact;
} }
return contact.image; return [self avatarImageForCNContactId:contact.cnContactId];
} }
- (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier - (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier

@ -17,12 +17,12 @@ protocol ContactStoreAdaptee {
var supportsContactEditing: Bool { get } var supportsContactEditing: Bool { get }
func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void)
func fetchContacts() -> Result<[Contact], Error> func fetchContacts() -> Result<[Contact], Error>
func fetchCNContact(contactId: String) -> CNContact?
func startObservingChanges(changeHandler: @escaping () -> Void) func startObservingChanges(changeHandler: @escaping () -> Void)
} }
public public
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee { class ContactsFrameworkContactStoreAdaptee: NSObject, ContactStoreAdaptee {
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
private let contactStore = CNContactStore() private let contactStore = CNContactStore()
private var changeHandler: (() -> Void)? private var changeHandler: (() -> Void)?
private var initializedObserver = false private var initializedObserver = false
@ -72,7 +72,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
return 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.lastSortOrder = currentSortOrder
self.runChangeHandler() self.runChangeHandler()
} }
@ -81,7 +81,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
@objc @objc
func runChangeHandler() { func runChangeHandler() {
guard let changeHandler = self.changeHandler else { 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 return
} }
changeHandler() changeHandler()
@ -100,13 +100,35 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
systemContacts.append(contact) systemContacts.append(contact)
} }
} catch let error as NSError { } 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) return .error(error)
} }
let contacts = systemContacts.map { Contact(systemContact: $0) } let contacts = systemContacts.map { Contact(systemContact: $0) }
return .success(contacts) 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 @objc
@ -125,8 +147,6 @@ public enum ContactStoreAuthorizationStatus: UInt {
@objc @objc
public class SystemContactsFetcher: NSObject { public class SystemContactsFetcher: NSObject {
private let TAG = "[SystemContactsFetcher]"
private let serialQueue = DispatchQueue(label: "SystemContactsFetcherQueue") private let serialQueue = DispatchQueue(label: "SystemContactsFetcherQueue")
var lastContactUpdateHash: Int? var lastContactUpdateHash: Int?
@ -207,20 +227,20 @@ public class SystemContactsFetcher: NSObject {
switch authorizationStatus { switch authorizationStatus {
case .notDetermined: case .notDetermined:
if CurrentAppContext().isInBackground() { 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) completion(nil)
return return
} }
self.contactStoreAdapter.requestAccess { (granted, error) in self.contactStoreAdapter.requestAccess { (granted, error) in
if let error = error { if let error = error {
Logger.error("\(self.TAG) error fetching contacts: \(error)") Logger.error("\(self.logTag) error fetching contacts: \(error)")
completion(error) completion(error)
return return
} }
guard granted else { guard granted else {
// This case should have been caught by the error guard a few lines up. // 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) completion(nil)
return return
} }
@ -232,7 +252,7 @@ public class SystemContactsFetcher: NSObject {
case .authorized: case .authorized:
self.updateContacts(completion: completion) self.updateContacts(completion: completion)
case .denied, .restricted: case .denied, .restricted:
Logger.debug("\(TAG) contacts were \(self.authorizationStatus)") Logger.debug("\(logTag) contacts were \(self.authorizationStatus)")
self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus) self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus)
completion(nil) completion(nil)
} }
@ -292,7 +312,7 @@ public class SystemContactsFetcher: NSObject {
guard let _ = self else { guard let _ = self else {
return 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. // Ensure completion is invoked on main thread.
@ -310,7 +330,7 @@ public class SystemContactsFetcher: NSObject {
serialQueue.async { serialQueue.async {
Logger.info("\(self.TAG) fetching contacts") Logger.info("\(self.logTag) fetching contacts")
var fetchedContacts: [Contact]? var fetchedContacts: [Contact]?
switch self.contactStoreAdapter.fetchContacts() { switch self.contactStoreAdapter.fetchContacts() {
@ -322,21 +342,21 @@ public class SystemContactsFetcher: NSObject {
} }
guard let contacts = fetchedContacts else { guard let contacts = fetchedContacts else {
owsFail("\(self.TAG) contacts was unexpectedly not set.") owsFail("\(self.logTag) contacts was unexpectedly not set.")
completion(nil) completion(nil)
} }
Logger.info("\(self.TAG) fetched \(contacts.count) contacts.") Logger.info("\(self.logTag) fetched \(contacts.count) contacts.")
let contactsHash = HashableArray(contacts).hashValue let contactsHash = HashableArray(contacts).hashValue
DispatchQueue.main.async { DispatchQueue.main.async {
var shouldNotifyDelegate = false var shouldNotifyDelegate = false
if self.lastContactUpdateHash != contactsHash { 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 shouldNotifyDelegate = true
} else if isUserRequested { } 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 shouldNotifyDelegate = true
} else { } else {
@ -346,19 +366,19 @@ public class SystemContactsFetcher: NSObject {
let expiresAtDate = Date(timeInterval: kDebounceInterval, since: lastDelegateNotificationDate) let expiresAtDate = Date(timeInterval: kDebounceInterval, since: lastDelegateNotificationDate)
if Date() > expiresAtDate { if Date() > expiresAtDate {
Logger.info("\(self.TAG) debounce interval expired at: \(expiresAtDate)") Logger.info("\(self.logTag) debounce interval expired at: \(expiresAtDate)")
shouldNotifyDelegate = true shouldNotifyDelegate = true
} else { } else {
Logger.info("\(self.TAG) ignoring since debounce interval hasn't expired") Logger.info("\(self.logTag) ignoring since debounce interval hasn't expired")
} }
} else { } else {
Logger.info("\(self.TAG) first contact fetch. contactsHash: \(contactsHash)") Logger.info("\(self.logTag) first contact fetch. contactsHash: \(contactsHash)")
shouldNotifyDelegate = true shouldNotifyDelegate = true
} }
} }
guard shouldNotifyDelegate else { guard shouldNotifyDelegate else {
Logger.info("\(self.TAG) no reason to notify delegate.") Logger.info("\(self.logTag) no reason to notify delegate.")
completion(nil) completion(nil)
@ -373,6 +393,18 @@ public class SystemContactsFetcher: NSObject {
} }
} }
} }
@objc
public func fetchCNContact(contactId: String) -> CNContact? {
SwiftAssertIsOnMainThread(#function)
guard authorizationStatus == .authorized else {
Logger.error("\(logTag) contact fetch failed; no access.")
return nil
}
return contactStoreAdapter.fetchCNContact(contactId: contactId)
}
} }
struct HashableArray<Element: Hashable>: Hashable { struct HashableArray<Element: Hashable>: Hashable {

@ -5,7 +5,7 @@
@objc @objc
public class AnyLRUCache: NSObject { public class AnyLRUCache: NSObject {
let backingCache: LRUCache<NSObject, NSObject> private let backingCache: LRUCache<NSObject, NSObject>
@objc @objc
public init(maxSize: Int) { public init(maxSize: Int) {
@ -21,29 +21,63 @@ public class AnyLRUCache: NSObject {
public func set(key: NSObject, value: NSObject) { public func set(key: NSObject, value: NSObject) {
self.backingCache.set(key: key, value: value) self.backingCache.set(key: key, value: value)
} }
@objc
public func clear() {
self.backingCache.clear()
}
} }
// A simple LRU cache bounded by the number of entries. // A simple LRU cache bounded by the number of entries.
//
// TODO: We might want to observe memory pressure notifications.
public class LRUCache<KeyType: Hashable & Equatable, ValueType> { public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
private var cacheMap: [KeyType: ValueType] = [:] private var cacheMap: [KeyType: ValueType] = [:]
private var cacheOrder: [KeyType] = [] private var cacheOrder: [KeyType] = []
private let maxSize: Int private let maxSize: Int
@objc
public init(maxSize: Int) { public init(maxSize: Int) {
self.maxSize = maxSize self.maxSize = maxSize
NotificationCenter.default.addObserver(self,
selector: #selector(didReceiveMemoryWarning),
name: NSNotification.Name.UIApplicationDidReceiveMemoryWarning,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(didEnterBackground),
name: NSNotification.Name.OWSApplicationDidEnterBackground,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func didEnterBackground() {
SwiftAssertIsOnMainThread(#function)
clear()
}
@objc func didReceiveMemoryWarning() {
SwiftAssertIsOnMainThread(#function)
clear()
}
private func updateCacheOrder(key: KeyType) {
cacheOrder = cacheOrder.filter { $0 != key }
cacheOrder.append(key)
} }
public func get(key: KeyType) -> ValueType? { public func get(key: KeyType) -> ValueType? {
guard let value = cacheMap[key] else { guard let value = cacheMap[key] else {
// Miss
return nil return nil
} }
// Update cache order. // Hit
cacheOrder = cacheOrder.filter { $0 != key } updateCacheOrder(key: key)
cacheOrder.append(key)
return value return value
} }
@ -51,9 +85,7 @@ public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
public func set(key: KeyType, value: ValueType) { public func set(key: KeyType, value: ValueType) {
cacheMap[key] = value cacheMap[key] = value
// Update cache order. updateCacheOrder(key: key)
cacheOrder = cacheOrder.filter { $0 != key }
cacheOrder.append(key)
while cacheOrder.count > maxSize { while cacheOrder.count > maxSize {
guard let staleKey = cacheOrder.first else { guard let staleKey = cacheOrder.first else {
@ -64,4 +96,10 @@ public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
cacheMap.removeValue(forKey: staleKey) cacheMap.removeValue(forKey: staleKey)
} }
} }
@objc
public func clear() {
cacheMap.removeAll()
cacheOrder.removeAll()
}
} }

@ -28,11 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (readonly, nonatomic) NSArray<NSString *> *emails; @property (readonly, nonatomic) NSArray<NSString *> *emails;
@property (readonly, nonatomic) NSString *uniqueId; @property (readonly, nonatomic) NSString *uniqueId;
@property (nonatomic, readonly) BOOL isSignalContact; @property (nonatomic, readonly) BOOL isSignalContact;
#if TARGET_OS_IOS @property (nonatomic, readonly) NSString *cnContactId;
@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; - (NSArray<SignalRecipient *> *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction;
// TODO: Remove this method. // TODO: Remove this method.
@ -40,8 +36,9 @@ NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_IOS #if TARGET_OS_IOS
- (instancetype)initWithSystemContact:(CNContact *)contact NS_AVAILABLE_IOS(9_0); - (instancetype)initWithSystemContact:(CNContact *)cnContact NS_AVAILABLE_IOS(9_0);
+ (nullable Contact *)contactWithVCardData:(NSData *)data; + (nullable Contact *)contactWithVCardData:(NSData *)data;
+ (nullable CNContact *)cnContactWithVCardData:(NSData *)data;
- (NSString *)nameForPhoneNumber:(NSString *)recipientId; - (NSString *)nameForPhoneNumber:(NSString *)recipientId;
@ -51,7 +48,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact NS_SWIFT_NAME(formattedFullName(cnContact:)); + (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact NS_SWIFT_NAME(formattedFullName(cnContact:));
+ (nullable NSString *)localizedStringForCNLabel:(nullable NSString *)cnLabel; + (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:));
+ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact;
@end @end

@ -9,6 +9,7 @@
#import "PhoneNumber.h" #import "PhoneNumber.h"
#import "SignalRecipient.h" #import "SignalRecipient.h"
#import "TSAccountManager.h" #import "TSAccountManager.h"
#import "TextSecureKitEnv.h"
@import Contacts; @import Contacts;
@ -16,7 +17,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface Contact () @interface Contact ()
@property (readonly, nonatomic) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap; @property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
@property (nonatomic, readonly) NSUInteger imageHash;
@end @end
@ -26,26 +28,24 @@ NS_ASSUME_NONNULL_BEGIN
@synthesize comparableNameFirstLast = _comparableNameFirstLast; @synthesize comparableNameFirstLast = _comparableNameFirstLast;
@synthesize comparableNameLastFirst = _comparableNameLastFirst; @synthesize comparableNameLastFirst = _comparableNameLastFirst;
@synthesize image = _image;
#if TARGET_OS_IOS #if TARGET_OS_IOS
- (instancetype)initWithSystemContact:(CNContact *)contact - (instancetype)initWithSystemContact:(CNContact *)cnContact
{ {
self = [super init]; self = [super init];
if (!self) { if (!self) {
return self; return self;
} }
_cnContact = contact; _cnContactId = cnContact.identifier;
_firstName = contact.givenName.ows_stripped; _firstName = cnContact.givenName.ows_stripped;
_lastName = contact.familyName.ows_stripped; _lastName = cnContact.familyName.ows_stripped;
_fullName = [Contact formattedFullNameWithCNContact:contact]; _fullName = [Contact formattedFullNameWithCNContact:cnContact];
_uniqueId = contact.identifier;
NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new]; NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new];
NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap = [NSMutableDictionary new]; NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap = [NSMutableDictionary new];
for (CNLabeledValue *phoneNumberField in contact.phoneNumbers) { for (CNLabeledValue *phoneNumberField in cnContact.phoneNumbers) {
if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) { if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) {
CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value; CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value;
[phoneNumbers addObject:phoneNumber.stringValue]; [phoneNumbers addObject:phoneNumber.stringValue];
@ -97,23 +97,31 @@ NS_ASSUME_NONNULL_BEGIN
[self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap]; [self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap];
NSMutableArray<NSString *> *emailAddresses = [NSMutableArray new]; NSMutableArray<NSString *> *emailAddresses = [NSMutableArray new];
for (CNLabeledValue *emailField in contact.emailAddresses) { for (CNLabeledValue *emailField in cnContact.emailAddresses) {
if ([emailField.value isKindOfClass:[NSString class]]) { if ([emailField.value isKindOfClass:[NSString class]]) {
[emailAddresses addObject:(NSString *)emailField.value]; [emailAddresses addObject:(NSString *)emailField.value];
} }
} }
_emails = [emailAddresses copy]; _emails = [emailAddresses copy];
if (contact.thumbnailImageData) { NSData *_Nullable avatarData = [Contact avatarDataForCNContact:cnContact];
_imageData = [contact.thumbnailImageData copy]; if (avatarData) {
} else if (contact.imageData) { NSUInteger hashValue = 0;
// This only occurs when sharing a contact via the share extension NSData *hashData = [Cryptography computeSHA256Digest:avatarData truncatedToBytes:sizeof(hashValue)];
_imageData = [contact.imageData copy]; [hashData getBytes:&hashValue length:sizeof(hashValue)];
_imageHash = hashValue;
} else {
_imageHash = 0;
} }
return self; return self;
} }
- (NSString *)uniqueId
{
return self.cnContactId;
}
+ (nullable Contact *)contactWithVCardData:(NSData *)data + (nullable Contact *)contactWithVCardData:(NSData *)data
{ {
CNContact *_Nullable cnContact = [self cnContactWithVCardData:data]; CNContact *_Nullable cnContact = [self cnContactWithVCardData:data];
@ -125,29 +133,6 @@ NS_ASSUME_NONNULL_BEGIN
return [[self alloc] initWithSystemContact:cnContact]; return [[self alloc] initWithSystemContact:cnContact];
} }
- (nullable UIImage *)image
{
if (_image) {
return _image;
}
if (!self.imageData) {
return nil;
}
_image = [UIImage imageWithData:self.imageData];
return _image;
}
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
{
if ([propertyKey isEqualToString:@"cnContact"] || [propertyKey isEqualToString:@"image"]) {
return MTLPropertyStorageTransitory;
} else {
return [super storageBehaviorForPropertyWithKey:propertyKey];
}
}
#endif // TARGET_OS_IOS #endif // TARGET_OS_IOS
- (NSArray<PhoneNumber *> *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray<NSString *> *)userTextPhoneNumbers - (NSArray<PhoneNumber *> *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray<NSString *> *)userTextPhoneNumbers
@ -278,6 +263,20 @@ NS_ASSUME_NONNULL_BEGIN
return value; return value;
} }
+ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact
{
if (cnContact.thumbnailImageData) {
return cnContact.thumbnailImageData.copy;
} else if (cnContact.imageData) {
// This only occurs when sharing a contact via the share extension
return cnContact.imageData.copy;
} else {
return nil;
}
}
// This method is used to de-bounce system contact fetch notifications
// by checking for changes in the contact data.
- (NSUInteger)hash - (NSUInteger)hash
{ {
// base hash is some arbitrary number // base hash is some arbitrary number
@ -285,13 +284,7 @@ NS_ASSUME_NONNULL_BEGIN
hash = hash ^ self.fullName.hash; hash = hash ^ self.fullName.hash;
if (self.imageData) { hash = hash ^ self.imageHash;
NSUInteger thumbnailHash = 0;
NSData *thumbnailHashData =
[Cryptography computeSHA256Digest:self.imageData truncatedToBytes:sizeof(thumbnailHash)];
[thumbnailHashData getBytes:&thumbnailHash length:sizeof(thumbnailHash)];
hash = hash ^ thumbnailHash;
}
for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) { for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) {
hash = hash ^ phoneNumber.toE164.hash; hash = hash ^ phoneNumber.toE164.hash;
@ -326,9 +319,14 @@ NS_ASSUME_NONNULL_BEGIN
return contacts.firstObject; 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) { if (!mergedCNContact) {
OWSFail(@"%@ in %s mergedCNContact was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__); OWSFail(@"%@ in %s mergedCNContact was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__);
return [CNContact new]; return [CNContact new];
@ -351,8 +349,8 @@ NS_ASSUME_NONNULL_BEGIN
} }
// Phone Numbers // Phone Numbers
NSSet<PhoneNumber *> *existingParsedPhoneNumberSet = [NSSet setWithArray:self.parsedPhoneNumbers]; NSSet<PhoneNumber *> *existingParsedPhoneNumberSet = [NSSet setWithArray:oldContact.parsedPhoneNumbers];
NSSet<NSString *> *existingUnparsedPhoneNumberSet = [NSSet setWithArray:self.userTextPhoneNumbers]; NSSet<NSString *> *existingUnparsedPhoneNumberSet = [NSSet setWithArray:oldContact.userTextPhoneNumbers];
NSMutableArray<CNLabeledValue<CNPhoneNumber *> *> *mergedPhoneNumbers = [mergedCNContact.phoneNumbers mutableCopy]; NSMutableArray<CNLabeledValue<CNPhoneNumber *> *> *mergedPhoneNumbers = [mergedCNContact.phoneNumbers mutableCopy];
for (CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber in newCNContact.phoneNumbers) { for (CNLabeledValue<CNPhoneNumber *> *labeledPhoneNumber in newCNContact.phoneNumbers) {
@ -371,7 +369,7 @@ NS_ASSUME_NONNULL_BEGIN
mergedCNContact.phoneNumbers = mergedPhoneNumbers; mergedCNContact.phoneNumbers = mergedPhoneNumbers;
// Emails // Emails
NSSet<NSString *> *existingEmailSet = [NSSet setWithArray:self.emails]; NSSet<NSString *> *existingEmailSet = [NSSet setWithArray:oldContact.emails];
NSMutableArray<CNLabeledValue<NSString *> *> *mergedEmailAddresses = [mergedCNContact.emailAddresses mutableCopy]; NSMutableArray<CNLabeledValue<NSString *> *> *mergedEmailAddresses = [mergedCNContact.emailAddresses mutableCopy];
for (CNLabeledValue<NSString *> *labeledEmail in newCNContact.emailAddresses) { for (CNLabeledValue<NSString *> *labeledEmail in newCNContact.emailAddresses) {
NSString *normalizedValue = labeledEmail.value.ows_stripped; NSString *normalizedValue = labeledEmail.value.ows_stripped;

@ -1,19 +1,22 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "OWSChunkedOutputStream.h" #import "OWSChunkedOutputStream.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class SignalAccount;
@class OWSRecipientIdentity; @class OWSRecipientIdentity;
@class SignalAccount;
@protocol ContactsManagerProtocol;
@interface OWSContactsOutputStream : OWSChunkedOutputStream @interface OWSContactsOutputStream : OWSChunkedOutputStream
- (void)writeSignalAccount:(SignalAccount *)signalAccount - (void)writeSignalAccount:(SignalAccount *)signalAccount
recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity
profileKeyData:(nullable NSData *)profileKeyData; profileKeyData:(nullable NSData *)profileKeyData
contactsManager:(id<ContactsManagerProtocol>)contactsManager;
@end @end

@ -4,6 +4,7 @@
#import "OWSContactsOutputStream.h" #import "OWSContactsOutputStream.h"
#import "Contact.h" #import "Contact.h"
#import "ContactsManagerProtocol.h"
#import "Cryptography.h" #import "Cryptography.h"
#import "MIMETypeUtil.h" #import "MIMETypeUtil.h"
#import "NSData+keyVersionByte.h" #import "NSData+keyVersionByte.h"
@ -22,9 +23,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)writeSignalAccount:(SignalAccount *)signalAccount - (void)writeSignalAccount:(SignalAccount *)signalAccount
recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity
profileKeyData:(nullable NSData *)profileKeyData profileKeyData:(nullable NSData *)profileKeyData
contactsManager:(id<ContactsManagerProtocol>)contactsManager
{ {
OWSAssert(signalAccount); OWSAssert(signalAccount);
OWSAssert(signalAccount.contact); OWSAssert(signalAccount.contact);
OWSAssert(contactsManager);
OWSSignalServiceProtosContactDetailsBuilder *contactBuilder = [OWSSignalServiceProtosContactDetailsBuilder new]; OWSSignalServiceProtosContactDetailsBuilder *contactBuilder = [OWSSignalServiceProtosContactDetailsBuilder new];
[contactBuilder setName:signalAccount.contact.fullName]; [contactBuilder setName:signalAccount.contact.fullName];
@ -38,16 +41,19 @@ NS_ASSUME_NONNULL_BEGIN
contactBuilder.verifiedBuilder = verifiedBuilder; contactBuilder.verifiedBuilder = verifiedBuilder;
} }
NSData *avatarPng; UIImage *_Nullable rawAvatar = [contactsManager avatarImageForCNContactId:signalAccount.contact.cnContactId];
if (signalAccount.contact.image) { NSData *_Nullable avatarPng;
if (rawAvatar) {
avatarPng = UIImagePNGRepresentation(rawAvatar);
if (avatarPng) {
OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder = OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder =
[OWSSignalServiceProtosContactDetailsAvatarBuilder new]; [OWSSignalServiceProtosContactDetailsAvatarBuilder new];
[avatarBuilder setContentType:OWSMimeTypeImagePng]; [avatarBuilder setContentType:OWSMimeTypeImagePng];
avatarPng = UIImagePNGRepresentation(signalAccount.contact.image);
[avatarBuilder setLength:(uint32_t)avatarPng.length]; [avatarBuilder setLength:(uint32_t)avatarPng.length];
[contactBuilder setAvatarBuilder:avatarBuilder]; [contactBuilder setAvatarBuilder:avatarBuilder];
} }
}
if (profileKeyData) { if (profileKeyData) {
OWSAssert(profileKeyData.length == kAES256_KeyByteLength); OWSAssert(profileKeyData.length == kAES256_KeyByteLength);
@ -79,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.delegateStream writeRawVarint32:contactDataLength]; [self.delegateStream writeRawVarint32:contactDataLength];
[self.delegateStream writeRawData:contactData]; [self.delegateStream writeRawData:contactData];
if (signalAccount.contact.image) { if (avatarPng) {
[self.delegateStream writeRawData:avatarPng]; [self.delegateStream writeRawData:avatarPng];
} }
} }

@ -13,6 +13,7 @@
#import "SignalAccount.h" #import "SignalAccount.h"
#import "TSAttachment.h" #import "TSAttachment.h"
#import "TSAttachmentStream.h" #import "TSAttachmentStream.h"
#import "TextSecureKitEnv.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -71,6 +72,8 @@ NS_ASSUME_NONNULL_BEGIN
- (NSData *)buildPlainTextAttachmentData - (NSData *)buildPlainTextAttachmentData
{ {
id<ContactsManagerProtocol> contactsManager = TextSecureKitEnv.sharedEnv.contactsManager;
// TODO use temp file stream to avoid loading everything into memory at once // TODO use temp file stream to avoid loading everything into memory at once
// First though, we need to re-engineer our attachment process to accept streams (encrypting with stream, // First though, we need to re-engineer our attachment process to accept streams (encrypting with stream,
// and uploading with streams). // and uploading with streams).
@ -85,7 +88,8 @@ NS_ASSUME_NONNULL_BEGIN
[contactsOutputStream writeSignalAccount:signalAccount [contactsOutputStream writeSignalAccount:signalAccount
recipientIdentity:recipientIdentity recipientIdentity:recipientIdentity
profileKeyData:profileKeyData]; profileKeyData:profileKeyData
contactsManager:contactsManager];
} }
[contactsOutputStream flush]; [contactsOutputStream flush];

@ -4,6 +4,7 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class CNContact;
@class Contact; @class Contact;
@class PhoneNumber; @class PhoneNumber;
@class SignalAccount; @class SignalAccount;
@ -20,6 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left - (NSComparisonResult)compareSignalAccount:(SignalAccount *)left
withSignalAccount:(SignalAccount *)right NS_SWIFT_NAME(compare(signalAccount:with:)); withSignalAccount:(SignalAccount *)right NS_SWIFT_NAME(compare(signalAccount:with:));
#pragma mark - CNContacts
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId;
- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId;
- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

Loading…
Cancel
Save