diff --git a/CHANGES.rst b/CHANGES.rst index 551a0ef5f..cadc8d154 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +Changes in 0.7.10 (2019-01-04) +=============================================== + +Bug fix: + * Share extension: Fix screenshot sharing (#2022). Improve image sharing performance to avoid out of memory crash. + Changes in 0.7.9 (2019-01-04) =============================================== diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 65dfe3390..f4fb1ea3e 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.7.9 + 0.7.10 CFBundleSignature ???? CFBundleVersion - 0.7.9 + 0.7.10 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/RiotShareExtension/Managers/ShareExtensionManager.h b/RiotShareExtension/Managers/ShareExtensionManager.h index 7ba468716..bbd549103 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.h +++ b/RiotShareExtension/Managers/ShareExtensionManager.h @@ -89,7 +89,7 @@ extern NSString *const kShareExtensionManagerDidUpdateAccountDataNotification; /** A delegate used to notify about needed UI changes when sharing */ -@property (nonatomic) id delegate; +@property (nonatomic, weak) id delegate; /** The singleton instance diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index 541b5892d..182a9481c 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -19,6 +19,7 @@ #import @import MobileCoreServices; #import "objc/runtime.h" +#include NSString *const kShareExtensionManagerDidUpdateAccountDataNotification = @"kShareExtensionManagerDidUpdateAccountDataNotification"; @@ -73,6 +74,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) sdkOptions.disableIdenticonUseForUserAvatar = YES; // Enable e2e encryption for newly created MXSession sdkOptions.enableCryptoWhenStartingMXSession = YES; + // Use UIKit BackgroundTask for handling background tasks in the SDK + sdkOptions.backgroundModeHandler = [[MXUIKitBackgroundModeHandler alloc] init]; // Customize the localized string table [NSBundle mxk_customizeLocalizedStringTableName:@"Vector"]; @@ -215,13 +218,31 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) else if ([itemProvider hasItemConformingToTypeIdentifier:UTTypeImage]) { itemProvider.isLoaded = NO; - [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(NSData *imageData, NSError * _Null_unspecified error) + + [itemProvider loadItemForTypeIdentifier:UTTypeImage options:nil completionHandler:^(id _Nullable itemProviderItem, NSError * _Null_unspecified error) { if (weakSelf) { typeof(self) self = weakSelf; itemProvider.isLoaded = YES; - + + NSData *imageData; + + if ([(NSObject *)itemProviderItem isKindOfClass:[NSData class]]) + { + imageData = (NSData*)itemProviderItem; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[NSURL class]]) + { + NSURL *imageURL = (NSURL*)itemProviderItem; + imageData = [NSData dataWithContentsOfURL:imageURL]; + } + else if ([(NSObject *)itemProviderItem isKindOfClass:[UIImage class]]) + { + UIImage *image = (UIImage*)itemProviderItem; + imageData = UIImageJPEGRepresentation(image, 1.0); + } + if (imageData) { [self.pendingImages addObject:imageData]; @@ -233,10 +254,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if ([self areAttachmentsFullyLoaded]) { - UIAlertController *compressionPrompt = [self compressionPromptForImage:self.pendingImages.firstObject shareBlock:^{ + UIAlertController *compressionPrompt = [self compressionPromptForPendingImagesWithShareBlock:^{ [self sendImages:self.pendingImages withProviders:item.attachments toRoom:room extensionItem:item failureBlock:failureBlock]; }]; - + if (compressionPrompt) { [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; @@ -311,6 +332,10 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self.primaryViewController destroy]; self.primaryViewController = nil; + + // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. + // For now, we force the share extension to exit and free memory. + [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; } #pragma mark - Private @@ -327,15 +352,51 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [self.primaryViewController destroy]; self.primaryViewController = nil; + + // FIXME: Share extension memory usage increase when launched several times and then crash due to some memory leaks. + // For now, we force the share extension to exit and free memory. + [NSException raise:@"Kill the app extension" format:@"Free memory used by share extension"]; } -- (UIAlertController *)compressionPromptForImage:(NSData *)imageData shareBlock:(void(^)())shareBlock +- (BOOL)isAPendingImageNotOrientedUp { + BOOL isAPendingImageNotOrientedUp = NO; + + for (NSData *imageData in self.pendingImages) + { + @autoreleasepool + { + UIImage *image = [UIImage imageWithData:imageData]; + + if (image && image.imageOrientation != UIImageOrientationUp) + { + isAPendingImageNotOrientedUp = YES; + break; + } + } + } + + return isAPendingImageNotOrientedUp; +} + +// TODO: When select multiple images: +// - Enhance prompt to display sum of all file sizes for each compression. +// - Find a way to choose compression sizes for all images. +- (UIAlertController *)compressionPromptForPendingImagesWithShareBlock:(void(^)(void))shareBlock +{ + if (!self.pendingImages.count) + { + return nil; + } + UIAlertController *compressionPrompt; - UIImage *image = [UIImage imageWithData:imageData]; + BOOL isAPendingImageNotOrientedUp = [self isAPendingImageNotOrientedUp]; + + NSData *firstImageData = self.pendingImages.firstObject; + UIImage *firstImage = [UIImage imageWithData:firstImageData]; // Get available sizes for this image - MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:image originalFileSize:imageData.length]; + MXKImageCompressionSizes compressionSizes = [MXKTools availableCompressionSizesForImage:firstImage originalFileSize:firstImageData.length]; // Apply the compression mode if (compressionSizes.small.fileSize || compressionSizes.medium.fileSize || compressionSizes.large.fileSize) @@ -364,8 +425,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { shareBlock(); } - - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; } }]]; @@ -391,8 +450,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { shareBlock(); } - - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; } }]]; @@ -419,15 +476,13 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { shareBlock(); } - - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; } }]]; } // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate - if (image.imageOrientation == UIImageOrientationUp || !compressionSizes.large.fileSize) + if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) { NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.original.fileSize round:NO], (int)compressionSizes.original.imageSize.width, (int)compressionSizes.original.imageSize.height]; @@ -446,8 +501,6 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { shareBlock(); } - - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; } }]]; @@ -455,20 +508,21 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) [compressionPrompt addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleCancel - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - [compressionPrompt dismissViewControllerAnimated:YES completion:nil]; - } - - }]]; + handler:nil]]; } else { - self.imageCompressionMode = ImageCompressionModeNone; + if (isAPendingImageNotOrientedUp && self.pendingImages.count > 1) + { + self.imageCompressionMode = ImageCompressionModeSmall; + } + else + { + self.imageCompressionMode = ImageCompressionModeNone; + } + if (shareBlock) { shareBlock(); @@ -599,113 +653,139 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)sendImages:(NSMutableArray *)imageDatas withProviders:(NSArray*)itemProviders toRoom:(MXRoom *)room extensionItem:(NSExtensionItem *)extensionItem failureBlock:(void(^)(NSError *error))failureBlock { + if (imageDatas.count == 0) + { + NSLog(@"[ShareExtensionManager] sendImages: no images to send."); + + if (failureBlock) + { + failureBlock(nil); + } + return; + } + [self didStartSendingToRoom:room]; __block NSUInteger count = imageDatas.count; for (NSInteger index = 0; index < imageDatas.count; index++) { - NSItemProvider *itemProvider = itemProviders[index]; - NSData *imageData = imageDatas[index]; - UIImage *image = [UIImage imageWithData:imageData]; - - if (!image) + @autoreleasepool { - NSLog(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - failureBlock = nil; - } - return; - } - - // Prepare the image - UIImage *convertedImage = image; - - if (self.imageCompressionMode == ImageCompressionModeSmall) - { - convertedImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE)]; - } - else if (self.imageCompressionMode == ImageCompressionModeMedium) - { - convertedImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE)]; - } - else if (self.imageCompressionMode == ImageCompressionModeLarge) - { - convertedImage = [MXKTools reduceImage:image toFitInSize:CGSizeMake(self.actualLargeSize, self.actualLargeSize)]; - } - - // Make sure the uploaded image orientation is up - convertedImage = [MXKTools forceImageOrientationUp:convertedImage]; - - NSString *mimeType; - if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePNG]) - { - mimeType = @"image/png"; - if (convertedImage != image) - { - imageData = UIImagePNGRepresentation(convertedImage); - } - } - else if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeJPEG]) - { - mimeType = @"image/jpeg"; - if (convertedImage != image) - { - imageData = UIImageJPEGRepresentation(convertedImage, 0.9); - } - } - else - { - // Other image types like GIF - NSString *imageFileName = itemProvider.registeredTypeIdentifiers[0]; - mimeType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)imageFileName, kUTTagClassMIMEType); - } - - // Sanity check - if (!mimeType) - { - NSLog(@"[ShareExtensionManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - UIImage *thumbnail = nil; - // Thumbnail is useful only in case of encrypted room - if (room.summary.isEncrypted) - { - thumbnail = [MXKTools reduceImage:convertedImage toFitInSize:CGSizeMake(800, 600)]; - if (thumbnail == convertedImage) - { - thumbnail = nil; - } - } - - __weak typeof(self) weakSelf = self; - - [room sendImage:imageData withImageSize:convertedImage.size mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { + NSItemProvider *itemProvider = itemProviders[index]; + NSData *imageData = imageDatas[index]; + UIImage *image = [UIImage imageWithData:imageData]; - if (!--count && weakSelf) + if (!image) { - typeof(self) self = weakSelf; + NSLog(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); + if (failureBlock) + { + failureBlock(nil); + } + return; + } + + // Prepare the image + UIImage *convertedImage; + CGSize newImageSize; + + switch (self.imageCompressionMode) { + case ImageCompressionModeSmall: + newImageSize = CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE); + break; + case ImageCompressionModeMedium: + newImageSize = CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE); + break; + case ImageCompressionModeLarge: + newImageSize = CGSizeMake(self.actualLargeSize, self.actualLargeSize); + break; + default: + newImageSize = CGSizeZero; + break; + } + + if (CGSizeEqualToSize(newImageSize, CGSizeZero)) + { + // No resize to make + // Make sure the uploaded image orientation is up + convertedImage = [MXKTools forceImageOrientationUp:image]; + } + else + { + // Resize the image and set image in right orientation too + convertedImage = [MXKTools resizeImageWithData:imageData toFitInSize:newImageSize]; + } + + NSString *mimeType; + if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePNG]) + { + mimeType = @"image/png"; + if (convertedImage != image) + { + imageData = UIImagePNGRepresentation(convertedImage); + } + } + else if ([itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeJPEG]) + { + mimeType = @"image/jpeg"; + if (convertedImage != image) + { + imageData = UIImageJPEGRepresentation(convertedImage, 0.9); + } + } + else + { + // Other image types like GIF + NSString *imageFileName = itemProvider.registeredTypeIdentifiers[0]; + mimeType = (__bridge_transfer NSString *) UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)imageFileName, kUTTagClassMIMEType); + } + + // Sanity check + if (!mimeType) + { + NSLog(@"[ShareExtensionManager] sendImage failed. Cannot determine MIME type of %@", itemProvider); + if (failureBlock) + { + failureBlock(nil); + } + return; + } + + UIImage *thumbnail = nil; + // Thumbnail is useful only in case of encrypted room + if (room.summary.isEncrypted) + { + thumbnail = [MXKTools reduceImage:convertedImage toFitInSize:CGSizeMake(800, 600)]; + if (thumbnail == convertedImage) + { + thumbnail = nil; + } + } + + __weak typeof(self) weakSelf = self; + + [room sendImage:imageData withImageSize:convertedImage.size mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { - [self resetPendingData]; - [self.shareExtensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil]; - } + if (!--count && weakSelf) + { + typeof(self) self = weakSelf; + + [self resetPendingData]; + [self completeRequestReturningItems:@[extensionItem] completionHandler:nil]; + } + + } failure:^(NSError *error) { + + NSLog(@"[ShareExtensionManager] sendImage failed."); + if (failureBlock) + { + failureBlock(error); + } + + }]; - } failure:^(NSError *error) { - - NSLog(@"[ShareExtensionManager] sendImage failed."); - if (failureBlock) - { - failureBlock(error); - } - - }]; + } } } diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index 564cffca5..af4f7ad4b 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.7.9 + 0.7.10 CFBundleVersion - 0.7.9 + 0.7.10 NSExtension NSExtensionAttributes diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist index 35241169c..6d62b7e83 100644 --- a/SiriIntents/Info.plist +++ b/SiriIntents/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.7.9 + 0.7.10 CFBundleVersion - 0.7.9 + 0.7.10 NSExtension NSExtensionAttributes