SendMediaAction.m 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-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 "SendMediaAction.h"
  21. #import <MobileCoreServices/MobileCoreServices.h>
  22. #import "UserSettings.h"
  23. #import "AppDelegate.h"
  24. #import "Threema-Swift.h"
  25. #import <Photos/Photos.h>
  26. #import "ModalPresenter.h"
  27. #import "UIDefines.h"
  28. #import "FileMessageSender.h"
  29. #import "MBProgressHUD.h"
  30. #import "BundleUtil.h"
  31. #import "UTIConverter.h"
  32. #import "MediaConverter.h"
  33. #import "MessageSender.h"
  34. #ifdef DEBUG
  35. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  36. #else
  37. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  38. #endif
  39. @interface SendMediaAction () <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIVideoEditorControllerDelegate, UploadProgressDelegate, VideoConversionProgressDelegate>
  40. @property UIImagePickerController *picker;
  41. @property BOOL pickedVideoSent;
  42. @property BOOL pickedVideoSaved;
  43. @property BOOL cancelled;
  44. @property NSURL *pickedVideoURL;
  45. @property NSMutableSet *videoEncoders;
  46. @property NSMutableSet *fileMessageSenders;
  47. @property MBProgressHUD *videoEncodeProgressHUD;
  48. @property PhotosAccessHelper *helper;
  49. @property NSTimer *sequentialSendTimer;
  50. @property dispatch_semaphore_t sequentialSema;
  51. @end
  52. @implementation SendMediaAction
  53. - (instancetype)init
  54. {
  55. self = [super init];
  56. if (self) {
  57. self.videoEncoders = [NSMutableSet set];
  58. self.fileMessageSenders = [NSMutableSet set];
  59. }
  60. return self;
  61. }
  62. - (NSArray *)diffSelection:(NSArray *)assets fromPreviouslySelected:(NSArray *)prevSelected {
  63. if (prevSelected == nil) {
  64. return assets;
  65. }
  66. NSMutableArray *urlItems = [[NSMutableArray alloc] init];
  67. for (int i = 0; i < prevSelected.count; i++) {
  68. if ([MediaPreviewViewController isURLItemWithItem:prevSelected[i]]) {
  69. [urlItems addObject:prevSelected[i]];
  70. }
  71. }
  72. long selectionCount = assets.count + urlItems.count;
  73. NSMutableArray *selection = [[NSMutableArray alloc] initWithCapacity:selectionCount];
  74. for (int i = 0; i < urlItems.count; i++) {
  75. [selection insertObject:urlItems[i] atIndex:i];
  76. }
  77. for (long i = 0; i < assets.count; i++) {
  78. long found = [MediaPreviewViewController containsWithAsset:assets[i] itemList:prevSelected];
  79. if (found == -1) {
  80. [selection insertObject:assets[i] atIndex:i + urlItems.count];
  81. } else {
  82. [selection insertObject:prevSelected[found] atIndex:i + urlItems.count];
  83. }
  84. }
  85. return selection;
  86. }
  87. - (void)executeAction {
  88. if (_mediaPickerType == MediaPickerChooseExisting) {
  89. [self showPhotoPicker:nil];
  90. } else if (_mediaPickerType == MediaPickerTakePhoto) {
  91. _picker = [[UIImagePickerController alloc] init];
  92. _picker.delegate = self;
  93. if (_mediaPickerType == MediaPickerChooseExisting)
  94. _picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
  95. else
  96. _picker.sourceType = UIImagePickerControllerSourceTypeCamera;
  97. NSMutableArray *myMediaTypes = [NSMutableArray array];
  98. NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:_picker.sourceType];
  99. if ([availableMediaTypes containsObject:(NSString *)kUTTypeImage]) {
  100. [myMediaTypes addObject:(NSString *)kUTTypeImage];
  101. }
  102. if ([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]) {
  103. [myMediaTypes addObject:(NSString *)kUTTypeMovie];
  104. }
  105. _picker.mediaTypes = myMediaTypes;
  106. _picker.videoMaximumDuration = [MediaConverter videoMaxDurationAtCurrentQuality];
  107. /* Always request high quality from UIImagePickerController, and transcode by ourselves later */
  108. _picker.videoQuality = UIImagePickerControllerQualityTypeHigh;
  109. UIImagePickerControllerCameraFlashMode flashmode = UIImagePickerControllerCameraFlashModeAuto;
  110. if ([[[[AppGroup userDefaults] dictionaryRepresentation] allKeys] containsObject:@"cameraFlashMode"]) {
  111. flashmode = (UIImagePickerControllerCameraFlashMode) [[AppGroup userDefaults] integerForKey:@"cameraFlashMode"];
  112. }
  113. _picker.cameraFlashMode = flashmode;
  114. CGRect rect = [self.chatViewController.view convertRect:self.chatViewController.chatBar.addButton.frame fromView:self.chatViewController.chatBar];
  115. [ModalPresenter present:_picker on:self.chatViewController fromRect:rect inView:self.chatViewController.view completion:nil];
  116. } else {
  117. DDLogError(@"Invalid MediaPickerType");
  118. }
  119. }
  120. - (void)showPhotoPicker:(NSArray *)lastSelection {
  121. __block NSArray *prevSelected = [[NSArray alloc] init];
  122. int limit = 50;
  123. _helper = [[PhotosAccessHelper alloc] initWithCompletion:^(NSArray * _Nonnull assets, DKImagePickerController * pickerController) {
  124. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MediaShareStoryboard" bundle:nil];
  125. MediaPreviewViewController *selectionViewController = [storyboard instantiateViewControllerWithIdentifier:@"MediaShareController"];
  126. if (assets.count > 0) {
  127. if ([assets.firstObject isKindOfClass:DKAsset.class]) {
  128. UIViewController *dismissable = self.chatViewController.presentedViewController.presentingViewController;
  129. NSArray *initialSelection = [self diffSelection:assets fromPreviouslySelected:prevSelected];
  130. [selectionViewController initWithMediaWithDataArray:initialSelection delegate:self completion:^(NSArray *selection, BOOL sendAsFile, NSArray *captions) {
  131. dispatch_async(dispatch_get_main_queue(), ^{
  132. [dismissable dismissViewControllerAnimated:true completion:nil];
  133. });
  134. [self sendAssets:selection asFile:sendAsFile withCaptions: captions];
  135. } returnToMe: ^(NSArray *defaultSelection, NSArray *prevSelection) {
  136. dispatch_async(dispatch_get_main_queue(), ^{
  137. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  138. prevSelected = [NSMutableArray arrayWithArray:prevSelection];
  139. [pickerController setDefaultSelectedAssets:defaultSelection];
  140. [((ThreemaImagePickerControllerDefaultUIDelegate *) pickerController.UIDelegate) updateButton];
  141. });
  142. } addMore: ^(NSArray *defaultSelection, NSArray *prevSelection) {
  143. dispatch_async(dispatch_get_main_queue(), ^{
  144. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  145. prevSelected = [NSMutableArray arrayWithArray:prevSelection];
  146. [pickerController setDefaultSelectedAssets:defaultSelection];
  147. [((ThreemaImagePickerControllerDefaultUIDelegate *) pickerController.UIDelegate) updateButton];
  148. });
  149. }];
  150. dispatch_async(dispatch_get_main_queue(), ^{
  151. selectionViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
  152. [self.chatViewController.presentedViewController presentViewController:selectionViewController animated:YES completion:nil];
  153. });
  154. } else {
  155. if (lastSelection != nil) {
  156. assets = [lastSelection arrayByAddingObjectsFromArray:assets];
  157. }
  158. [selectionViewController initWithMediaWithDataArray:assets delegate:self completion:^(NSArray *selection, BOOL sendAsFile, NSArray *captions) {
  159. dispatch_async(dispatch_get_main_queue(), ^{
  160. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  161. });
  162. [self sendAssets:selection asFile:sendAsFile withCaptions: captions];
  163. } returnToMe: ^(__unused NSArray *defaultSelection, NSArray *prevSelection) {
  164. dispatch_async(dispatch_get_main_queue(), ^{
  165. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  166. });
  167. [self clearTemporaryDirectoryItems:prevSelection];
  168. } addMore: ^(__unused NSArray *defaultSelection, NSArray *prevSelection) {
  169. dispatch_async(dispatch_get_main_queue(), ^{
  170. prevSelected = [NSMutableArray arrayWithArray:prevSelection];
  171. _helper = [[PhotosAccessHelper alloc] initWithCompletion:^(NSArray * _Nonnull defaultAssets, __unused DKImagePickerController *returnedPickerController) {
  172. NSArray *newAssets = [prevSelection arrayByAddingObjectsFromArray:defaultAssets];
  173. [selectionViewController resetMediaToDataArray:newAssets reloadData:true];
  174. }];
  175. [_helper showPickerWithViewController:selectionViewController limit:50-prevSelection.count];
  176. });
  177. }];
  178. dispatch_async(dispatch_get_main_queue(), ^{
  179. [selectionViewController setBackIsCancel:true];
  180. [self.chatViewController presentViewController:selectionViewController animated:YES completion:nil];
  181. });
  182. }
  183. }
  184. }];
  185. dispatch_async(dispatch_get_main_queue(), ^{
  186. [_helper showPickerWithViewController:self.chatViewController limit:limit];
  187. });
  188. }
  189. - (DKImagePickerController *)setupDKImagePickerController {
  190. DKImagePickerController *pickerController = [DKImagePickerController new];
  191. pickerController.assetType = DKImagePickerControllerAssetTypeAllAssets;
  192. pickerController.showsCancelButton = YES;
  193. pickerController.showsEmptyAlbums = NO;
  194. pickerController.allowMultipleTypes = YES;
  195. pickerController.autoDownloadWhenAssetIsInCloud = YES;
  196. pickerController.defaultSelectedAssets = @[];
  197. pickerController.sourceType = DKImagePickerControllerSourceTypePhoto;
  198. pickerController.maxSelectableCount = 50;
  199. pickerController.UIDelegate = [[ThreemaImagePickerControllerDefaultUIDelegate alloc] init];
  200. pickerController.allowsLandscape = YES;
  201. return pickerController;
  202. }
  203. - (void)showAssetPickerWithAssets:(NSArray *)assets {
  204. DKImagePickerController *pickerController = [self setupDKImagePickerController];
  205. [self.chatViewController presentViewController:pickerController animated:YES completion:nil];
  206. }
  207. - (void)showPreviewForAssets:(NSArray *)assets {
  208. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MediaShareStoryboard" bundle:nil];
  209. MediaPreviewViewController *selectionViewController = [storyboard instantiateViewControllerWithIdentifier:@"MediaShareController"];
  210. selectionViewController.backIsCancel = true;
  211. [selectionViewController initWithMediaWithDataArray:assets delegate:self completion:^(NSArray *selection, BOOL sendAsFile, NSArray *captions) {
  212. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  213. [self sendAssets:selection asFile:sendAsFile withCaptions: captions];
  214. } returnToMe: ^(__unused NSArray *defaultSelection, NSArray *prevSelection) {
  215. dispatch_async(dispatch_get_main_queue(), ^{
  216. [selectionViewController dismissViewControllerAnimated:true completion:nil];
  217. });
  218. [self clearTemporaryDirectoryItems:prevSelection];
  219. } addMore: ^(NSArray *defaultSelection, NSArray *prevSelection) {
  220. DKImagePickerController *pickerController = [self setupDKImagePickerController];
  221. pickerController.defaultSelectedAssets = defaultSelection;
  222. [selectionViewController presentViewController:pickerController animated:YES completion:nil];
  223. __weak typeof(pickerController) weakPickerController = pickerController;
  224. [pickerController setDidSelectAssets:^(NSArray * __nonnull selectedAssetes) {
  225. [weakPickerController dismissViewControllerAnimated:true completion:nil];
  226. NSArray *selection = [self diffSelection:selectedAssetes fromPreviouslySelected:prevSelection];
  227. [selectionViewController resetMediaToDataArray:selection reloadData:true];
  228. }];
  229. }];
  230. [self.chatViewController presentViewController:selectionViewController animated:YES completion:nil];
  231. }
  232. - (void)prepareAssets:(NSArray *)assets withTarget:(NSMutableArray *)itemArray asFile:(bool) sendAsFile {
  233. for (int i = 0; i < assets.count; i++) {
  234. @autoreleasepool {
  235. if ([assets[i] isKindOfClass:PHAsset.class]) {
  236. PHAsset *asset = assets[i];
  237. if (asset.mediaType == PHAssetMediaTypeImage) {
  238. URLSenderItem *item = [self getSenderItemForImageAsset:asset asFile:sendAsFile];
  239. [itemArray addObject:item];
  240. } else {
  241. URLSenderItem *item = [self getSenderItemForVideoAsset:asset asFile:sendAsFile];
  242. [itemArray addObject:item];
  243. }
  244. } else {
  245. NSURL *url = assets[i];
  246. NSString *uti = [UTIConverter utiForFileURL:url];
  247. if ([UTIConverter conformsToImageType:uti] || [UTIConverter conformsToMovieType:uti]) {
  248. URLSenderItem *item = [URLSenderItemCreator getSenderItemFor:url];
  249. if (item != nil) {
  250. [itemArray addObject:item];
  251. } else {
  252. DDLogError(@"Could not create URLSenderItem from media asset");
  253. }
  254. } else {
  255. DDLogError(@"Unrecognized media asset");
  256. }
  257. }
  258. dispatch_async(dispatch_get_main_queue(), ^{
  259. NSString *text = NSLocalizedString(@"processing_items_progress", nil);
  260. [self incrementVideoProgressHUDWithText:text placeholderIncluded:true];
  261. });
  262. }
  263. }
  264. }
  265. - (void)sendItems:(NSMutableArray *)itemArray asFile:(bool)sendAsFile withCaptions:(NSArray *)captions {
  266. long k = itemArray.count;
  267. dispatch_semaphore_t sema = dispatch_semaphore_create(0);
  268. _sequentialSema = dispatch_semaphore_create(1);
  269. NSString *correlationID = [ImageURLSenderItemCreator createCorrelationID];
  270. for (long i = 0; i < k; i++) {
  271. @autoreleasepool {
  272. dispatch_semaphore_wait(_sequentialSema, DISPATCH_TIME_FOREVER);
  273. FileMessageSender *sender = [[FileMessageSender alloc] init];
  274. [self.fileMessageSenders addObject:sender];
  275. if (self.cancelled) {
  276. dispatch_async(dispatch_get_main_queue(), ^{
  277. [self hideVideoEncodeProgressHUD];
  278. });
  279. return;
  280. }
  281. if ([itemArray[i] isKindOfClass:[URLSenderItem class]]) {
  282. URLSenderItem *item = itemArray[i];
  283. if (![captions[i] isEqualToString:@""]) {
  284. item.caption = captions[i];
  285. }
  286. sender.uploadProgressDelegate = self;
  287. [sender sendItem:item
  288. inConversation:self.chatViewController.conversation
  289. requestId:nil
  290. correlationId:correlationID];
  291. } else {
  292. // Video
  293. AVAsset *item = itemArray[i];
  294. [self sendVideoAsset:item onCompletion:^{
  295. dispatch_semaphore_signal(sema);
  296. }];
  297. dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
  298. _sequentialSendTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
  299. target:self
  300. selector:@selector(checkVideoDone)
  301. userInfo:nil
  302. repeats:YES];
  303. if (![captions[i] isEqualToString:@""]) {
  304. [MessageSender sendMessage:captions[i]
  305. inConversation:self.chatViewController.conversation
  306. async:false
  307. quickReply:false
  308. requestId:nil
  309. onCompletion:^(__unused TextMessage *message, __unused Conversation *conv) {
  310. ;//nop
  311. }];
  312. }
  313. }
  314. dispatch_async(dispatch_get_main_queue(), ^{
  315. NSString *text = NSLocalizedString(@"sending_item", nil);
  316. [self incrementVideoProgressHUDWithText:text placeholderIncluded:false];
  317. });
  318. }
  319. }
  320. }
  321. - (void)sendAssets:(NSArray *)assets asFile:(bool)sendAsFile withCaptions:(NSArray *)captions {
  322. NSMutableArray *itemArray = [[NSMutableArray alloc] init];
  323. NSString *text = NSLocalizedString(@"processing_items_progress", nil);
  324. [self showVideoEncodeProgressHUDWithUnitCount:assets.count text:text];
  325. self.videoEncodeProgressHUD.progressObject.totalUnitCount = assets.count;
  326. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(void) {
  327. [self prepareAssets:assets withTarget:itemArray asFile:sendAsFile];
  328. dispatch_async(dispatch_get_main_queue(), ^{
  329. NSString *text = NSLocalizedString(@"sending_item", nil);
  330. [self hideVideoEncodeProgressHUD];
  331. [self showVideoEncodeProgressHUDWithUnitCount:assets.count text:text];
  332. });
  333. [self sendItems:itemArray asFile:sendAsFile withCaptions:captions];
  334. double delayInSeconds = 1.0;
  335. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  336. dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
  337. [self hideVideoEncodeProgressHUD];
  338. });
  339. [self clearTemporaryDirectoryItems:assets];
  340. });
  341. }
  342. - (void)clearTemporaryDirectoryItems: (NSArray *)items {
  343. dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT , 0), ^(void){
  344. DDLogInfo(@"Started clearing items in temporary directory.");
  345. NSFileManager *defaultManager = NSFileManager.defaultManager;
  346. NSError *err;
  347. for (int i = 0; i < items.count; i++) {
  348. if ([items[i] isKindOfClass:NSURL.class]) {
  349. [defaultManager removeItemAtURL:items[i] error:&err];
  350. if (err != nil) {
  351. DDLogError(@"Could not clear item in temporary directory. Error: %@", err.description);
  352. }
  353. }
  354. }
  355. });
  356. }
  357. - (void)checkVideoDone {
  358. bool done = true;
  359. for (SDAVAssetExportSession *exportSession in self.videoEncoders) {
  360. done = exportSession.progress == 1.0;
  361. }
  362. if (self.videoEncoders.count == 0 && done) {
  363. [_sequentialSendTimer invalidate];
  364. }
  365. }
  366. - (URLSenderItem *)getSenderItemForImageAsset:(PHAsset *)asset asFile:(bool)sendAsFile {
  367. __block URLSenderItem *item;
  368. dispatch_semaphore_t sema = dispatch_semaphore_create(0);
  369. PHImageManager *imageManager = [PHImageManager defaultManager];
  370. PHImageRequestOptions *options = [PHImageRequestOptions new];
  371. options.networkAccessAllowed = YES;
  372. options.version = PHImageRequestOptionsVersionCurrent;
  373. [imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, __unused UIImageOrientation orientation, __unused NSDictionary *info) {
  374. if (imageData) {
  375. NSArray *resources = [PHAssetResource assetResourcesForAsset:asset];
  376. NSString *orgFilename = @"File";
  377. if (resources.count > 0) {
  378. orgFilename = ((PHAssetResource*)resources[0]).originalFilename;
  379. }
  380. if (sendAsFile) {
  381. item = [URLSenderItem itemWithData:imageData fileName:orgFilename type:dataUTI renderType:@0 sendAsFile:true];
  382. } else {
  383. ImageURLSenderItemCreator *itemCreator = [[ImageURLSenderItemCreator alloc] init];
  384. item = [itemCreator senderItemFrom:imageData uti:dataUTI];
  385. }
  386. dispatch_semaphore_signal(sema);
  387. }
  388. }];
  389. dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
  390. return item;
  391. }
  392. - (URLSenderItem *)getSenderItemForVideoAsset:(PHAsset *)asset asFile:(bool)sendAsFile {
  393. PHImageManager *imageManager = [PHImageManager defaultManager];
  394. __block URLSenderItem *senderItem;
  395. dispatch_semaphore_t sema = dispatch_semaphore_create(0);
  396. PHVideoRequestOptions *options = [PHVideoRequestOptions new];
  397. options.version = PHVideoRequestOptionsVersionCurrent;
  398. options.networkAccessAllowed = YES;
  399. [imageManager requestAVAssetForVideo:asset options:options resultHandler:^(AVAsset *videoAsset, __unused AVAudioMix *audioMix, __unused NSDictionary *info) {
  400. VideoURLSenderItemCreator *senderCreator = [[VideoURLSenderItemCreator alloc] init];
  401. senderItem = [senderCreator senderItemFromAsset:videoAsset];
  402. dispatch_semaphore_signal(sema);
  403. }];
  404. dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
  405. return senderItem;
  406. }
  407. #pragma mark - Image picker controller delegate
  408. - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
  409. [self storeFlashConfigurationFor:picker];
  410. if ([[info objectForKey:UIImagePickerControllerMediaType] isEqualToString:(NSString*)kUTTypeImage]) {
  411. /* image picked */
  412. UIImage *pickedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
  413. if (picker.sourceType == UIImagePickerControllerSourceTypeCamera && [UserSettings sharedUserSettings].autoSaveMedia) {
  414. [[AlbumManager shared] saveWithImage:pickedImage];
  415. }
  416. if (picker.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) {
  417. /* need to ask for confirmation when picking images from library, as it's much too
  418. easy to send the wrong picture with one tap */
  419. PreviewImageViewController *previewVc = [self.chatViewController.storyboard instantiateViewControllerWithIdentifier:@"PreviewImage"];
  420. previewVc.delegate = self.chatViewController;
  421. previewVc.image = pickedImage;
  422. [picker pushViewController:previewVc animated:YES];
  423. } else {
  424. NSURL *imageUrl = [PhotosAccessHelper storeImageToTmprDirWithImageData:pickedImage];
  425. NSArray *array = [NSArray arrayWithObject:imageUrl];
  426. [picker dismissViewControllerAnimated:true completion:^{
  427. [self showPreviewForAssets:array];
  428. }];
  429. }
  430. } else if ([[info objectForKey:UIImagePickerControllerMediaType] isEqualToString:(NSString*)kUTTypeMovie]) {
  431. /* video picked */
  432. _pickedVideoURL = [info objectForKey:UIImagePickerControllerMediaURL];
  433. _pickedVideoSent = NO;
  434. _pickedVideoSaved = NO;
  435. if (picker.sourceType == UIImagePickerControllerSourceTypeCamera && [UserSettings sharedUserSettings].autoSaveMedia) {
  436. [[AlbumManager shared] saveMovieToLibraryWithMovieURL:_pickedVideoURL completionHandler:^(BOOL success) {
  437. _pickedVideoSaved = YES;
  438. if (_pickedVideoSent && _pickedVideoSaved)
  439. [[NSFileManager defaultManager] removeItemAtURL:_pickedVideoURL error:nil];
  440. }];
  441. } else {
  442. _pickedVideoSaved = YES;
  443. }
  444. /* check video duration - if this has come from the photo library, the video may be longer than
  445. videoMaximumDuration (it is not enforced if allowEditing = NO, but we don't want to enable that
  446. as we don't need the image cropping UI) */
  447. if ([MediaConverter isVideoDurationValidAtUrl:_pickedVideoURL]) {
  448. NSArray *array = [NSArray arrayWithObject:_pickedVideoURL];
  449. [picker dismissViewControllerAnimated:true completion:^{
  450. [self showPreviewForAssets:array];
  451. }];
  452. } else {
  453. /* video too long - must present editor */
  454. UIVideoEditorController *videoEditor = [[UIVideoEditorController alloc] init];
  455. videoEditor.videoQuality = UIImagePickerControllerQualityTypeHigh;
  456. videoEditor.videoMaximumDuration = [MediaConverter videoMaxDurationAtCurrentQuality] * 60;
  457. videoEditor.videoPath = [_pickedVideoURL path];
  458. videoEditor.delegate = self;
  459. [ModalPresenter dismissPresentedControllerOn:self.chatViewController animated:YES completion:^{
  460. _picker = nil;
  461. [self.chatViewController presentViewController:videoEditor animated:YES completion:nil];
  462. }];
  463. }
  464. }
  465. }
  466. - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
  467. [self storeFlashConfigurationFor:picker];
  468. [ModalPresenter dismissPresentedControllerOn:self.chatViewController animated:YES completion:^{
  469. _picker = nil;
  470. }];
  471. }
  472. - (void)storeFlashConfigurationFor:(UIImagePickerController *) picker {
  473. if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
  474. UIImagePickerControllerCameraFlashMode flashmode = picker.cameraFlashMode;
  475. [[AppGroup userDefaults] setInteger:flashmode forKey:@"cameraFlashMode"];
  476. [[AppGroup userDefaults] synchronize];
  477. }
  478. }
  479. #pragma mark - Media sending utility functions
  480. - (void)sendPickedImage:(UIImage *)image {
  481. if (_picker != nil) {
  482. [ModalPresenter dismissPresentedControllerOn:self.chatViewController animated:YES completion:^{
  483. _picker = nil;
  484. }];
  485. }
  486. if (image == nil) {
  487. return;
  488. }
  489. dispatch_async(dispatch_get_main_queue(), ^{
  490. ImageURLSenderItemCreator *item = [[ImageURLSenderItemCreator alloc] init];
  491. URLSenderItem *senderItem = [item senderItemFromImage:image];
  492. FileMessageSender *sender = [[FileMessageSender alloc] init];
  493. [sender sendItem:senderItem inConversation:self.chatViewController.conversation requestId:nil];
  494. });
  495. }
  496. - (void)video:(NSString*)videoPath didFinishSavingWithError:(NSError*)error contextInfo:(void*)contextInfo {
  497. _pickedVideoSaved = YES;
  498. if (_pickedVideoSent && _pickedVideoSaved)
  499. [[NSFileManager defaultManager] removeItemAtURL:_pickedVideoURL error:nil];
  500. }
  501. - (void)sendPickedVideo {
  502. if (_picker != nil) {
  503. _picker = nil;
  504. }
  505. [self.chatViewController dismissViewControllerAnimated:YES completion:nil];
  506. if (_pickedVideoURL == nil)
  507. return;
  508. AVURLAsset *asset = [AVURLAsset URLAssetWithURL:_pickedVideoURL options:nil];
  509. [self sendVideoAsset:asset onCompletion:^{
  510. _pickedVideoSent = YES;
  511. if (_pickedVideoSent && _pickedVideoSaved)
  512. [[NSFileManager defaultManager] removeItemAtURL:_pickedVideoURL error:nil];
  513. }];
  514. }
  515. - (void)sendVideoAsset:(AVAsset*)asset onCompletion:(void(^)(void))onCompletion {
  516. dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  517. VideoURLSenderItemCreator *senderCreator = [[VideoURLSenderItemCreator alloc] init];
  518. senderCreator.encodeProgressDelegate = self;
  519. SDAVAssetExportSession *exportSession = [senderCreator getExportSessionFor:asset];
  520. if (exportSession == nil) {
  521. DDLogError(@"VideoURL was nil.");
  522. return;
  523. }
  524. [self.videoEncoders addObject:exportSession];
  525. URLSenderItem *senderItem = [senderCreator senderItemFrom:asset on:exportSession];
  526. FileMessageSender *sender = [[FileMessageSender alloc] init];
  527. sender.uploadProgressDelegate = self;
  528. [sender sendItem:senderItem inConversation:self.chatViewController.conversation requestId:nil];
  529. if (onCompletion != nil) {
  530. onCompletion();
  531. }
  532. });
  533. }
  534. - (void)videoExportSessionWithExportSession:(SDAVAssetExportSession * _Nonnull)exportSession {
  535. float progress = exportSession.progress;
  536. dispatch_async(dispatch_get_main_queue(), ^{
  537. if (progress == 1.0f) {
  538. [self.videoEncoders removeObject:exportSession];
  539. }
  540. });
  541. }
  542. - (void)showVideoEncodeProgressHUDWithUnitCount:(long)unitCount text:(NSString *)text {
  543. if (self.videoEncodeProgressHUD == nil) {
  544. self.videoEncodeProgressHUD = [MBProgressHUD showHUDAddedTo:self.chatViewController.view animated:YES];
  545. self.videoEncodeProgressHUD.mode = MBProgressHUDModeAnnularDeterminate;
  546. [self.videoEncodeProgressHUD.button setTitle:NSLocalizedString(@"cancel", nil) forState:UIControlStateNormal];
  547. [self.videoEncodeProgressHUD.button addTarget:self action:@selector(progressHUDCancelPressed) forControlEvents:UIControlEventTouchUpInside];
  548. self.videoEncodeProgressHUD.progressObject = [[NSProgress alloc] init];
  549. self.videoEncodeProgressHUD.progressObject.totalUnitCount = unitCount;
  550. long current = self.videoEncodeProgressHUD.progressObject.completedUnitCount;
  551. long total = self.videoEncodeProgressHUD.progressObject.totalUnitCount;
  552. [self updateHUDTextWithCompleted:current total:total text:text];
  553. }
  554. }
  555. - (void)hideVideoEncodeProgressHUD {
  556. if (self.videoEncodeProgressHUD != nil) {
  557. [self.videoEncodeProgressHUD hideAnimated:YES];
  558. self.videoEncodeProgressHUD = nil;
  559. }
  560. }
  561. - (void)incrementVideoProgressHUDWithText:(NSString *)text placeholderIncluded:(bool)placeholder {
  562. if (self.videoEncodeProgressHUD != nil) {
  563. self.videoEncodeProgressHUD.progressObject.completedUnitCount++;
  564. long current = self.videoEncodeProgressHUD.progressObject.completedUnitCount;
  565. long total = self.videoEncodeProgressHUD.progressObject.totalUnitCount;
  566. if (!placeholder) {
  567. text = [text stringByAppendingString:@" %d/%d"];
  568. }
  569. [self updateHUDTextWithCompleted:current total:total text:text];
  570. }
  571. }
  572. - (void)updateHUDTextWithCompleted:(long)completed total:(long)total text:(NSString *)text {
  573. self.videoEncodeProgressHUD.label.text = [NSString stringWithFormat:text, completed, total];
  574. }
  575. - (void)removeFileMessageSender:(FileMessageSender*)videoMessageSender {
  576. dispatch_async(dispatch_get_main_queue(), ^{
  577. [self.fileMessageSenders removeObject:videoMessageSender];
  578. });
  579. }
  580. - (void)progressHUDCancelPressed {
  581. for (FileMessageSender *fileMessageSender in self.fileMessageSenders) {
  582. [fileMessageSender uploadShouldCancel];
  583. }
  584. for (SDAVAssetExportSession *exportSession in self.videoEncoders) {
  585. [exportSession cancelExport];
  586. }
  587. __unused bool val = [VideoURLSenderItemCreator cleanTemporaryDirectory];
  588. self.cancelled = true;
  589. }
  590. #pragma mark - Video editor delegate
  591. - (void)videoEditorController:(UIVideoEditorController *)editor didSaveEditedVideoToPath:(NSString *)editedVideoPath {
  592. /* delete original video file */
  593. [[NSFileManager defaultManager] removeItemAtURL:_pickedVideoURL error:nil];
  594. _pickedVideoURL = [NSURL fileURLWithPath:editedVideoPath];
  595. [self sendPickedVideo];
  596. }
  597. - (void)videoEditorController:(UIVideoEditorController *)editor didFailWithError:(NSError *)error {
  598. DDLogWarn(@"Video editor failed: %@", error);
  599. [editor dismissViewControllerAnimated:YES completion:nil];
  600. }
  601. - (void)videoEditorControllerDidCancel:(UIVideoEditorController *)editor {
  602. [editor dismissViewControllerAnimated:YES completion:nil];
  603. }
  604. #pragma mark - UploadProgressDelegate
  605. - (void)blobMessageSender:(BlobMessageSender*)blobMessageSender uploadFailedForMessage:(BaseMessage *)message error:(UploadError)error {
  606. dispatch_async(dispatch_get_main_queue(), ^{
  607. if (error == UploadErrorCancelled) {
  608. return;
  609. }
  610. NSString *errorTitle = [BundleUtil localizedStringForKey:@"error_sending_failed"];
  611. NSString *errorMessage = [FileMessageSender messageForError:error];
  612. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:errorTitle message:errorMessage actionOk:nil];
  613. });
  614. self.cancelled = true;
  615. dispatch_semaphore_signal(_sequentialSema);
  616. }
  617. - (void)blobMessageSender:(BlobMessageSender*)blobMessageSender uploadSucceededForMessage:(BaseMessage *)message {
  618. if ([blobMessageSender isKindOfClass:FileMessageSender.class]) {
  619. [self removeFileMessageSender:(FileMessageSender*)blobMessageSender];
  620. }
  621. dispatch_semaphore_signal(_sequentialSema);
  622. }
  623. - (BOOL)blobMessageSenderUploadShouldCancel:(BlobMessageSender*)blobMessageSender {
  624. return NO;
  625. }
  626. - (void)blobMessageSender:(BlobMessageSender*)blobMessageSender uploadProgress:(NSNumber *)progress forMessage:(BaseMessage *)message {
  627. if ([blobMessageSender isKindOfClass:FileMessageSender.class]) {
  628. [self removeFileMessageSender:(FileMessageSender*)blobMessageSender];
  629. }
  630. }
  631. @end