SenderItemManager.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2016-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "SenderItemManager.h"
  21. #import "URLSenderItem.h"
  22. #import "UTIConverter.h"
  23. #import "Conversation.h"
  24. #import "BundleUtil.h"
  25. #import "MessageSender.h"
  26. #import "TextMessage.h"
  27. #import "DatabaseManager.h"
  28. #import "EntityManager.h"
  29. #import "MediaConverter.h"
  30. #import "FileMessageSender.h"
  31. #ifdef DEBUG
  32. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  33. #else
  34. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  35. #endif
  36. @interface IntermediateItem : NSObject
  37. @property NSItemProvider *itemProvider;
  38. @property NSString *type;
  39. @property NSString *secondType;
  40. @property NSString *caption;
  41. @end
  42. @implementation IntermediateItem
  43. @end
  44. @interface SenderItemManager () <UploadProgressDelegate>
  45. @property NSSet *recipientConversations;
  46. @property NSMutableSet *itemsToSend;
  47. @property NSString *textToSend;
  48. @property NSInteger sentItemCount;
  49. @property NSInteger totalSendCount;
  50. @property NSMutableArray *correlationIDs;
  51. @property dispatch_semaphore_t loadItemsSema;
  52. @end
  53. @implementation SenderItemManager
  54. - (instancetype)init
  55. {
  56. self = [super init];
  57. if (self) {
  58. _itemsToSend = [NSMutableSet set];
  59. _containsFileItem = NO;
  60. _shouldCancel = NO;
  61. _sendAsFile = false;
  62. _loadItemsSema = dispatch_semaphore_create(0);
  63. }
  64. return self;
  65. }
  66. - (NSUInteger)itemCount {
  67. NSUInteger count = _itemsToSend.count;
  68. if (_textToSend.length > 0 && ![self canSendCaptions]) {
  69. count++;
  70. }
  71. return count;
  72. }
  73. - (BOOL)isFileItem:(IntermediateItem *)item {
  74. if ([UTIConverter type:item.type conformsTo:UTTYPE_AUDIO]) {
  75. return NO;
  76. } else if ([UTIConverter type:item.type conformsTo:UTTYPE_PLAIN_TEXT]) {
  77. return NO;
  78. } else if ([UTIConverter type:item.type conformsTo:UTTYPE_URL]) {
  79. return NO;
  80. }
  81. return YES;
  82. }
  83. - (void)addItem:(NSItemProvider *)itemProvider forType:(NSString *)type secondType:(NSString *)secondType {
  84. IntermediateItem *item = [IntermediateItem new];
  85. item.itemProvider = itemProvider;
  86. item.type = type;
  87. item.secondType = secondType;
  88. [_itemsToSend addObject:item];
  89. if ([self isFileItem:item]) {
  90. _containsFileItem = YES;
  91. }
  92. }
  93. - (void)addText:(NSString *)text {
  94. _textToSend = text;
  95. }
  96. - (void)sendItemsTo:(NSSet *)conversations {
  97. _recipientConversations = conversations;
  98. NSInteger count = [conversations count] * self.itemCount;
  99. _totalSendCount = count;
  100. _sentItemCount = 0;
  101. _correlationIDs = [[NSMutableArray alloc] initWithCapacity:conversations.count];
  102. for (int i = 0; i < conversations.count; i++) {
  103. _correlationIDs[i] = [ImageURLSenderItemCreator createCorrelationID];
  104. }
  105. if (_textToSend.length > 0) {
  106. if ([self canSendCaptions]) {
  107. IntermediateItem *anyItem = [_itemsToSend anyObject];
  108. anyItem.caption = _textToSend;
  109. } else {
  110. for (Conversation *conversation in _recipientConversations) {
  111. if (_shouldCancel) {
  112. return;
  113. }
  114. [self sendItem:_textToSend toConversation:conversation correlationID:nil];
  115. }
  116. }
  117. }
  118. dispatch_queue_t dispatchQueue = dispatch_queue_create("ch.threema.LoadItemsForShareExtension", NULL);
  119. dispatch_async(dispatchQueue, ^{
  120. for (IntermediateItem *intermediateItem in _itemsToSend) {
  121. [self loadAndSendItem:intermediateItem];
  122. }
  123. });
  124. }
  125. - (void)loadAndSendItem:(IntermediateItem *)intermediateItem {
  126. NSString *type = intermediateItem.type;
  127. if ([type isEqualToString:@"com.apple.live-photo"]) {
  128. type = intermediateItem.secondType;
  129. }
  130. if ([type isEqualToString:@"com.apple.avfoundation.urlasset"]) {
  131. type = intermediateItem.secondType;
  132. }
  133. [intermediateItem.itemProvider loadItemForTypeIdentifier:type options:nil completionHandler:^(id item, NSError *error) {
  134. if (error == nil && _shouldCancel == NO) {
  135. id senderItem = [self loadSenderItem:item ofType:intermediateItem.type secondType:intermediateItem.secondType];
  136. if (senderItem == nil) {
  137. return;
  138. }
  139. if (intermediateItem.caption && [senderItem isKindOfClass:[URLSenderItem class]]) {
  140. ((URLSenderItem*)senderItem).caption = intermediateItem.caption;
  141. }
  142. NSArray *recipients = [_recipientConversations allObjects];
  143. for (int i = 0; i < _recipientConversations.count; i++) {
  144. if (_shouldCancel) {
  145. return;
  146. }
  147. [self sendItem:senderItem toConversation:recipients[i] correlationID:_correlationIDs[i]];
  148. }
  149. }
  150. }];
  151. dispatch_semaphore_wait(_loadItemsSema, DISPATCH_TIME_FOREVER);
  152. }
  153. - (BOOL)canSendCaptionInline {
  154. if (_itemsToSend.count == 1) {
  155. IntermediateItem *anyItem = [_itemsToSend anyObject];
  156. if ([UTIConverter type:anyItem.type conformsTo:UTTYPE_GIF_IMAGE]) {
  157. return NO;
  158. }
  159. if ([UTIConverter type:anyItem.type conformsTo:UTTYPE_IMAGE]) {
  160. // Only one image, so we can send the text as an inline caption
  161. return YES;
  162. }
  163. }
  164. return NO;
  165. }
  166. - (BOOL)canSendCaptions {
  167. if (_itemsToSend.count == 1) {
  168. return [self canSendCaptionInline];
  169. }
  170. return false;
  171. }
  172. - (id)loadSenderItem:item ofType:(NSString *)type secondType:(NSString *)secondType {
  173. if ([item isKindOfClass:[NSURL class]]) {
  174. if (secondType != nil) {
  175. return [self prepareUrlItem:item forType:secondType];
  176. }
  177. return [self prepareUrlItem:item forType:type];
  178. } else if ([item isKindOfClass:[NSData class]]) {
  179. return [self prepareDataItem:item forType:type];
  180. } else if ([item isKindOfClass:[NSString class]]) {
  181. return item;
  182. } else if ([item isKindOfClass:[UIImage class]]) {
  183. return [self prepareImageItem:item forType:type];
  184. }
  185. else {
  186. NSString *title = NSLocalizedString(@"error_message_no_items_title", nil);
  187. NSString *message = NSLocalizedString(@"error_message_no_items_message", nil);
  188. [_delegate showAlertWithTitle:title message:message];
  189. return nil;
  190. }
  191. }
  192. - (void)sendItem:(id)senderItem toConversation:(Conversation *)conversation correlationID:(NSString *)correlationID {
  193. if ([senderItem isKindOfClass:[URLSenderItem class]]) {
  194. FileMessageSender *sender = [[FileMessageSender alloc] init];
  195. [sender sendItem:senderItem inConversation:conversation requestId:nil correlationId:correlationID];
  196. sender.uploadProgressDelegate = self;
  197. } else if ([senderItem isKindOfClass:[NSString class]]) {
  198. NSString *message = (NSString *)senderItem;
  199. // make sure DB object is created in main thread (to fetch KVO for keypath "sent")
  200. if (message && message.length) {
  201. dispatch_async(dispatch_get_main_queue(), ^{
  202. [_delegate setProgress:[NSNumber numberWithFloat:0.1] forItem:[self progressItemKey:message conversation:conversation]];
  203. [MessageSender sendMessage:message inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  204. [self awaitAckForMessageId:message.id];
  205. }];
  206. });
  207. } else {
  208. // increment sent count for unknown types
  209. [_delegate finishedItem:[self progressItemKey:message conversation:conversation]];
  210. _sentItemCount++;
  211. [self checkIsFinished];
  212. }
  213. } else {
  214. NSString *title = NSLocalizedString(@"error_message_no_items_title", nil);
  215. NSString *message = NSLocalizedString(@"error_message_no_items_message", nil);
  216. [_delegate showAlertWithTitle:title message:message];
  217. // increment sent count for unknown types
  218. _sentItemCount++;
  219. [self checkIsFinished];
  220. }
  221. }
  222. - (void)awaitAckForMessageId:(NSData *)messageId {
  223. EntityManager *entityManager = [[EntityManager alloc] init];
  224. BaseMessage *message = [entityManager.entityFetcher ownMessageWithId:messageId];
  225. [message addObserver:self forKeyPath:@"sent" options:0 context:nil];
  226. }
  227. #pragma mark - KVO
  228. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  229. if ([object isKindOfClass:[TextMessage class]]) {
  230. TextMessage *message = (TextMessage *)object;
  231. [_delegate finishedItem:[self progressItemKey:message.text conversation:message.conversation]];
  232. [message removeObserver:self forKeyPath:@"sent"];
  233. [[DatabaseManager dbManager] addDirtyObject:message.conversation];
  234. [[DatabaseManager dbManager] addDirtyObject:message.conversation.lastMessage];
  235. dispatch_semaphore_signal(_loadItemsSema);
  236. _sentItemCount++;
  237. [self checkIsFinished];
  238. }
  239. }
  240. - (id)progressItemKey:(id)item conversation:(Conversation *)conversation {
  241. NSInteger hash = [item hash] + [conversation hash];
  242. return [NSNumber numberWithInteger: hash];
  243. }
  244. - (NSString *)renderHtmlToText:(NSString *)htmlString {
  245. NSString *editedHtmlString = [htmlString stringByReplacingOccurrencesOfString:@"\n" withString:@"\</br>"];
  246. NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:[editedHtmlString dataUsingEncoding:NSUTF8StringEncoding] options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} documentAttributes:nil error:nil];
  247. if (attributedString != nil) {
  248. return [attributedString string];
  249. }
  250. return htmlString;
  251. }
  252. - (URLSenderItem *)prepareImageItem:(id<NSSecureCoding>)item forType:(NSString *)type {
  253. UIImage *image = (UIImage *)item;
  254. ImageURLSenderItemCreator *creator = [[ImageURLSenderItemCreator alloc] init];
  255. return [creator senderItemFromImage:image];
  256. }
  257. - (URLSenderItem *)prepareDataItem:(id<NSSecureCoding>)item forType:(NSString *)type {
  258. NSData *data = (NSData *)item;
  259. NSString *uti = [UTIConverter utiFromMimeType:type];
  260. if ([UTIConverter conformsToImageType:uti]) {
  261. ImageURLSenderItemCreator *creator = [[ImageURLSenderItemCreator alloc] init];
  262. return [creator senderItemFrom:data uti:uti];
  263. } else if ([UTIConverter conformsToMovieType:uti]) {
  264. VideoURLSenderItemCreator *creator = [[VideoURLSenderItemCreator alloc] init];
  265. NSURL *url = [VideoURLSenderItemCreator writeToTemporaryDirectoryWithData:data];
  266. if (url == nil) {
  267. DDLogError(@"Could not create URLSenderItem from media asset");
  268. return nil;
  269. }
  270. return [creator senderItemFrom:url];
  271. }
  272. return [URLSenderItem itemWithData:data fileName:nil type:type renderType:@0 sendAsFile:_sendAsFile];
  273. }
  274. - (id)prepareUrlItem:(id<NSSecureCoding>)item forType:(NSString *)type {
  275. NSURL *url = (NSURL *)item;
  276. if (url == nil) {
  277. return false;
  278. }
  279. if ([url.scheme isEqualToString:@"file"]) {
  280. URLSenderItem *senderItem = [URLSenderItemCreator getSenderItemFor:url maxSize:@"large"];
  281. if ([self checkFileConstraints:senderItem]) {
  282. return senderItem;
  283. }
  284. } else {
  285. return url.absoluteString;
  286. }
  287. return nil;
  288. }
  289. - (BOOL)checkFileConstraints:(URLSenderItem *)senderItem {
  290. NSString *errorTitle;
  291. NSString *errorMessage;
  292. if ([UTIConverter type:senderItem.type conformsTo:UTTYPE_MOVIE]) {
  293. if ([MediaConverter isVideoDurationValidAtUrl:senderItem.url] == NO) {
  294. errorTitle = [BundleUtil localizedStringForKey:@"video_too_long_title"];
  295. errorMessage = [NSString stringWithFormat:[BundleUtil localizedStringForKey:@"video_too_long_message"], [MediaConverter videoMaxDurationAtCurrentQuality]];
  296. }
  297. } else {
  298. NSDictionary *fileDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:senderItem.url.path error:nil];
  299. if ([fileDictionary fileSize] > kMaxFileSize) {
  300. errorTitle = [BundleUtil localizedStringForKey:@"error_constraints_failed"];
  301. errorMessage = [FileMessageSender messageForError:UploadErrorFileTooBig];
  302. }
  303. }
  304. if (errorMessage) {
  305. // cancel everything in case of error
  306. _shouldCancel = YES;
  307. [_delegate showAlertWithTitle:errorTitle message:errorMessage];
  308. return NO;
  309. }
  310. return YES;
  311. }
  312. - (void)checkIsFinished {
  313. if (_sentItemCount == _totalSendCount) {
  314. // delay finish slightly since DB safe of ack might not have finished yet
  315. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
  316. [_delegate setFinished];
  317. });
  318. }
  319. }
  320. #pragma mark - UploadProgressDelegate
  321. - (BOOL)blobMessageSenderUploadShouldCancel:(BlobMessageSender *)blobMessageSender {
  322. return _shouldCancel;
  323. }
  324. - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadProgress:(NSNumber *)progress forMessage:(BaseMessage *)message {
  325. [_delegate setProgress:progress forItem:message.id];
  326. }
  327. - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadSucceededForMessage:(BaseMessage *)message {
  328. dispatch_semaphore_signal(_loadItemsSema);
  329. _sentItemCount++;
  330. [_delegate finishedItem:message.id];
  331. [[DatabaseManager dbManager] addDirtyObject:message.conversation];
  332. [[DatabaseManager dbManager] addDirtyObject:message];
  333. [self checkIsFinished];
  334. }
  335. - (void)blobMessageSender:(BlobMessageSender *)blobMessageSender uploadFailedForMessage:(BaseMessage *)message error:(UploadError)error {
  336. dispatch_semaphore_signal(_loadItemsSema);
  337. _sentItemCount++;
  338. NSString *errorTitle = [BundleUtil localizedStringForKey:@"error_sending_failed"];
  339. NSString *errorMessage = [FileMessageSender messageForError:error];
  340. [_delegate showAlertWithTitle:errorTitle message:errorMessage];
  341. if (error == UploadErrorSendFailed) {
  342. [[DatabaseManager dbManager] addDirtyObject:message.conversation];
  343. [[DatabaseManager dbManager] addDirtyObject:message];
  344. }
  345. }
  346. @end