Files
bundesmessenger-ios/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m

713 lines
26 KiB
Objective-C

/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAttachment.h"
#import "MXKSwiftHeader.h"
@import MatrixSDK;
@import MobileCoreServices;
#import "MXKTools.h"
// The size of thumbnail we request from the server
// Note that this is smaller than the ones we upload: when sending, one size
// must fit all, including the web which will want relatively high res thumbnails.
// We, however, are a mobile client and so would prefer smaller thumbnails, which
// we can have if they're being generated by the media repo.
static const int kThumbnailWidth = 320;
static const int kThumbnailHeight = 240;
NSString *const kMXKAttachmentErrorDomain = @"kMXKAttachmentErrorDomain";
NSString *const kMXKAttachmentFileNameBase = @"attatchment";
@interface MXKAttachment ()
{
/**
The information on the encrypted content.
*/
MXEncryptedContentFile *contentFile;
/**
The information on the encrypted thumbnail.
*/
MXEncryptedContentFile *thumbnailFile;
/**
Observe Attachment download
*/
id onAttachmentDownloadObs;
/**
The local path used to store the attachment with its original name
*/
NSString *documentCopyPath;
/**
The attachment mimetype.
*/
NSString *mimetype;
}
@end
@implementation MXKAttachment
- (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager
{
self = [super init];
if (self)
{
_mediaManager = mediaManager;
// Make a copy as the data can be read at anytime later
_eventId = event.eventId;
_eventRoomId = event.roomId;
_eventSentState = event.sentState;
NSDictionary *eventContent = event.content;
// Set default thumbnail orientation
_thumbnailOrientation = UIImageOrientationUp;
if (event.eventType == MXEventTypeSticker)
{
_type = MXKAttachmentTypeSticker;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else
{
// Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here.
NSString *msgtype = eventContent[kMXMessageTypeKey];
if ([msgtype isEqualToString:kMXMessageTypeImage])
{
_type = MXKAttachmentTypeImage;
}
else if (event.isVoiceMessage)
{
_type = MXKAttachmentTypeVoiceMessage;
}
else if ([msgtype isEqualToString:kMXMessageTypeAudio])
{
_type = MXKAttachmentTypeAudio;
}
else if ([msgtype isEqualToString:kMXMessageTypeVideo])
{
_type = MXKAttachmentTypeVideo;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else if ([msgtype isEqualToString:kMXMessageTypeFile])
{
_type = MXKAttachmentTypeFile;
}
else
{
return nil;
}
}
MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]);
MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]);
MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]);
// Retrieve the content url by taking into account the potential encryption.
if (contentFile)
{
_isEncrypted = YES;
_contentURL = contentFile.url;
MXJSONModelSetMXJSONModel(thumbnailFile, MXEncryptedContentFile, _contentInfo[@"thumbnail_file"]);
}
else
{
_isEncrypted = NO;
MXJSONModelSetString(_contentURL, eventContent[@"url"]);
}
mimetype = nil;
if (_contentInfo)
{
MXJSONModelSetString(mimetype, _contentInfo[@"mimetype"]);
}
_cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:_contentURL andType:mimetype inFolder:_eventRoomId];
_downloadId = [MXMediaManager downloadIdForMatrixContentURI:_contentURL inFolder:_eventRoomId];
// Deduce the thumbnail information from the retrieved data.
_mxcThumbnailURI = [self getThumbnailURI];
_thumbnailMimeType = [self getThumbnailMimeType];
_thumbnailCachePath = [self getThumbnailCachePath];
_thumbnailDownloadId = [self getThumbnailDownloadId];
}
return self;
}
- (void)dealloc
{
[self destroy];
}
- (void)destroy
{
if (onAttachmentDownloadObs)
{
[[NSNotificationCenter defaultCenter] removeObserver:onAttachmentDownloadObs];
onAttachmentDownloadObs = nil;
}
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
_previewImage = nil;
}
- (NSString *)getThumbnailURI
{
if (thumbnailFile)
{
// there's an encrypted thumbnail: we return the mxc url
return thumbnailFile.url;
}
// Look for a clear thumbnail url
return _contentInfo[@"thumbnail_url"];
}
- (NSString *)getThumbnailMimeType
{
return _thumbnailInfo[@"mimetype"];
}
- (NSString*)getThumbnailCachePath
{
if (_mxcThumbnailURI)
{
return [MXMediaManager cachePathForMatrixContentURI:_mxcThumbnailURI andType:_thumbnailMimeType inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailCachePathForMatrixContentURI:_contentURL
andType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (NSString *)getThumbnailDownloadId
{
if (_mxcThumbnailURI)
{
return [MXMediaManager downloadIdForMatrixContentURI:_mxcThumbnailURI inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_contentURL
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (UIImage *)getCachedThumbnail
{
if (_thumbnailCachePath)
{
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb) return thumb;
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
return [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath];
}
}
return nil;
}
- (void)getThumbnail:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
// Check whether a thumbnail is defined.
if (!_thumbnailCachePath)
{
// there is no thumbnail: if we're an image, return the full size image. Otherwise, nothing we can do.
if (_type == MXKAttachmentTypeImage)
{
[self getImage:onSuccess failure:onFailure];
}
else if (onFailure)
{
onFailure(self, nil);
}
return;
}
// Check the current memory cache.
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb)
{
onSuccess(self, thumb);
return;
}
if (thumbnailFile)
{
MXWeakify(self);
void (^decryptAndCache)(void) = ^{
MXStrongifyAndReturnIfNil(self);
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.thumbnailCachePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->thumbnailFile inputStream:instream outputStream:outstream success:^{
UIImage *img = [UIImage imageWithData:[outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
// Save this image to in-memory cache.
[MXMediaManager cacheImage:img withCachePath:self.thumbnailCachePath];
onSuccess(self, img);
} failure:^(NSError *err) {
if (err) {
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
if (onFailure) onFailure(self, err);
return;
}
}];
};
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
decryptAndCache();
}
else
{
[_mediaManager downloadEncryptedMediaFromMatrixContentFile:thumbnailFile
mimeType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
decryptAndCache();
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
else
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath]);
}
else if (_mxcThumbnailURI)
{
[_mediaManager downloadMediaFromMatrixContentURI:_mxcThumbnailURI
withType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
else
{
// Here _thumbnailCachePath is defined, so a thumbnail is available.
// Because _mxcThumbnailURI is null, this means we have to consider the content uri (see getThumbnailCachePath).
[_mediaManager downloadThumbnailFromMatrixContentURI:_contentURL
withType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
}
- (void)getImage:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
[self getAttachmentData:^(NSData *data) {
UIImage *img = [UIImage imageWithData:data];
if (img)
{
if (onSuccess)
{
onSuccess(self, img);
}
}
else
{
if (onFailure)
{
NSError *error = [NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_get_image_from_data"}];
onFailure(self, error);
}
}
} failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
- (void)getAttachmentData:(void (^)(NSData *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.isEncrypted)
{
// decrypt the encrypted file
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.cacheFilePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:instream outputStream:outstream success:^{
onSuccess([outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]);
} failure:^(NSError *err) {
if (err)
{
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
return;
}
}];
}
else
{
onSuccess([NSData dataWithContentsOfFile:self.cacheFilePath]);
}
} failure:onFailure];
}
- (void)decryptToTempFile:(void (^)(NSString *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSString *tempPath = [self getTempFile];
if (!tempPath)
{
if (onFailure) onFailure([NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_creating_temp_file"}]);
return;
}
NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:self.cacheFilePath];
NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:NO];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:inStream outputStream:outStream success:^{
onSuccess(tempPath);
} failure:^(NSError *err) {
if (err) {
if (onFailure) onFailure(err);
return;
}
}];
} failure:onFailure];
}
- (NSString *)getTempFile
{
// create a file with an appropriate extension because iOS detects based on file extension
// all over the place
NSString *ext = [MXTools fileExtensionFromContentType:mimetype];
NSString *filenameTemplate = [NSString stringWithFormat:@"%@.XXXXXX%@", kMXKAttachmentFileNameBase, ext];
NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:filenameTemplate];
const char *templateCstr = [template fileSystemRepresentation];
char *tempPathCstr = (char *)malloc(strlen(templateCstr) + 1);
strcpy(tempPathCstr, templateCstr);
int fd = mkstemps(tempPathCstr, (int)ext.length);
if (!fd)
{
return nil;
}
close(fd);
NSString *tempPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPathCstr
length:strlen(tempPathCstr)];
free(tempPathCstr);
return tempPath;
}
+ (void)clearCache
{
NSString *temporaryDirectoryPath = NSTemporaryDirectory();
NSDirectoryEnumerator<NSString *> *enumerator = [NSFileManager.defaultManager enumeratorAtPath:temporaryDirectoryPath];
NSString *filePath;
while (filePath = [enumerator nextObject]) {
if(![filePath containsString:kMXKAttachmentFileNameBase]) {
continue;
}
NSError *error;
BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error];
if (!result && error) {
MXLogError(@"[MXKAttachment] Failed deleting temporary file with error: %@", error);
}
}
}
- (void)prepare:(void (^)(void))onAttachmentReady failure:(void (^)(NSError *error))onFailure
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_cacheFilePath])
{
// Done
if (onAttachmentReady)
{
onAttachmentReady();
}
}
else
{
// Trigger download if it is not already in progress
MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:_downloadId];
if (!loader)
{
if (_isEncrypted)
{
loader = [_mediaManager downloadEncryptedMediaFromMatrixContentFile:contentFile
mimeType:mimetype
inFolder:_eventRoomId];
}
else
{
loader = [_mediaManager downloadMediaFromMatrixContentURI:_contentURL
withType:mimetype
inFolder:_eventRoomId];
}
}
if (loader)
{
MXWeakify(self);
// Add observers
onAttachmentDownloadObs = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:loader queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
switch (loader.state) {
case MXMediaLoaderStateDownloadCompleted:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onAttachmentReady)
{
onAttachmentReady ();
}
break;
case MXMediaLoaderStateDownloadFailed:
case MXMediaLoaderStateCancelled:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onFailure)
{
onFailure (loader.error);
}
break;
default:
break;
}
}];
}
else if (onFailure)
{
onFailure (nil);
}
}
}
- (void)save:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
if (_type == MXKAttachmentTypeImage || _type == MXKAttachmentTypeVideo)
{
MXWeakify(self);
if (self.isEncrypted) {
[self decryptToTempFile:^(NSString *path) {
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:path];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
else
{
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:self.cacheFilePath];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
}
else
{
// Not supported
if (onFailure)
{
onFailure(nil);
}
}
}
- (void)copy:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.type == MXKAttachmentTypeImage)
{
[self getImage:^(MXKAttachment *attachment, UIImage *img) {
MXKPasteboardManager.shared.pasteboard.image = img;
if (onSuccess)
{
onSuccess();
}
} failure:^(MXKAttachment *attachment, NSError *error) {
if (onFailure) onFailure(error);
}];
}
else
{
MXWeakify(self);
[self getAttachmentData:^(NSData *data) {
if (data)
{
MXStrongifyAndReturnIfNil(self);
NSString* UTI = (__bridge_transfer NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[self.cacheFilePath pathExtension] , NULL);
if (UTI)
{
[MXKPasteboardManager.shared.pasteboard setData:data forPasteboardType:UTI];
if (onSuccess)
{
onSuccess();
}
}
}
} failure:onFailure];
}
// Unexpected error
if (onFailure)
{
onFailure(nil);
}
} failure:onFailure];
}
- (MXKUTI *)uti
{
return [[MXKUTI alloc] initWithMimeType:mimetype];
}
- (void)prepareShare:(void (^)(NSURL *fileURL))onReadyToShare failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
void (^haveFile)(NSString *) = ^(NSString *path) {
// Prepare the file URL by considering the original file name (if any)
NSURL *fileUrl;
MXStrongifyAndReturnIfNil(self);
// Check whether the original name retrieved from event body has extension
if (self.originalFileName && [self.originalFileName pathExtension].length)
{
// Copy the cached file to restore its original name
// Note: We used previously symbolic link (instead of copy) but UIDocumentInteractionController failed to open Office documents (.docx, .pptx...).
self->documentCopyPath = [[MXMediaManager getCachePath] stringByAppendingPathComponent:self.originalFileName];
[[NSFileManager defaultManager] removeItemAtPath:self->documentCopyPath error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:path toPath:self->documentCopyPath error:nil])
{
fileUrl = [NSURL fileURLWithPath:self->documentCopyPath];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
}
if (!fileUrl)
{
// Use the cached file by default
fileUrl = [NSURL fileURLWithPath:path];
self->documentCopyPath = path;
}
onReadyToShare (fileUrl);
};
if (self.isEncrypted)
{
[self decryptToTempFile:^(NSString *path) {
haveFile(path);
} failure:onFailure];
}
else
{
// First download data if it is not already done
[self prepare:^{
haveFile(self.cacheFilePath);
} failure:onFailure];
}
}
- (void)onShareEnded
{
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
}
@end