diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 7ff1f1805..2926a492d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -5134,7 +5134,7 @@ interactionControllerForAnimationController:(id 0); @@ -158,6 +162,38 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification 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 = (UIImage * _Nullable)[self.cnContactAvatarCache getWithKey:contactId]; + if (!avatarImage) { + NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId]; + if (avatarData) { + avatarImage = [UIImage imageWithData:avatarData]; + } + if (avatarImage) { + [self.cnContactAvatarCache setWithKey:contactId value:avatarImage]; + } + } + } + + return avatarImage; +} + #pragma mark - SystemContactsFetcherDelegate - (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher @@ -257,6 +293,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification dispatch_async(dispatch_get_main_queue(), ^{ self.allContacts = contacts; self.allContactsMap = [allContactsMap copy]; + [self.cnContactCache clear]; + [self.cnContactAvatarCache clear]; [self.avatarCache removeAllImages]; @@ -791,7 +829,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification contact = [self signalAccountForRecipientId:identifier].contact; } - return contact.image; + return [self avatarImageForCNContactId:contact.cnContactId]; } - (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier diff --git a/SignalMessaging/utils/LRUCache.swift b/SignalMessaging/utils/LRUCache.swift index 4321d6845..9a252dcee 100644 --- a/SignalMessaging/utils/LRUCache.swift +++ b/SignalMessaging/utils/LRUCache.swift @@ -5,7 +5,7 @@ @objc public class AnyLRUCache: NSObject { - let backingCache: LRUCache + private let backingCache: LRUCache @objc public init(maxSize: Int) { @@ -21,6 +21,11 @@ public class AnyLRUCache: NSObject { public func set(key: NSObject, value: NSObject) { self.backingCache.set(key: key, value: value) } + + @objc + public func clear() { + self.backingCache.clear() + } } // A simple LRU cache bounded by the number of entries. @@ -47,8 +52,7 @@ public class LRUCache { @objc func didReceiveMemoryWarning() { SwiftAssertIsOnMainThread(#function) - cacheMap.removeAll() - cacheOrder.removeAll() + clear() } private func updateCacheOrder(key: KeyType) { @@ -82,4 +86,10 @@ public class LRUCache { cacheMap.removeValue(forKey: staleKey) } } + + @objc + public func clear() { + cacheMap.removeAll() + cacheOrder.removeAll() + } } diff --git a/SignalServiceKit/src/Contacts/Contact.h b/SignalServiceKit/src/Contacts/Contact.h index d46624548..1663fd8ee 100644 --- a/SignalServiceKit/src/Contacts/Contact.h +++ b/SignalServiceKit/src/Contacts/Contact.h @@ -28,10 +28,6 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) NSArray *emails; @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; -#endif // TARGET_OS_IOS - (NSArray *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction; // TODO: Remove this method. @@ -39,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN #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 CNContact *)cnContactWithVCardData:(NSData *)data; @@ -54,6 +50,8 @@ NS_ASSUME_NONNULL_BEGIN + (CNContact *)mergeCNContact:(CNContact *)oldCNContact newCNContact:(CNContact *)newCNContact NS_SWIFT_NAME(merge(cnContact:newCNContact:)); ++ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Contacts/Contact.m b/SignalServiceKit/src/Contacts/Contact.m index be582d401..e079d2afa 100644 --- a/SignalServiceKit/src/Contacts/Contact.m +++ b/SignalServiceKit/src/Contacts/Contact.m @@ -9,6 +9,7 @@ #import "PhoneNumber.h" #import "SignalRecipient.h" #import "TSAccountManager.h" +#import "TextSecureKitEnv.h" @import Contacts; @@ -17,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @interface Contact () @property (nonatomic, readonly) NSMutableDictionary *phoneNumberNameMap; +@property (nonatomic, readonly) NSUInteger imageHash; @end @@ -26,25 +28,24 @@ NS_ASSUME_NONNULL_BEGIN @synthesize comparableNameFirstLast = _comparableNameFirstLast; @synthesize comparableNameLastFirst = _comparableNameLastFirst; -@synthesize image = _image; #if TARGET_OS_IOS -- (instancetype)initWithSystemContact:(CNContact *)contact +- (instancetype)initWithSystemContact:(CNContact *)cnContact { self = [super init]; if (!self) { return self; } - _cnContactId = contact.identifier; - _firstName = contact.givenName.ows_stripped; - _lastName = contact.familyName.ows_stripped; - _fullName = [Contact formattedFullNameWithCNContact:contact]; + _cnContactId = cnContact.identifier; + _firstName = cnContact.givenName.ows_stripped; + _lastName = cnContact.familyName.ows_stripped; + _fullName = [Contact formattedFullNameWithCNContact:cnContact]; NSMutableArray *phoneNumbers = [NSMutableArray new]; NSMutableDictionary *phoneNumberNameMap = [NSMutableDictionary new]; - for (CNLabeledValue *phoneNumberField in contact.phoneNumbers) { + for (CNLabeledValue *phoneNumberField in cnContact.phoneNumbers) { if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) { CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value; [phoneNumbers addObject:phoneNumber.stringValue]; @@ -96,18 +97,21 @@ NS_ASSUME_NONNULL_BEGIN [self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap]; NSMutableArray *emailAddresses = [NSMutableArray new]; - for (CNLabeledValue *emailField in contact.emailAddresses) { + for (CNLabeledValue *emailField in cnContact.emailAddresses) { if ([emailField.value isKindOfClass:[NSString class]]) { [emailAddresses addObject:(NSString *)emailField.value]; } } _emails = [emailAddresses copy]; - if (contact.thumbnailImageData) { - _imageData = [contact.thumbnailImageData copy]; - } else if (contact.imageData) { - // This only occurs when sharing a contact via the share extension - _imageData = [contact.imageData copy]; + NSData *_Nullable avatarData = [Contact avatarDataForCNContact:cnContact]; + if (avatarData) { + NSUInteger hashValue = 0; + NSData *hashData = [Cryptography computeSHA256Digest:avatarData truncatedToBytes:sizeof(hashValue)]; + [hashData getBytes:&hashValue length:sizeof(hashValue)]; + _imageHash = hashValue; + } else { + _imageHash = 0; } return self; @@ -124,29 +128,6 @@ NS_ASSUME_NONNULL_BEGIN 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 - (NSArray *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray *)userTextPhoneNumbers @@ -277,6 +258,20 @@ NS_ASSUME_NONNULL_BEGIN 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 { // base hash is some arbitrary number @@ -284,13 +279,7 @@ NS_ASSUME_NONNULL_BEGIN hash = hash ^ self.fullName.hash; - if (self.imageData) { - NSUInteger thumbnailHash = 0; - NSData *thumbnailHashData = - [Cryptography computeSHA256Digest:self.imageData truncatedToBytes:sizeof(thumbnailHash)]; - [thumbnailHashData getBytes:&thumbnailHash length:sizeof(thumbnailHash)]; - hash = hash ^ thumbnailHash; - } + hash = hash ^ self.imageHash; for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) { hash = hash ^ phoneNumber.toE164.hash; diff --git a/SignalServiceKit/src/Devices/OWSContactsOutputStream.h b/SignalServiceKit/src/Devices/OWSContactsOutputStream.h index 4f59766c7..1f9734102 100644 --- a/SignalServiceKit/src/Devices/OWSContactsOutputStream.h +++ b/SignalServiceKit/src/Devices/OWSContactsOutputStream.h @@ -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" NS_ASSUME_NONNULL_BEGIN -@class SignalAccount; @class OWSRecipientIdentity; +@class SignalAccount; + +@protocol ContactsManagerProtocol; @interface OWSContactsOutputStream : OWSChunkedOutputStream - (void)writeSignalAccount:(SignalAccount *)signalAccount recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity - profileKeyData:(nullable NSData *)profileKeyData; + profileKeyData:(nullable NSData *)profileKeyData + contactsManager:(id)contactsManager; @end diff --git a/SignalServiceKit/src/Devices/OWSContactsOutputStream.m b/SignalServiceKit/src/Devices/OWSContactsOutputStream.m index 50470f7d2..6b66f838a 100644 --- a/SignalServiceKit/src/Devices/OWSContactsOutputStream.m +++ b/SignalServiceKit/src/Devices/OWSContactsOutputStream.m @@ -4,6 +4,7 @@ #import "OWSContactsOutputStream.h" #import "Contact.h" +#import "ContactsManagerProtocol.h" #import "Cryptography.h" #import "MIMETypeUtil.h" #import "NSData+keyVersionByte.h" @@ -22,9 +23,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)writeSignalAccount:(SignalAccount *)signalAccount recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity profileKeyData:(nullable NSData *)profileKeyData + contactsManager:(id)contactsManager { OWSAssert(signalAccount); OWSAssert(signalAccount.contact); + OWSAssert(contactsManager); OWSSignalServiceProtosContactDetailsBuilder *contactBuilder = [OWSSignalServiceProtosContactDetailsBuilder new]; [contactBuilder setName:signalAccount.contact.fullName]; @@ -38,15 +41,18 @@ NS_ASSUME_NONNULL_BEGIN contactBuilder.verifiedBuilder = verifiedBuilder; } - NSData *avatarPng; - if (signalAccount.contact.image) { - OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder = - [OWSSignalServiceProtosContactDetailsAvatarBuilder new]; - - [avatarBuilder setContentType:OWSMimeTypeImagePng]; - avatarPng = UIImagePNGRepresentation(signalAccount.contact.image); - [avatarBuilder setLength:(uint32_t)avatarPng.length]; - [contactBuilder setAvatarBuilder:avatarBuilder]; + UIImage *_Nullable rawAvatar = [contactsManager avatarImageForCNContactId:signalAccount.contact.cnContactId]; + NSData *_Nullable avatarPng; + if (rawAvatar) { + avatarPng = UIImagePNGRepresentation(rawAvatar); + if (avatarPng) { + OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder = + [OWSSignalServiceProtosContactDetailsAvatarBuilder new]; + + [avatarBuilder setContentType:OWSMimeTypeImagePng]; + [avatarBuilder setLength:(uint32_t)avatarPng.length]; + [contactBuilder setAvatarBuilder:avatarBuilder]; + } } if (profileKeyData) { @@ -79,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN [self.delegateStream writeRawVarint32:contactDataLength]; [self.delegateStream writeRawData:contactData]; - if (signalAccount.contact.image) { + if (avatarPng) { [self.delegateStream writeRawData:avatarPng]; } } diff --git a/SignalServiceKit/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m b/SignalServiceKit/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m index 05d07f93b..0612d2bc1 100644 --- a/SignalServiceKit/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m +++ b/SignalServiceKit/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m @@ -13,6 +13,7 @@ #import "SignalAccount.h" #import "TSAttachment.h" #import "TSAttachmentStream.h" +#import "TextSecureKitEnv.h" NS_ASSUME_NONNULL_BEGIN @@ -71,6 +72,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSData *)buildPlainTextAttachmentData { + id contactsManager = TextSecureKitEnv.sharedEnv.contactsManager; + // 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, // and uploading with streams). @@ -82,10 +85,11 @@ NS_ASSUME_NONNULL_BEGIN OWSRecipientIdentity *_Nullable recipientIdentity = [self.identityManager recipientIdentityForRecipientId:signalAccount.recipientId]; NSData *_Nullable profileKeyData = [self.profileManager profileKeyDataForRecipientId:signalAccount.recipientId]; - + [contactsOutputStream writeSignalAccount:signalAccount recipientIdentity:recipientIdentity - profileKeyData:profileKeyData]; + profileKeyData:profileKeyData + contactsManager:contactsManager]; } [contactsOutputStream flush]; diff --git a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h index 7a6e5cc69..4954610d5 100644 --- a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h @@ -4,6 +4,7 @@ NS_ASSUME_NONNULL_BEGIN +@class CNContact; @class Contact; @class PhoneNumber; @class SignalAccount; @@ -20,6 +21,12 @@ NS_ASSUME_NONNULL_BEGIN - (NSComparisonResult)compareSignalAccount:(SignalAccount *)left 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 NS_ASSUME_NONNULL_END