123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2016-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- #import "SenderItemManager.h"
- #import "URLSenderItem.h"
- #import "UTIConverter.h"
- #import "Conversation.h"
- #import "BundleUtil.h"
- #import "MessageSender.h"
- #import "TextMessage.h"
- #import "DatabaseManager.h"
- #import "EntityManager.h"
- #import "MediaConverter.h"
- #import "FileMessageSender.h"
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- @interface IntermediateItem : NSObject
- @property NSItemProvider *itemProvider;
- @property NSString *type;
- @property NSString *secondType;
- @property NSString *caption;
- @end
- @implementation IntermediateItem
- @end
- @interface SenderItemManager () <UploadProgressDelegate>
- @property NSSet *recipientConversations;
- @property NSMutableSet *itemsToSend;
- @property NSString *textToSend;
- @property NSInteger sentItemCount;
- @property NSInteger totalSendCount;
- @property NSMutableArray *correlationIDs;
- @property dispatch_semaphore_t loadItemsSema;
- @end
- @implementation SenderItemManager
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- _itemsToSend = [NSMutableSet set];
- _containsFileItem = NO;
- _shouldCancel = NO;
- _sendAsFile = false;
- _loadItemsSema = dispatch_semaphore_create(0);
- }
- return self;
- }
- - (NSUInteger)itemCount {
- NSUInteger count = _itemsToSend.count;
- if (_textToSend.length > 0 && ![self canSendCaptions]) {
- count++;
- }
-
- return count;
- }
- - (BOOL)isFileItem:(IntermediateItem *)item {
- if ([UTIConverter type:item.type conformsTo:UTTYPE_AUDIO]) {
- return NO;
- } else if ([UTIConverter type:item.type conformsTo:UTTYPE_PLAIN_TEXT]) {
- return NO;
- } else if ([UTIConverter type:item.type conformsTo:UTTYPE_URL]) {
- return NO;
- }
-
- return YES;
- }
- - (void)addItem:(NSItemProvider *)itemProvider forType:(NSString *)type secondType:(NSString *)secondType {
- IntermediateItem *item = [IntermediateItem new];
- item.itemProvider = itemProvider;
- item.type = type;
- item.secondType = secondType;
-
- [_itemsToSend addObject:item];
-
- if ([self isFileItem:item]) {
- _containsFileItem = YES;
- }
-
- }
- - (void)addText:(NSString *)text {
- _textToSend = text;
- }
- - (void)sendItemsTo:(NSSet *)conversations {
- _recipientConversations = conversations;
- NSInteger count = [conversations count] * self.itemCount;
- _totalSendCount = count;
- _sentItemCount = 0;
- _correlationIDs = [[NSMutableArray alloc] initWithCapacity:conversations.count];
-
- for (int i = 0; i < conversations.count; i++) {
- _correlationIDs[i] = [ImageURLSenderItemCreator createCorrelationID];
- }
-
- if (_textToSend.length > 0) {
- if ([self canSendCaptions]) {
- IntermediateItem *anyItem = [_itemsToSend anyObject];
- anyItem.caption = _textToSend;
- } else {
- for (Conversation *conversation in _recipientConversations) {
- if (_shouldCancel) {
- return;
- }
-
- [self sendItem:_textToSend toConversation:conversation correlationID:nil];
- }
- }
- }
- dispatch_queue_t dispatchQueue = dispatch_queue_create("ch.threema.LoadItemsForShareExtension", NULL);
- dispatch_async(dispatchQueue, ^{
- for (IntermediateItem *intermediateItem in _itemsToSend) {
- [self loadAndSendItem:intermediateItem];
- }
- });
- }
- - (void)loadAndSendItem:(IntermediateItem *)intermediateItem {
- NSString *type = intermediateItem.type;
- if ([type isEqualToString:@"com.apple.live-photo"]) {
- type = intermediateItem.secondType;
- }
- if ([type isEqualToString:@"com.apple.avfoundation.urlasset"]) {
- type = intermediateItem.secondType;
- }
-
- [intermediateItem.itemProvider loadItemForTypeIdentifier:type options:nil completionHandler:^(id item, NSError *error) {
- if (error == nil && _shouldCancel == NO) {
- id senderItem = [self loadSenderItem:item ofType:intermediateItem.type secondType:intermediateItem.secondType];
- if (senderItem == nil) {
- return;
- }
- if (intermediateItem.caption && [senderItem isKindOfClass:[URLSenderItem class]]) {
- ((URLSenderItem*)senderItem).caption = intermediateItem.caption;
- }
-
- NSArray *recipients = [_recipientConversations allObjects];
- for (int i = 0; i < _recipientConversations.count; i++) {
- if (_shouldCancel) {
- return;
- }
- [self sendItem:senderItem toConversation:recipients[i] correlationID:_correlationIDs[i]];
- }
- }
- }];
- dispatch_semaphore_wait(_loadItemsSema, DISPATCH_TIME_FOREVER);
- }
- - (BOOL)canSendCaptionInline {
- if (_itemsToSend.count == 1) {
- IntermediateItem *anyItem = [_itemsToSend anyObject];
- if ([UTIConverter type:anyItem.type conformsTo:UTTYPE_GIF_IMAGE]) {
- return NO;
- }
- if ([UTIConverter type:anyItem.type conformsTo:UTTYPE_IMAGE]) {
- // Only one image, so we can send the text as an inline caption
- return YES;
- }
- }
- return NO;
- }
- - (BOOL)canSendCaptions {
- if (_itemsToSend.count == 1) {
- return [self canSendCaptionInline];
- }
- return false;
- }
- - (id)loadSenderItem:item ofType:(NSString *)type secondType:(NSString *)secondType {
- if ([item isKindOfClass:[NSURL class]]) {
- if (secondType != nil) {
- return [self prepareUrlItem:item forType:secondType];
- }
- return [self prepareUrlItem:item forType:type];
- } else if ([item isKindOfClass:[NSData class]]) {
- return [self prepareDataItem:item forType:type];
- } else if ([item isKindOfClass:[NSString class]]) {
- return item;
- } else if ([item isKindOfClass:[UIImage class]]) {
- return [self prepareImageItem:item forType:type];
- }
- else {
- NSString *title = NSLocalizedString(@"error_message_no_items_title", nil);
- NSString *message = NSLocalizedString(@"error_message_no_items_message", nil);
- [_delegate showAlertWithTitle:title message:message];
-
- return nil;
- }
- }
- - (void)sendItem:(id)senderItem toConversation:(Conversation *)conversation correlationID:(NSString *)correlationID {
- if ([senderItem isKindOfClass:[URLSenderItem class]]) {
- FileMessageSender *sender = [[FileMessageSender alloc] init];
- [sender sendItem:senderItem inConversation:conversation requestId:nil correlationId:correlationID];
- sender.uploadProgressDelegate = self;
- } else if ([senderItem isKindOfClass:[NSString class]]) {
- NSString *message = (NSString *)senderItem;
-
- // make sure DB object is created in main thread (to fetch KVO for keypath "sent")
-
- if (message && message.length) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [_delegate setProgress:[NSNumber numberWithFloat:0.1] forItem:[self progressItemKey:message conversation:conversation]];
- [MessageSender sendMessage:message inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
- [self awaitAckForMessageId:message.id];
- }];
- });
- } else {
- // increment sent count for unknown types
- [_delegate finishedItem:[self progressItemKey:message conversation:conversation]];
- _sentItemCount++;
- [self checkIsFinished];
- }
-
- } else {
- NSString *title = NSLocalizedString(@"error_message_no_items_title", nil);
- NSString *message = NSLocalizedString(@"error_message_no_items_message", nil);
- [_delegate showAlertWithTitle:title message:message];
-
- // increment sent count for unknown types
- _sentItemCount++;
- [self checkIsFinished];
- }
- }
- - (void)awaitAckForMessageId:(NSData *)messageId {
- EntityManager *entityManager = [[EntityManager alloc] init];
- BaseMessage *message = [entityManager.entityFetcher ownMessageWithId:messageId];
-
- [message addObserver:self forKeyPath:@"sent" options:0 context:nil];
- }
- #pragma mark - KVO
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
- if ([object isKindOfClass:[TextMessage class]]) {
- TextMessage *message = (TextMessage *)object;
-
- [_delegate finishedItem:[self progressItemKey:message.text conversation:message.conversation]];
-
- [message removeObserver:self forKeyPath:@"sent"];
-
- [[DatabaseManager dbManager] addDirtyObject:message.conversation];
- [[DatabaseManager dbManager] addDirtyObject:message.conversation.lastMessage];
-
- dispatch_semaphore_signal(_loadItemsSema);
-
- _sentItemCount++;
- [self checkIsFinished];
- }
- }
- - (id)progressItemKey:(id)item conversation:(Conversation *)conversation {
- NSInteger hash = [item hash] + [conversation hash];
- return [NSNumber numberWithInteger: hash];
- }
- - (NSString *)renderHtmlToText:(NSString *)htmlString {
- NSString *editedHtmlString = [htmlString stringByReplacingOccurrencesOfString:@"\n" withString:@"\</br>"];
- NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:[editedHtmlString dataUsingEncoding:NSUTF8StringEncoding] options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} documentAttributes:nil error:nil];
-
- if (attributedString != nil) {
- return [attributedString string];
- }
-
- return htmlString;
- }
- - (URLSenderItem *)prepareImageItem:(id<NSSecureCoding>)item forType:(NSString *)type {
- UIImage *image = (UIImage *)item;
- ImageURLSenderItemCreator *creator = [[ImageURLSenderItemCreator alloc] init];
-
- return [creator senderItemFromImage:image];
- }
- - (URLSenderItem *)prepareDataItem:(id<NSSecureCoding>)item forType:(NSString *)type {
- NSData *data = (NSData *)item;
- NSString *uti = [UTIConverter utiFromMimeType:type];
- if ([UTIConverter conformsToImageType:uti]) {
- ImageURLSenderItemCreator *creator = [[ImageURLSenderItemCreator alloc] init];
- return [creator senderItemFrom:data uti:uti];
- } else if ([UTIConverter conformsToMovieType:uti]) {
- VideoURLSenderItemCreator *creator = [[VideoURLSenderItemCreator alloc] init];
- NSURL *url = [VideoURLSenderItemCreator writeToTemporaryDirectoryWithData:data];
- if (url == nil) {
- DDLogError(@"Could not create URLSenderItem from media asset");
- return nil;
- }
- return [creator senderItemFrom:url];
- }
- return [URLSenderItem itemWithData:data fileName:nil type:type renderType:@0 sendAsFile:_sendAsFile];
- }
- - (id)prepareUrlItem:(id<NSSecureCoding>)item forType:(NSString *)type {
- NSURL *url = (NSURL *)item;
-
- if (url == nil) {
- return false;
- }
-
- if ([url.scheme isEqualToString:@"file"]) {
- URLSenderItem *senderItem = [URLSenderItemCreator getSenderItemFor:url maxSize:@"large"];
-
- if ([self checkFileConstraints:senderItem]) {
- return senderItem;
- }
- } else {
- return url.absoluteString;
- }
-
- return nil;
- }
- - (BOOL)checkFileConstraints:(URLSenderItem *)senderItem {
- NSString *errorTitle;
- NSString *errorMessage;
-
- if ([UTIConverter type:senderItem.type conformsTo:UTTYPE_MOVIE]) {
- if ([MediaConverter isVideoDurationValidAtUrl:senderItem.url] == NO) {
- errorTitle = [BundleUtil localizedStringForKey:@"video_too_long_title"];
- errorMessage = [NSString stringWithFormat:[BundleUtil localizedStringForKey:@"video_too_long_message"], [MediaConverter videoMaxDurationAtCurrentQuality]];
- }
- } else {
- NSDictionary *fileDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:senderItem.url.path error:nil];
- if ([fileDictionary fileSize] > kMaxFileSize) {
- errorTitle = [BundleUtil localizedStringForKey:@"error_constraints_failed"];
- errorMessage = [FileMessageSender messageForError:UploadErrorFileTooBig];
- }
- }
-
- if (errorMessage) {
- // cancel everything in case of error
- _shouldCancel = YES;
-
- [_delegate showAlertWithTitle:errorTitle message:errorMessage];
-
- return NO;
- }
-
- return YES;
- }
- - (void)checkIsFinished {
- if (_sentItemCount == _totalSendCount) {
- // delay finish slightly since DB safe of ack might not have finished yet
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
- [_delegate setFinished];
- });
- }
- }
- #pragma mark - UploadProgressDelegate
- - (BOOL)blobMessageSenderUploadShouldCancel:(BlobMessageSender *)blobMessageSender {
- return _shouldCancel;
- }
- - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadProgress:(NSNumber *)progress forMessage:(BaseMessage *)message {
- [_delegate setProgress:progress forItem:message.id];
- }
- - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadSucceededForMessage:(BaseMessage *)message {
- dispatch_semaphore_signal(_loadItemsSema);
- _sentItemCount++;
- [_delegate finishedItem:message.id];
-
- [[DatabaseManager dbManager] addDirtyObject:message.conversation];
- [[DatabaseManager dbManager] addDirtyObject:message];
-
- [self checkIsFinished];
- }
- - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadFailedForMessage:(BaseMessage *)message error:(UploadError)error {
- dispatch_semaphore_signal(_loadItemsSema);
- _sentItemCount++;
-
- NSString *errorTitle = [BundleUtil localizedStringForKey:@"error_sending_failed"];
- NSString *errorMessage = [FileMessageSender messageForError:error];
- [_delegate showAlertWithTitle:errorTitle message:errorMessage];
-
- if (error == UploadErrorSendFailed) {
- [[DatabaseManager dbManager] addDirtyObject:message.conversation];
- [[DatabaseManager dbManager] addDirtyObject:message];
- }
- }
- @end
|