Lazy-load contact avatar data and images. Use NSCache for avatar images.
This commit is contained in:
parent
af977ca409
commit
08ca4fdb50
@ -5134,7 +5134,7 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
|
||||
}
|
||||
|
||||
BOOL isProfileAvatar = NO;
|
||||
NSData *_Nullable avatarImageData = contact.imageData;
|
||||
NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:cnContact.identifier];
|
||||
for (NSString *recipientId in contact.textSecureIdentifiers) {
|
||||
if (avatarImageData) {
|
||||
break;
|
||||
|
||||
@ -78,7 +78,7 @@ class ContactCell: UITableViewCell {
|
||||
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
|
||||
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
||||
|
||||
if let contactImage = contact.image {
|
||||
if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
|
||||
contactImageView.image = contactImage
|
||||
} else {
|
||||
let contactIdForDeterminingBackgroundColor: String
|
||||
|
||||
@ -187,7 +187,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
|
||||
}
|
||||
|
||||
BOOL isProfileAvatar = NO;
|
||||
NSData *_Nullable avatarImageData = contact.imageData;
|
||||
NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:contact.cnContactId];
|
||||
for (NSString *recipientId in contact.textSecureIdentifiers) {
|
||||
if (avatarImageData) {
|
||||
break;
|
||||
|
||||
@ -59,8 +59,6 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
|
||||
// contacts haven't changed, and will clear out any stale cached SignalAccounts
|
||||
- (void)userRequestedSystemContactsRefreshWithCompletion:(void (^)(NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId;
|
||||
|
||||
#pragma mark - Util
|
||||
|
||||
- (BOOL)isSystemContact:(NSString *)recipientId;
|
||||
|
||||
@ -37,6 +37,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbReadConnection;
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbWriteConnection;
|
||||
@property (nonatomic, readonly) AnyLRUCache *cnContactCache;
|
||||
@property (nonatomic, readonly) AnyLRUCache *cnContactAvatarCache;
|
||||
|
||||
@end
|
||||
|
||||
@ -62,6 +63,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
_systemContactsFetcher = [SystemContactsFetcher new];
|
||||
_systemContactsFetcher.delegate = self;
|
||||
_cnContactCache = [[AnyLRUCache alloc] initWithMaxSize:50];
|
||||
_cnContactAvatarCache = [[AnyLRUCache alloc] initWithMaxSize:25];
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
@ -135,6 +137,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
return self.systemContactsFetcher.supportsContactEditing;
|
||||
}
|
||||
|
||||
#pragma mark - CNContacts
|
||||
|
||||
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId
|
||||
{
|
||||
OWSAssert(contactId.length > 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
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
@objc
|
||||
public class AnyLRUCache: NSObject {
|
||||
|
||||
let backingCache: LRUCache<NSObject, NSObject>
|
||||
private let backingCache: LRUCache<NSObject, NSObject>
|
||||
|
||||
@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<KeyType: Hashable & Equatable, ValueType> {
|
||||
@objc func didReceiveMemoryWarning() {
|
||||
SwiftAssertIsOnMainThread(#function)
|
||||
|
||||
cacheMap.removeAll()
|
||||
cacheOrder.removeAll()
|
||||
clear()
|
||||
}
|
||||
|
||||
private func updateCacheOrder(key: KeyType) {
|
||||
@ -82,4 +86,10 @@ public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
|
||||
cacheMap.removeValue(forKey: staleKey)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func clear() {
|
||||
cacheMap.removeAll()
|
||||
cacheOrder.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (readonly, nonatomic) NSArray<NSString *> *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<SignalRecipient *> *)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
|
||||
|
||||
@ -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<NSString *, NSString *> *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<NSString *> *phoneNumbers = [NSMutableArray 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]]) {
|
||||
CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value;
|
||||
[phoneNumbers addObject:phoneNumber.stringValue];
|
||||
@ -96,18 +97,21 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap];
|
||||
|
||||
NSMutableArray<NSString *> *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<PhoneNumber *> *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray<NSString *> *)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;
|
||||
|
||||
@ -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<ContactsManagerProtocol>)contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@ -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<ContactsManagerProtocol>)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];
|
||||
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];
|
||||
avatarPng = UIImagePNGRepresentation(signalAccount.contact.image);
|
||||
[avatarBuilder setLength:(uint32_t)avatarPng.length];
|
||||
[contactBuilder setAvatarBuilder:avatarBuilder];
|
||||
[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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ContactsManagerProtocol> 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];
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user