Rework concurrency in the profile manager.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent 8dce481ea1
commit 02f8b13f4f

@ -20,18 +20,14 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
// UserProfile properties should only be mutated on the main thread. // UserProfile properties may be read from any thread, but should
// only be mutated when synchronized on the profile manager.
@interface UserProfile : TSYapDatabaseObject @interface UserProfile : TSYapDatabaseObject
// These properties may be accessed from any thread.
@property (atomic, readonly) NSString *recipientId; @property (atomic, readonly) NSString *recipientId;
@property (atomic, nullable) OWSAES128Key *profileKey; @property (atomic, nullable) OWSAES128Key *profileKey;
// These properties may be accessed only from the main thread.
@property (nonatomic, nullable) NSString *profileName; @property (nonatomic, nullable) NSString *profileName;
@property (nonatomic, nullable) NSString *avatarUrlPath; @property (nonatomic, nullable) NSString *avatarUrlPath;
// This filename is relative to OWSProfileManager.profileAvatarsDirPath. // This filename is relative to OWSProfileManager.profileAvatarsDirPath.
@property (nonatomic, nullable) NSString *avatarFileName; @property (nonatomic, nullable) NSString *avatarFileName;
@ -39,8 +35,6 @@ NS_ASSUME_NONNULL_BEGIN
// //
// * The last successful update finished. // * The last successful update finished.
// * The current in-flight update began. // * The current in-flight update began.
//
// This property may be accessed from any thread.
@property (nonatomic, nullable) NSDate *lastUpdateDate; @property (nonatomic, nullable) NSDate *lastUpdateDate;
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
@ -102,26 +96,26 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; @property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) TSNetworkManager *networkManager; @property (nonatomic, readonly) TSNetworkManager *networkManager;
// These properties can be accessed on any thread, while synchronized on self.
@property (atomic, nullable) UserProfile *localUserProfile; @property (atomic, nullable) UserProfile *localUserProfile;
// This property should only be mutated on the main thread, @property (atomic, nullable) UIImage *localCachedAvatarImage;
@property (nonatomic, nullable) UIImage *localCachedAvatarImage;
// These caches are lazy-populated. The single point of truth is the database. // These caches are lazy-populated. The single point of truth is the database.
// //
// These three properties can be accessed on any thread. // These properties can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) NSMutableDictionary<NSString *, NSNumber *> *userProfileWhitelistCache; @property (atomic, readonly) NSMutableDictionary<NSString *, NSNumber *> *userProfileWhitelistCache;
@property (atomic, readonly) NSMutableDictionary<NSString *, NSNumber *> *groupProfileWhitelistCache; @property (atomic, readonly) NSMutableDictionary<NSString *, NSNumber *> *groupProfileWhitelistCache;
// This property should only be mutated on the main thread, // These properties can be accessed on any thread, while synchronized on self.
@property (nonatomic, readonly) NSCache<NSString *, UIImage *> *otherUsersProfileAvatarImageCache; @property (atomic, readonly) NSCache<NSString *, UIImage *> *otherUsersProfileAvatarImageCache;
// This property should only be mutated on the main thread,
@property (atomic, readonly) NSMutableSet<NSString *> *currentAvatarDownloads; @property (atomic, readonly) NSMutableSet<NSString *> *currentAvatarDownloads;
@end @end
#pragma mark - #pragma mark -
// Access to most state should happen while synchronized on the profile manager.
// Writes should happen off the main thread, wherever possible.
@implementation OWSProfileManager @implementation OWSProfileManager
+ (instancetype)sharedManager + (instancetype)sharedManager
@ -226,25 +220,31 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
return instance; return instance;
} }
// All writes to user profiles should occur on the main thread.
- (void)saveUserProfile:(UserProfile *)userProfile - (void)saveUserProfile:(UserProfile *)userProfile
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(userProfile); OWSAssert(userProfile);
// Make sure to save on the local db connection for consistency. @synchronized(self)
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { {
[userProfile saveWithTransaction:transaction]; // Make sure to save on the local db connection for consistency.
}]; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[userProfile saveWithTransaction:transaction];
}];
if (userProfile == self.localUserProfile) { BOOL isLocalUserProfile = userProfile == self.localUserProfile;
[[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_LocalProfileDidChange
object:nil dispatch_async(dispatch_get_main_queue(), ^{
userInfo:nil]; if (isLocalUserProfile) {
} else { [[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_LocalProfileDidChange
[[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_OtherUsersProfileDidChange object:nil
object:nil userInfo:nil];
userInfo:nil]; } else {
[[NSNotificationCenter defaultCenter]
postNotificationName:kNSNotificationName_OtherUsersProfileDidChange
object:nil
userInfo:nil];
}
});
} }
} }
@ -252,124 +252,138 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (OWSAES128Key *)localProfileKey - (OWSAES128Key *)localProfileKey
{ {
OWSAssert(self.localUserProfile.profileKey.keyData.length == kAES128_KeyByteLength); @synchronized(self)
{
OWSAssert(self.localUserProfile.profileKey.keyData.length == kAES128_KeyByteLength);
return self.localUserProfile.profileKey; return self.localUserProfile.profileKey;
}
} }
- (BOOL)hasLocalProfile - (BOOL)hasLocalProfile
{ {
OWSAssert([NSThread isMainThread]);
return (self.localProfileName.length > 0 || self.localProfileAvatarImage != nil); return (self.localProfileName.length > 0 || self.localProfileAvatarImage != nil);
} }
- (nullable NSString *)localProfileName - (nullable NSString *)localProfileName
{ {
OWSAssert([NSThread isMainThread]); @synchronized(self)
{
return self.localUserProfile.profileName; return self.localUserProfile.profileName;
}
} }
- (nullable UIImage *)localProfileAvatarImage - (nullable UIImage *)localProfileAvatarImage
{ {
OWSAssert([NSThread isMainThread]); @synchronized(self)
{
if (!self.localCachedAvatarImage) { if (!self.localCachedAvatarImage) {
if (self.localUserProfile.avatarFileName) { if (self.localUserProfile.avatarFileName) {
self.localCachedAvatarImage = [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName]; self.localCachedAvatarImage = [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName];
}
} }
}
return self.localCachedAvatarImage; return self.localCachedAvatarImage;
}
} }
- (void)updateLocalProfileName:(nullable NSString *)profileName - (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage avatarImage:(nullable UIImage *)avatarImage
success:(void (^)())successBlock success:(void (^)())successBlockParameter
failure:(void (^)())failureBlockParameter failure:(void (^)())failureBlockParameter
{ {
OWSAssert([NSThread isMainThread]); OWSAssert(successBlockParameter);
OWSAssert(successBlock);
OWSAssert(failureBlockParameter); OWSAssert(failureBlockParameter);
// Ensure that the failure block is called on the main thread. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
void (^failureBlock)() = ^{ @synchronized(self)
dispatch_async(dispatch_get_main_queue(), ^{ {
failureBlockParameter(); // Ensure that the success and failure blocks are called on the main thread.
}); void (^failureBlock)() = ^{
};
// The final steps are to:
//
// * Try to update the service.
// * Update client state on success.
void (^tryToUpdateService)(NSString *_Nullable, NSString *_Nullable) = ^(
NSString *_Nullable avatarUrlPath, NSString *_Nullable avatarFileName) {
[self updateServiceWithProfileName:profileName
success:^{
// All reads and writes to user profiles should happen on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
UserProfile *userProfile = self.localUserProfile; failureBlockParameter();
OWSAssert(userProfile);
userProfile.profileName = profileName;
// TODO remote avatarUrlPath changes as result of fetching form -
// we should probably invalidate it at that point, and refresh again when uploading file completes.
userProfile.avatarUrlPath = avatarUrlPath;
userProfile.avatarFileName = avatarFileName;
[self saveUserProfile:userProfile];
self.localCachedAvatarImage = avatarImage;
successBlock();
}); });
} };
failure:^{ void (^successBlock)() = ^{
failureBlock(); dispatch_async(dispatch_get_main_queue(), ^{
}]; successBlockParameter();
}; });
};
UserProfile *userProfile = self.localUserProfile;
OWSAssert(userProfile); // The final steps are to:
//
if (avatarImage) { // * Try to update the service.
// * Update client state on success.
// If we have a new avatar image, we must first: void (^tryToUpdateService)(NSString *_Nullable, NSString *_Nullable)
// = ^(NSString *_Nullable avatarUrlPath, NSString *_Nullable avatarFileName) {
// * Encode it to JPEG. [self updateServiceWithProfileName:profileName
// * Write it to disk. success:^{
// * Encrypt it dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// * Upload it to asset service @synchronized(self)
// * Send asset service info to Signal Service {
if (self.localCachedAvatarImage == avatarImage) { UserProfile *userProfile = self.localUserProfile;
OWSAssert(userProfile.avatarUrlPath.length > 0); OWSAssert(userProfile);
OWSAssert(userProfile.avatarFileName.length > 0); userProfile.profileName = profileName;
DDLogVerbose(@"%@ Updating local profile on service with unchanged avatar.", self.tag); // TODO remote avatarUrlPath changes as result of fetching form -
// If the avatar hasn't changed, reuse the existing metadata. // we should probably invalidate it at that point, and refresh again when
tryToUpdateService(userProfile.avatarUrlPath, userProfile.avatarFileName); // uploading file completes.
} else { userProfile.avatarUrlPath = avatarUrlPath;
DDLogVerbose(@"%@ Updating local profile on service with new avatar.", self.tag); userProfile.avatarFileName = avatarFileName;
[self writeAvatarToDisk:avatarImage
success:^(NSData *data, NSString *fileName) { [self saveUserProfile:userProfile];
[self uploadAvatarToService:data
success:^(NSString *avatarUrlPath) { self.localCachedAvatarImage = avatarImage;
tryToUpdateService(avatarUrlPath, fileName);
successBlock();
}
});
}
failure:^{
failureBlock();
}];
};
UserProfile *userProfile = self.localUserProfile;
OWSAssert(userProfile);
if (avatarImage) {
// If we have a new avatar image, we must first:
//
// * Encode it to JPEG.
// * Write it to disk.
// * Encrypt it
// * Upload it to asset service
// * Send asset service info to Signal Service
if (self.localCachedAvatarImage == avatarImage) {
OWSAssert(userProfile.avatarUrlPath.length > 0);
OWSAssert(userProfile.avatarFileName.length > 0);
DDLogVerbose(@"%@ Updating local profile on service with unchanged avatar.", self.tag);
// If the avatar hasn't changed, reuse the existing metadata.
tryToUpdateService(userProfile.avatarUrlPath, userProfile.avatarFileName);
} else {
DDLogVerbose(@"%@ Updating local profile on service with new avatar.", self.tag);
[self writeAvatarToDisk:avatarImage
success:^(NSData *data, NSString *fileName) {
[self uploadAvatarToService:data
success:^(NSString *avatarUrlPath) {
tryToUpdateService(avatarUrlPath, fileName);
}
failure:^{
failureBlock();
}];
} }
failure:^{ failure:^{
failureBlock(); failureBlock();
}]; }];
} }
failure:^{ } else {
failureBlock(); DDLogVerbose(@"%@ Updating local profile on service with no avatar.", self.tag);
}]; tryToUpdateService(nil, nil);
}
} }
} else { });
DDLogVerbose(@"%@ Updating local profile on service with no avatar.", self.tag);
tryToUpdateService(nil, nil);
}
} }
- (void)writeAvatarToDisk:(UIImage *)avatar - (void)writeAvatarToDisk:(UIImage *)avatar
@ -580,58 +594,75 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (void)addUserToProfileWhitelist:(NSString *)recipientId - (void)addUserToProfileWhitelist:(NSString *)recipientId
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
[self.dbConnection setBool:YES forKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.userProfileWhitelistCache[recipientId] = @(YES); @synchronized(self)
{
self.userProfileWhitelistCache[recipientId] = @(YES);
[self.dbConnection setBool:YES forKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection];
}
});
} }
- (void)addUsersToProfileWhitelist:(NSArray<NSString *> *)recipientIds - (void)addUsersToProfileWhitelist:(NSArray<NSString *> *)recipientIds
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientIds); OWSAssert(recipientIds);
NSMutableArray<NSString *> *newRecipientIds = [NSMutableArray new]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSString *recipientId in recipientIds) { @synchronized(self)
if (!self.userProfileWhitelistCache[recipientId]) { {
[newRecipientIds addObject:recipientId]; NSMutableArray<NSString *> *newRecipientIds = [NSMutableArray new];
} for (NSString *recipientId in recipientIds) {
} if (!self.userProfileWhitelistCache[recipientId]) {
[newRecipientIds addObject:recipientId];
}
}
if (newRecipientIds.count < 1) { if (newRecipientIds.count < 1) {
return; return;
} }
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *recipientId in recipientIds) { for (NSString *recipientId in recipientIds) {
[transaction setObject:@(YES) forKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection]; [transaction setObject:@(YES)
self.userProfileWhitelistCache[recipientId] = @(YES); forKey:recipientId
inCollection:kOWSProfileManager_UserWhitelistCollection];
self.userProfileWhitelistCache[recipientId] = @(YES);
}
}];
} }
}]; });
} }
- (BOOL)isUserInProfileWhitelist:(NSString *)recipientId - (BOOL)isUserInProfileWhitelist:(NSString *)recipientId
{ {
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
NSNumber *_Nullable value = self.userProfileWhitelistCache[recipientId]; @synchronized(self)
if (value) { {
NSNumber *_Nullable value = self.userProfileWhitelistCache[recipientId];
if (value) {
return [value boolValue];
}
value =
@([self.dbConnection hasObjectForKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection]);
self.userProfileWhitelistCache[recipientId] = value;
return [value boolValue]; return [value boolValue];
} }
value = @([self.dbConnection hasObjectForKey:recipientId inCollection:kOWSProfileManager_UserWhitelistCollection]);
self.userProfileWhitelistCache[recipientId] = value;
return [value boolValue];
} }
- (void)addGroupIdToProfileWhitelist:(NSData *)groupId - (void)addGroupIdToProfileWhitelist:(NSData *)groupId
{ {
OWSAssert(groupId.length > 0); OWSAssert(groupId.length > 0);
NSString *groupIdKey = [groupId hexadecimalString]; @synchronized(self)
[self.dbConnection setObject:@(1) forKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection]; {
self.groupProfileWhitelistCache[groupIdKey] = @(YES); NSString *groupIdKey = [groupId hexadecimalString];
[self.dbConnection setObject:@(1) forKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection];
self.groupProfileWhitelistCache[groupIdKey] = @(YES);
}
} }
- (void)addThreadToProfileWhitelist:(TSThread *)thread - (void)addThreadToProfileWhitelist:(TSThread *)thread
@ -652,16 +683,19 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
{ {
OWSAssert(groupId.length > 0); OWSAssert(groupId.length > 0);
NSString *groupIdKey = [groupId hexadecimalString]; @synchronized(self)
NSNumber *_Nullable value = self.groupProfileWhitelistCache[groupIdKey]; {
if (value) { NSString *groupIdKey = [groupId hexadecimalString];
NSNumber *_Nullable value = self.groupProfileWhitelistCache[groupIdKey];
if (value) {
return [value boolValue];
}
value = @(nil !=
[self.dbConnection objectForKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection]);
self.groupProfileWhitelistCache[groupIdKey] = value;
return [value boolValue]; return [value boolValue];
} }
value =
@(nil != [self.dbConnection objectForKey:groupIdKey inCollection:kOWSProfileManager_GroupWhitelistCollection]);
self.groupProfileWhitelistCache[groupIdKey] = value;
return [value boolValue];
} }
- (BOOL)isThreadInProfileWhitelist:(TSThread *)thread - (BOOL)isThreadInProfileWhitelist:(TSThread *)thread
@ -680,7 +714,6 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (void)setContactRecipientIds:(NSArray<NSString *> *)contactRecipientIds - (void)setContactRecipientIds:(NSArray<NSString *> *)contactRecipientIds
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(contactRecipientIds); OWSAssert(contactRecipientIds);
// TODO: The persisted whitelist could either be: // TODO: The persisted whitelist could either be:
@ -695,30 +728,33 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId; - (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
{ {
OWSAES128Key *_Nullable profileKey = [OWSAES128Key keyWithData:profileKeyData]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (profileKey == nil) { @synchronized(self)
OWSFail(@"Failed to make profile key for key data"); {
return; OWSAES128Key *_Nullable profileKey = [OWSAES128Key keyWithData:profileKeyData];
} if (profileKey == nil) {
OWSFail(@"Failed to make profile key for key data");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{ UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; OWSAssert(userProfile);
OWSAssert(userProfile); if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) {
if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) { // Ignore redundant update.
// Ignore redundant update. return;
return; }
}
userProfile.profileKey = profileKey; userProfile.profileKey = profileKey;
// Clear profile state. // Clear profile state.
userProfile.profileName = nil; userProfile.profileName = nil;
userProfile.avatarUrlPath = nil; userProfile.avatarUrlPath = nil;
userProfile.avatarFileName = nil; userProfile.avatarFileName = nil;
[self saveUserProfile:userProfile]; [self saveUserProfile:userProfile];
[self refreshProfileForRecipientId:recipientId ignoreThrottling:YES]; [self refreshProfileForRecipientId:recipientId ignoreThrottling:YES];
}
}); });
} }
@ -726,20 +762,26 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
{ {
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; @synchronized(self)
OWSAssert(userProfile); {
return userProfile.profileKey; UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
OWSAssert(userProfile);
return userProfile.profileKey;
}
} }
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId - (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
[self refreshProfileForRecipientId:recipientId]; [self refreshProfileForRecipientId:recipientId];
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; @synchronized(self)
return userProfile.profileName; {
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
return userProfile.profileName;
return self.localUserProfile.profileName;
}
} }
- (nullable NSData *)profileAvatarDataForRecipientId:(NSString *)recipientId - (nullable NSData *)profileAvatarDataForRecipientId:(NSString *)recipientId
@ -753,107 +795,123 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (nullable UIImage *)profileAvatarForRecipientId:(NSString *)recipientId - (nullable UIImage *)profileAvatarForRecipientId:(NSString *)recipientId
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
[self refreshProfileForRecipientId:recipientId]; [self refreshProfileForRecipientId:recipientId];
UIImage *_Nullable image = [self.otherUsersProfileAvatarImageCache objectForKey:recipientId]; @synchronized(self)
if (image) { {
return image; UIImage *_Nullable image = [self.otherUsersProfileAvatarImageCache objectForKey:recipientId];
}
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
if (userProfile.avatarFileName.length > 0) {
image = [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
if (image) { if (image) {
[self.otherUsersProfileAvatarImageCache setObject:image forKey:recipientId]; return image;
} }
} else if (userProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:userProfile];
}
return image; UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
if (userProfile.avatarFileName.length > 0) {
image = [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
if (image) {
[self.otherUsersProfileAvatarImageCache setObject:image forKey:recipientId];
}
} else if (userProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:userProfile];
}
return image;
}
} }
- (void)downloadAvatarForUserProfile:(UserProfile *)userProfile - (void)downloadAvatarForUserProfile:(UserProfile *)userProfile
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(userProfile); OWSAssert(userProfile);
if (userProfile.avatarUrlPath.length < 1) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSFail(@"%@ Malformed avatar URL: %@", self.tag, userProfile.avatarUrlPath); @synchronized(self)
return; {
} if (userProfile.avatarUrlPath.length < 1) {
OWSFail(@"%@ Malformed avatar URL: %@", self.tag, userProfile.avatarUrlPath);
return;
}
NSString *_Nullable avatarUrlPathAtStart = userProfile.avatarUrlPath;
if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrlPath.length < 1) { if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrlPath.length < 1) {
return; return;
} }
OWSAES128Key *profileKeyAtStart = userProfile.profileKey; OWSAES128Key *profileKeyAtStart = userProfile.profileKey;
NSString *fileName = [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"]; NSString *fileName = [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"];
NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName]; NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName];
if ([self.currentAvatarDownloads containsObject:userProfile.recipientId]) { if ([self.currentAvatarDownloads containsObject:userProfile.recipientId]) {
// Download already in flight; ignore. // Download already in flight; ignore.
return; return;
} }
[self.currentAvatarDownloads addObject:userProfile.recipientId]; [self.currentAvatarDownloads addObject:userProfile.recipientId];
NSString *tempDirectory = NSTemporaryDirectory(); NSString *tempDirectory = NSTemporaryDirectory();
NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName]; NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName];
NSURL *avatarUrlPath = [NSURL URLWithString:userProfile.avatarUrlPath relativeToURL:self.avatarHTTPManager.baseURL]; NSURL *avatarUrlPath =
NSURLRequest *request = [NSURLRequest requestWithURL:avatarUrlPath]; [NSURL URLWithString:userProfile.avatarUrlPath relativeToURL:self.avatarHTTPManager.baseURL];
NSURLSessionDownloadTask *downloadTask = [self.avatarHTTPManager downloadTaskWithRequest:request NSURLRequest *request = [NSURLRequest requestWithURL:avatarUrlPath];
progress:^(NSProgress *_Nonnull downloadProgress) { NSURLSessionDownloadTask *downloadTask = [self.avatarHTTPManager downloadTaskWithRequest:request
DDLogVerbose(@"%@ Downloading avatar for %@", self.tag, userProfile.recipientId); progress:^(NSProgress *_Nonnull downloadProgress) {
} DDLogVerbose(@"%@ Downloading avatar for %@", self.tag, userProfile.recipientId);
destination:^NSURL *_Nonnull(NSURL *_Nonnull targetPath, NSURLResponse *_Nonnull response) {
return [NSURL fileURLWithPath:tempFilePath];
}
completionHandler:^(
NSURLResponse *_Nonnull response, NSURL *_Nullable filePathParam, NSError *_Nullable error) {
// Ensure disk IO and decryption occurs off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]);
NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart];
UIImage *_Nullable image = nil;
if (decryptedData) {
BOOL success = [decryptedData writeToFile:filePath atomically:YES];
if (success) {
image = [UIImage imageWithContentsOfFile:filePath];
}
} }
destination:^NSURL *_Nonnull(NSURL *_Nonnull targetPath, NSURLResponse *_Nonnull response) {
return [NSURL fileURLWithPath:tempFilePath];
}
completionHandler:^(
NSURLResponse *_Nonnull response, NSURL *_Nullable filePathParam, NSError *_Nullable error) {
// Ensure disk IO and decryption occurs off the main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]);
NSData *_Nullable decryptedData =
[self decryptProfileData:encryptedData profileKey:profileKeyAtStart];
UIImage *_Nullable image = nil;
if (decryptedData) {
BOOL success = [decryptedData writeToFile:filePath atomically:YES];
if (success) {
image = [UIImage imageWithContentsOfFile:filePath];
}
}
dispatch_async(dispatch_get_main_queue(), ^{ @synchronized(self)
[self.currentAvatarDownloads removeObject:userProfile.recipientId]; {
[self.currentAvatarDownloads removeObject:userProfile.recipientId];
UserProfile *currentUserProfile =
[self getOrBuildUserProfileForRecipientId:userProfile.recipientId]; UserProfile *currentUserProfile =
if (currentUserProfile.profileKey.keyData.length < 1 [self getOrBuildUserProfileForRecipientId:userProfile.recipientId];
|| ![currentUserProfile.profileKey isEqual:userProfile.profileKey]) { if (currentUserProfile.profileKey.keyData.length < 1
DDLogWarn(@"%@ Ignoring avatar download for obsolete user profile.", self.tag); || ![currentUserProfile.profileKey isEqual:userProfile.profileKey]) {
} else if (error) { DDLogWarn(@"%@ Ignoring avatar download for obsolete user profile.", self.tag);
DDLogError(@"%@ avatar download failed: %@", self.tag, error); } else if (![avatarUrlPathAtStart isEqualToString:currentUserProfile.avatarUrlPath]) {
} else if (!encryptedData) { DDLogInfo(@"%@ avatar url has changed during download", self.tag);
DDLogError(@"%@ avatar encrypted data could not be read.", self.tag); if (currentUserProfile.avatarUrlPath.length > 0) {
} else if (!decryptedData) { [self downloadAvatarForUserProfile:currentUserProfile];
DDLogError(@"%@ avatar data could not be decrypted.", self.tag); }
} else if (!image) { } else if (error) {
DDLogError(@"%@ avatar image could not be loaded: %@", self.tag, error); DDLogError(@"%@ avatar download failed: %@", self.tag, error);
} else { } else if (!encryptedData) {
[self.otherUsersProfileAvatarImageCache setObject:image forKey:userProfile.recipientId]; DDLogError(@"%@ avatar encrypted data could not be read.", self.tag);
} else if (!decryptedData) {
userProfile.avatarFileName = fileName; DDLogError(@"%@ avatar data could not be decrypted.", self.tag);
} else if (!image) {
[self saveUserProfile:userProfile]; DDLogError(@"%@ avatar image could not be loaded: %@", self.tag, error);
} } else {
}); // TODO: Verify that the avatar URL hasn't changed since the download began.
}); [self.otherUsersProfileAvatarImageCache setObject:image forKey:userProfile.recipientId];
}];
[downloadTask resume]; userProfile.avatarFileName = fileName;
[self saveUserProfile:userProfile];
}
}
});
}];
[downloadTask resume];
}
});
} }
- (void)refreshProfileForRecipientId:(NSString *)recipientId - (void)refreshProfileForRecipientId:(NSString *)recipientId
@ -863,32 +921,37 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
- (void)refreshProfileForRecipientId:(NSString *)recipientId ignoreThrottling:(BOOL)ignoreThrottling - (void)refreshProfileForRecipientId:(NSString *)recipientId ignoreThrottling:(BOOL)ignoreThrottling
{ {
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientId.length > 0); OWSAssert(recipientId.length > 0);
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self)
if (!userProfile.profileKey) { {
// There's no point in fetching the profile for a user UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
// if we don't have their profile key; we won't be able
// to decrypt it. if (!userProfile.profileKey) {
return; // There's no point in fetching the profile for a user
} // if we don't have their profile key; we won't be able
// to decrypt it.
return;
}
// Throttle and debounce the updates. // Throttle and debounce the updates.
const NSTimeInterval kMaxRefreshFrequency = 5 * kMinuteInterval; const NSTimeInterval kMaxRefreshFrequency = 5 * kMinuteInterval;
if (userProfile.lastUpdateDate && fabs([userProfile.lastUpdateDate timeIntervalSinceNow]) < kMaxRefreshFrequency) { if (userProfile.lastUpdateDate
// This profile was updated recently or already has an update in flight. && fabs([userProfile.lastUpdateDate timeIntervalSinceNow]) < kMaxRefreshFrequency) {
return; // This profile was updated recently or already has an update in flight.
} return;
}
userProfile.lastUpdateDate = [NSDate new]; userProfile.lastUpdateDate = [NSDate new];
[self saveUserProfile:userProfile]; [self saveUserProfile:userProfile];
[ProfileFetcherJob runWithRecipientId:recipientId [ProfileFetcherJob runWithRecipientId:recipientId
networkManager:self.networkManager networkManager:self.networkManager
ignoreThrottling:ignoreThrottling]; ignoreThrottling:ignoreThrottling];
}
});
} }
- (void)updateProfileForRecipientId:(NSString *)recipientId - (void)updateProfileForRecipientId:(NSString *)recipientId
@ -899,18 +962,18 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
// Ensure decryption, etc. off main thread. // Ensure decryption, etc. off main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self)
{
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId];
if (!userProfile.profileKey) {
return;
}
UserProfile *userProfile = [self getOrBuildUserProfileForRecipientId:recipientId]; NSString *_Nullable profileName =
if (!userProfile.profileKey) { [self decryptProfileNameData:profileNameEncrypted profileKey:userProfile.profileKey];
return;
}
NSString *_Nullable profileName =
[self decryptProfileNameData:profileNameEncrypted profileKey:userProfile.profileKey];
BOOL isAvatarSame = [self isNullableStringEqual:userProfile.avatarUrlPath toString:avatarUrlPath]; BOOL isAvatarSame = [self isNullableStringEqual:userProfile.avatarUrlPath toString:avatarUrlPath];
dispatch_async(dispatch_get_main_queue(), ^{
userProfile.profileName = profileName; userProfile.profileName = profileName;
userProfile.avatarUrlPath = avatarUrlPath; userProfile.avatarUrlPath = avatarUrlPath;
@ -926,7 +989,7 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
userProfile.lastUpdateDate = [NSDate new]; userProfile.lastUpdateDate = [NSDate new];
[self saveUserProfile:userProfile]; [self saveUserProfile:userProfile];
}); }
}); });
} }

Loading…
Cancel
Save