another.im-ios/Monal/Classes/MLImageManager.m
2024-11-18 15:53:52 +01:00

417 lines
16 KiB
Objective-C

//
// MLImageManager.m
// Monal
//
// Created by Anurodh Pokharel on 8/16/13.
//
//
#import "MLImageManager.h"
#import "MLXMPPManager.h"
#import "HelperTools.h"
#import "DataLayer.h"
#import "AESGcm.h"
#import "UIColor+Extension.h"
@interface MLImageManager()
@property (nonatomic, strong) NSCache* iconCache;
@property (nonatomic, strong) NSString* documentsDirectory;
@property (nonatomic, strong) NSCache* backgroundCache;
@end
@implementation MLImageManager
#pragma mark initilization
+(MLImageManager*) sharedInstance
{
static dispatch_once_t once;
static MLImageManager* sharedInstance;
dispatch_once(&once, ^{
DDLogVerbose(@"Creating shared image manager instance...");
sharedInstance = [MLImageManager new];
});
return sharedInstance;
}
//this mehod should *only* be used in the mainapp due to memory requirements for large images
+(UIImage*) circularImage:(UIImage*) image
{
return [[[UIGraphicsImageRenderer alloc] initWithSize:image.size] imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull rendererContext) {
UIBezierPath* clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
[clipPath addClip];
//Flip coordinates before drawing image as UIKit and CoreGraphics have inverted coordinate system
CGContextTranslateCTM(rendererContext.CGContext, 0, image.size.height);
CGContextScaleCTM(rendererContext.CGContext, 1, -1);
CGContextDrawImage(rendererContext.CGContext, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage);
}];
}
+(UIImage*) image:(UIImage*) image withMucOverlay:(UIImage*) overlay
{
UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat new];
format.opaque = NO;
format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
format.scale = 1.0;
CGRect drawRect = CGRectMake(0, 0, image.size.width, image.size.height);
CGFloat overlaySize = (float)(image.size.width / 3);
UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize:drawRect.size format:format];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext* _Nonnull context __unused) {
[image drawInRect:drawRect];
CGRect overlayRect = CGRectMake(0, //renderer.format.bounds.size.width - overlaySize
0, //renderer.format.bounds.size.height - overlaySize
overlaySize,
overlaySize);
[overlay drawInRect:overlayRect];
}];
}
-(id) init
{
self = [super init];
self.iconCache = [NSCache new];
self.backgroundCache = [NSCache new];
NSFileManager* fileManager = [NSFileManager defaultManager];
self.documentsDirectory = [[HelperTools getContainerURLForPathComponents:@[]] path];
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"imagecache"];
[fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:nil];
[HelperTools configureFileProtectionFor:writablePath];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryPressureNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
return self;
}
-(void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void) handleMemoryPressureNotification
{
DDLogVerbose(@"Removing all objects in avatar cache due to memory pressure...");
[self purgeCache];
}
#pragma mark cache
-(void) purgeCache
{
[self.iconCache removeAllObjects];
[self.backgroundCache removeAllObjects];
}
-(void) purgeCacheForContact:(NSString*) contact andAccount:(NSNumber*) accountID
{
[self.iconCache removeObjectForKey:[NSString stringWithFormat:@"%@_%@", accountID, contact]];
[self resetCachedBackgroundImageForContact:[MLContact createContactFromJid:contact andAccountID:accountID]];
}
-(void) cleanupHashes
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSArray<MLContact*>* contactList = [[DataLayer sharedInstance] contactList];
for(MLContact* contact in contactList)
{
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
writablePath = [writablePath stringByAppendingPathComponent:contact.accountID.stringValue];
writablePath = [writablePath stringByAppendingPathComponent:[self fileNameforContact:contact]];
NSString* hash = [[DataLayer sharedInstance] getAvatarHashForContact:contact.contactJid andAccount:contact.accountID];
BOOL hasHash = ![@"" isEqualToString:hash];
if(hasHash && ![fileManager isReadableFileAtPath:writablePath])
{
DDLogDebug(@"Deleting orphan hash '%@' of contact: %@", hash, contact);
//delete avatar hash from db if the file containing our image data vanished
[[DataLayer sharedInstance] setAvatarHash:@"" forContact:contact.contactJid andAccount:contact.accountID];
}
if(!hasHash && [fileManager isReadableFileAtPath:writablePath])
{
DDLogDebug(@"Deleting orphan avatar file '%@' of contact: %@", writablePath, contact);
NSError* error;
[fileManager removeItemAtPath:writablePath error:&error];
if(error)
DDLogError(@"Error deleting orphan avatar file: %@", error);
}
}
}
-(void) removeAllIcons
{
NSError* error;
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
[fileManager removeItemAtPath:writablePath error:&error];
if(error)
DDLogError(@"Got error while trying to delete all avatar files: %@", error);
}
#pragma mark chat bubbles
-(UIImage*) inboundImage
{
if(_inboundImage)
return _inboundImage;
_inboundImage = [[UIImage imageNamed:@"incoming"] resizableImageWithCapInsets:UIEdgeInsetsMake(6, 6, 6, 6)];
return _inboundImage;
}
-(UIImage*) outboundImage
{
if (_outboundImage)
return _outboundImage;
_outboundImage = [[UIImage imageNamed:@"outgoing"] resizableImageWithCapInsets:UIEdgeInsetsMake(6, 6, 6, 6)];
return _outboundImage;
}
#pragma mark user icons
-(UIImage*) generateDummyIconForContact:(MLContact*) contact
{
NSString* contactLetter;
if(contact.isSelfChat)
{
xmpp* account = contact.account;
contactLetter = [[[MLContact ownDisplayNameForAccount:account] substringToIndex:1] uppercaseString];
}
else
contactLetter = [[[contact contactDisplayName] substringToIndex:1] uppercaseString];
UIColor* background = [HelperTools generateColorFromJid:contact.contactJid];
UIColor* foreground = [UIColor blackColor];
if(![background isLightColor])
foreground = [UIColor whiteColor];
CGRect drawRect = CGRectMake(0, 0, 200, 200);
UIGraphicsImageRenderer* renderer = [[UIGraphicsImageRenderer alloc] initWithSize:drawRect.size];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) {
//make sure our image is circular
[[UIBezierPath bezierPathWithOvalInRect:drawRect] addClip];
//fill the background of our image
[background setFill];
[context fillRect:renderer.format.bounds];
//draw letter in the middleof our image
NSMutableParagraphStyle* paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary* attributes = @{
NSFontAttributeName: [[UIFont preferredFontForTextStyle:UIFontTextStyleLargeTitle] fontWithSize:(unsigned int)(drawRect.size.height / 1.666)],
NSForegroundColorAttributeName: foreground,
NSParagraphStyleAttributeName: paragraphStyle
};
CGSize textSize = [contactLetter sizeWithAttributes:attributes];
CGRect textRect = CGRectMake(floorf((float)(renderer.format.bounds.size.width - textSize.width) / 2),
floorf((float)(renderer.format.bounds.size.height - textSize.height) / 2),
textSize.width,
textSize.height);
[contactLetter drawInRect:textRect withAttributes:attributes];
}];
}
-(NSString*) fileNameforContact:(MLContact*) contact
{
return [NSString stringWithFormat:@"%@_%@.png", contact.accountID.stringValue, [contact.contactJid lowercaseString]];;
}
-(void) setIconForContact:(MLContact*) contact WithData:(NSData* _Nullable) data
{
//documents directory/buddyicons/account no/contact
NSString* filename = [self fileNameforContact:contact];
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString *writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
writablePath = [writablePath stringByAppendingPathComponent:contact.accountID.stringValue];
NSError* error;
[fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:&error];
[HelperTools configureFileProtectionFor:writablePath];
writablePath = [writablePath stringByAppendingPathComponent:filename];
if([fileManager fileExistsAtPath:writablePath])
[fileManager removeItemAtPath:writablePath error:nil];
if(data)
{
if([data writeToFile:writablePath atomically:NO])
{
[HelperTools configureFileProtectionFor:writablePath];
DDLogVerbose(@"wrote image to file: %@", writablePath);
}
else
DDLogError(@"failed to write image to file: %@", writablePath);
}
//remove from cache if its there
[self.iconCache removeObjectForKey:[NSString stringWithFormat:@"%@_%@", contact.accountID, contact]];
}
-(BOOL) hasIconForContact:(MLContact*) contact
{
NSString* filename = [self fileNameforContact:contact];
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
writablePath = [writablePath stringByAppendingPathComponent:contact.accountID.stringValue];
writablePath = [writablePath stringByAppendingPathComponent:filename];
DDLogVerbose(@"Checking avatar image at: %@", writablePath);
return [UIImage imageWithContentsOfFile:writablePath] != nil;
}
-(UIImage*) getIconForContact:(MLContact*) contact
{
return [self getIconForContact:contact withCompletion:nil];
}
-(UIImage*) getIconForContact:(MLContact*) contact withCompletion:(void (^)(UIImage *))completion
{
NSString* filename = [self fileNameforContact:contact];
__block UIImage* toreturn = nil;
//get filname from DB
NSString* cacheKey = [NSString stringWithFormat:@"%@_%@", contact.accountID, contact.contactJid];
//check cache
toreturn = [self.iconCache objectForKey:cacheKey];
if(!toreturn)
{
NSString* writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"buddyicons"];
writablePath = [writablePath stringByAppendingPathComponent:contact.accountID.stringValue];
writablePath = [writablePath stringByAppendingPathComponent:filename];
DDLogVerbose(@"Loading avatar image at: %@", writablePath);
UIImage* savedImage = [UIImage imageWithContentsOfFile:writablePath];
if(savedImage)
toreturn = savedImage;
DDLogVerbose(@"Loaded image: %@", toreturn);
if(toreturn == nil) //return default avatar
{
DDLogVerbose(@"Using/generating dummy icon for contact: %@", contact);
if(contact.isMuc)
{
if([kMucTypeChannel isEqualToString:contact.mucType])
toreturn = [MLImageManager circularImage:[UIImage imageNamed:@"noicon_channel" inBundle:nil compatibleWithTraitCollection:nil]];
else
toreturn = [MLImageManager circularImage:[UIImage imageNamed:@"noicon_muc" inBundle:nil compatibleWithTraitCollection:nil]];
}
else
toreturn = [self generateDummyIconForContact:contact];
}
else if(contact.isMuc) //add group indicator overlay for non-default muc avatar
{
UIImage* overlay = nil;
if([kMucTypeChannel isEqualToString:contact.mucType])
overlay = [MLImageManager circularImage:[UIImage imageNamed:@"noicon_channel" inBundle:nil compatibleWithTraitCollection:nil]];
else
overlay = [MLImageManager circularImage:[UIImage imageNamed:@"noicon_muc" inBundle:nil compatibleWithTraitCollection:nil]];
if(overlay)
toreturn = [MLImageManager image:toreturn withMucOverlay:overlay];
}
//uiimage is cached if avaialable, but only if not in appex due to memory limits therein
if(toreturn && ![HelperTools isAppExtension])
[self.iconCache setObject:toreturn forKey:cacheKey];
if(completion)
dispatch_async(dispatch_get_main_queue(), ^{
completion(toreturn);
});
}
else if(completion)
dispatch_async(dispatch_get_main_queue(), ^{
completion(toreturn);
});
return toreturn;
}
-(void) saveBackgroundImageData:(NSData* _Nullable) data forContact:(MLContact* _Nullable) contact
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* writablePath;
if(contact != nil)
{
NSString* filename = [self fileNameforContact:contact];
writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"backgrounds"];
[fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:nil];
[HelperTools configureFileProtectionFor:writablePath];
writablePath = [writablePath stringByAppendingPathComponent:filename];
if([fileManager fileExistsAtPath:writablePath])
[fileManager removeItemAtPath:writablePath error:nil];
}
else
{
writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"background.jpg"];
if([fileManager fileExistsAtPath:writablePath])
[fileManager removeItemAtPath:writablePath error:nil];
}
[self resetCachedBackgroundImageForContact:contact];
//file was deleted above, just don't create it again
if(data != nil)
{
DDLogVerbose(@"Writing background image data %@ for %@ to '%@'...", data, contact, writablePath);
[data writeToFile:writablePath atomically:YES];
[HelperTools configureFileProtectionFor:writablePath];
}
//don't queue this notification because it should be handled immediately
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalBackgroundChanged object:contact];
}
-(UIImage* _Nullable) getBackgroundFor:(MLContact* _Nullable) contact
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* filename = @"background.jpg";
if(contact != nil)
filename = [self fileNameforContact:contact];
UIImage* img = [self.backgroundCache objectForKey:filename];
if(img != nil)
return img;
NSString* writablePath;
if(contact != nil)
{
writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"backgrounds"];
writablePath = [writablePath stringByAppendingPathComponent:filename];
if(![fileManager fileExistsAtPath:writablePath])
return nil;
}
else
{
writablePath = [self.documentsDirectory stringByAppendingPathComponent:@"background.jpg"];
if(![fileManager fileExistsAtPath:writablePath])
return nil;
}
DDLogVerbose(@"Loading background image for %@ from '%@'...", contact, writablePath);
img = [UIImage imageWithContentsOfFile:writablePath];
DDLogVerbose(@"Got image: %@", img);
[self.backgroundCache setObject:img forKey:filename];
return img;
}
-(void) resetCachedBackgroundImageForContact:(MLContact* _Nullable) contact
{
NSString* filename = @"background.jpg";
if(contact != nil)
filename = [self fileNameforContact:contact];
[self.backgroundCache removeObjectForKey:filename];
}
@end