9984b0e20233380427ea0477b66bcfb84b15f957
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / forms / WKFileUploadPanel.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKFileUploadPanel.h"
28
29 #if PLATFORM(IOS)
30
31 #import "APIArray.h"
32 #import "APIData.h"
33 #import "APIString.h"
34 #import "UIKitSPI.h"
35 #import "WKContentViewInteraction.h"
36 #import "WKData.h"
37 #import "WKStringCF.h"
38 #import "WKURLCF.h"
39 #import "WebOpenPanelParameters.h"
40 #import "WebOpenPanelResultListenerProxy.h"
41 #import "WebPageProxy.h"
42 #import <AVFoundation/AVFoundation.h>
43 #import <CoreMedia/CoreMedia.h>
44 #import <MobileCoreServices/MobileCoreServices.h>
45 #import <WebCore/LocalizedStrings.h>
46 #import <WebCore/SoftLinking.h>
47 #import <WebKit/WebNSFileManagerExtras.h>
48 #import <wtf/RetainPtr.h>
49
50 using namespace WebKit;
51
52 SOFT_LINK_FRAMEWORK(AVFoundation);
53 SOFT_LINK_CLASS(AVFoundation, AVAssetImageGenerator);
54 SOFT_LINK_CLASS(AVFoundation, AVURLAsset);
55
56 SOFT_LINK_FRAMEWORK(CoreMedia);
57 SOFT_LINK_CONSTANT(CoreMedia, kCMTimeZero, CMTime);
58 #define kCMTimeZero getkCMTimeZero()
59
60 #pragma mark - Icon generation
61
62 static CGRect squareCropRectForSize(CGSize size)
63 {
64     CGFloat smallerSide = MIN(size.width, size.height);
65     CGRect cropRect = CGRectMake(0, 0, smallerSide, smallerSide);
66
67     if (size.width < size.height)
68         cropRect.origin.y = rintf((size.height - smallerSide) / 2);
69     else
70         cropRect.origin.x = rintf((size.width - smallerSide) / 2);
71
72     return cropRect;
73 }
74
75 static UIImage *squareImage(UIImage *image)
76 {
77     if (!image)
78         return nil;
79
80     CGImageRef imageRef = [image CGImage];
81     CGSize imageSize = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
82     if (imageSize.width == imageSize.height)
83         return image;
84
85     CGRect squareCropRect = squareCropRectForSize(imageSize);
86     CGImageRef squareImageRef = CGImageCreateWithImageInRect(imageRef, squareCropRect);
87     UIImage *squareImage = [[UIImage alloc] initWithCGImage:squareImageRef imageOrientation:[image imageOrientation]];
88     CGImageRelease(squareImageRef);
89     return [squareImage autorelease];
90 }
91
92 static UIImage *thumbnailSizedImageForImage(UIImage *image)
93 {
94     UIImage *squaredImage = squareImage(image);
95     if (!squaredImage)
96         return nil;
97
98     CGRect destRect = CGRectMake(0, 0, 100, 100);
99     UIGraphicsBeginImageContext(destRect.size);
100     CGContextSetInterpolationQuality(UIGraphicsGetCurrentContext(), kCGInterpolationHigh);
101     [squaredImage drawInRect:destRect];
102     UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
103     UIGraphicsEndImageContext();
104     return resultImage;
105 }
106
107 static UIImage* fallbackIconForFile(NSURL *file)
108 {
109     ASSERT_ARG(file, [file isFileURL]);
110
111     UIDocumentInteractionController *interactionController = [UIDocumentInteractionController interactionControllerWithURL:file];
112     return thumbnailSizedImageForImage(interactionController.icons[0]);
113 }
114
115 static UIImage* iconForImageFile(NSURL *file)
116 {
117     ASSERT_ARG(file, [file isFileURL]);
118
119     if (UIImage *image = [UIImage imageWithContentsOfFile:file.path])
120         return thumbnailSizedImageForImage(image);
121
122     LOG_ERROR("WKFileUploadPanel: Error creating thumbnail image for image: %@", file);
123     return fallbackIconForFile(file);
124 }
125
126 static UIImage* iconForVideoFile(NSURL *file)
127 {
128     ASSERT_ARG(file, [file isFileURL]);
129
130     RetainPtr<AVURLAsset> asset = adoptNS([allocAVURLAssetInstance() initWithURL:file options:nil]);
131     RetainPtr<AVAssetImageGenerator> generator = adoptNS([allocAVAssetImageGeneratorInstance() initWithAsset:asset.get()]);
132     [generator setAppliesPreferredTrackTransform:YES];
133
134     NSError *error = nil;
135     RetainPtr<CGImageRef> imageRef = adoptCF([generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:&error]);
136     if (!imageRef) {
137         LOG_ERROR("WKFileUploadPanel: Error creating image for video '%@': %@", file, error);
138         return fallbackIconForFile(file);
139     }
140
141     RetainPtr<UIImage> image = adoptNS([[UIImage alloc] initWithCGImage:imageRef.get()]);
142     return thumbnailSizedImageForImage(image.get());
143 }
144
145 static UIImage* iconForFile(NSURL *file)
146 {
147     ASSERT_ARG(file, [file isFileURL]);
148
149     NSString *fileExtension = file.pathExtension;
150     if (!fileExtension.length)
151         return nil;
152
153     RetainPtr<CFStringRef> fileUTI = adoptCF(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)fileExtension, 0));
154
155     if (UTTypeConformsTo(fileUTI.get(), kUTTypeImage))
156         return iconForImageFile(file);
157
158     if (UTTypeConformsTo(fileUTI.get(), kUTTypeMovie))
159         return iconForVideoFile(file);
160
161     return fallbackIconForFile(file);
162 }
163
164
165 #pragma mark - _WKFileUploadItem
166
167 @interface _WKFileUploadItem : NSObject
168 - (instancetype)initWithFileURL:(NSURL *)fileURL;
169 @property (nonatomic, readonly, getter=isVideo) BOOL video;
170 @property (nonatomic, readonly) NSURL *fileURL;
171 @property (nonatomic, readonly) UIImage *displayImage;
172 @end
173
174 @implementation _WKFileUploadItem {
175     RetainPtr<NSURL> _fileURL;
176 }
177
178 - (instancetype)initWithFileURL:(NSURL *)fileURL
179 {
180     if (!(self = [super init]))
181         return nil;
182
183     ASSERT([fileURL isFileURL]);
184     ASSERT([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]);
185     _fileURL = fileURL;
186     return self;
187 }
188
189 - (BOOL)isVideo
190 {
191     ASSERT_NOT_REACHED();
192     return NO;
193 }
194
195 - (NSURL *)fileURL
196 {
197     return _fileURL.get();
198 }
199
200 - (UIImage *)displayImage
201 {
202     ASSERT_NOT_REACHED();
203     return nil;
204 }
205
206 @end
207
208
209 @interface _WKImageFileUploadItem : _WKFileUploadItem
210 - (instancetype)initWithFileURL:(NSURL *)fileURL originalImage:(UIImage *)originalImage;
211 @end
212
213 @implementation _WKImageFileUploadItem {
214     RetainPtr<UIImage> _originalImage;
215 }
216
217 - (instancetype)initWithFileURL:(NSURL *)fileURL originalImage:(UIImage *)originalImage
218 {
219     if (!(self = [super initWithFileURL:fileURL]))
220         return nil;
221
222     _originalImage = originalImage;
223     return self;
224 }
225
226 - (BOOL)isVideo
227 {
228     return NO;
229 }
230
231 - (UIImage *)displayImage
232 {
233     return thumbnailSizedImageForImage(_originalImage.get());
234 }
235
236 @end
237
238
239 @interface _WKVideoFileUploadItem : _WKFileUploadItem
240 @end
241
242 @implementation _WKVideoFileUploadItem
243
244 - (BOOL)isVideo
245 {
246     return YES;
247 }
248
249 - (UIImage *)displayImage
250 {
251     return iconForVideoFile(self.fileURL);
252 }
253
254 @end
255
256
257 #pragma mark - WKFileUploadPanel
258
259 @interface WKFileUploadPanel () <UIPopoverControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, UIDocumentMenuDelegate>
260 @end
261
262 @implementation WKFileUploadPanel {
263     WKContentView *_view;
264     WebKit::WebOpenPanelResultListenerProxy* _listener;
265     RetainPtr<NSArray> _mimeTypes;
266     CGPoint _interactionPoint;
267     BOOL _allowMultipleFiles;
268     BOOL _usingCamera;
269     RetainPtr<UIImagePickerController> _imagePicker;
270     RetainPtr<UIViewController> _presentationViewController; // iPhone always. iPad for Fullscreen Camera.
271     RetainPtr<UIPopoverController> _presentationPopover; // iPad for action sheet and Photo Library.
272     RetainPtr<UIDocumentMenuViewController> _documentMenuController;
273     RetainPtr<UIAlertController> _actionSheetController;
274 }
275
276 - (instancetype)initWithView:(WKContentView *)view
277 {
278     if (!(self = [super init]))
279         return nil;
280     _view = view;
281     return self;
282 }
283
284 - (void)dealloc
285 {
286     [_imagePicker setDelegate:nil];
287     [_presentationPopover setDelegate:nil];
288     [_documentMenuController setDelegate:nil];
289
290     [super dealloc];
291 }
292
293 - (void)_dispatchDidDismiss
294 {
295     if ([_delegate respondsToSelector:@selector(fileUploadPanelDidDismiss:)])
296         [_delegate fileUploadPanelDidDismiss:self];
297 }
298
299 #pragma mark - Panel Completion (one of these must be called)
300
301 - (void)_cancel
302 {
303     _listener->cancel();
304     [self _dispatchDidDismiss];
305 }
306
307 - (void)_chooseFiles:(NSArray *)fileURLs displayString:(NSString *)displayString iconImage:(UIImage *)iconImage
308 {
309     NSUInteger count = [fileURLs count];
310     if (!count) {
311         [self _cancel];
312         return;
313     }
314
315     Vector<RefPtr<API::Object>> urls;
316     urls.reserveInitialCapacity(count);
317     for (NSURL *fileURL in fileURLs)
318         urls.uncheckedAppend(adoptRef(toImpl(WKURLCreateWithCFURL((CFURLRef)fileURL))));
319     RefPtr<API::Array> fileURLsRef = API::Array::create(WTF::move(urls));
320
321     NSData *jpeg = UIImageJPEGRepresentation(iconImage, 1.0);
322     RefPtr<API::Data> iconImageDataRef = adoptRef(toImpl(WKDataCreate(reinterpret_cast<const unsigned char*>([jpeg bytes]), [jpeg length])));
323
324     RefPtr<API::String> displayStringRef = adoptRef(toImpl(WKStringCreateWithCFString((CFStringRef)displayString)));
325
326     _listener->chooseFiles(fileURLsRef.get(), displayStringRef.get(), iconImageDataRef.get());
327     [self _dispatchDidDismiss];
328 }
329
330 #pragma mark - Present / Dismiss API
331
332 - (void)presentWithParameters:(WebKit::WebOpenPanelParameters*)parameters resultListener:(WebKit::WebOpenPanelResultListenerProxy*)listener
333 {
334     ASSERT(!_listener);
335
336     _listener = listener;
337     _allowMultipleFiles = parameters->allowMultipleFiles();
338     _interactionPoint = [_view lastInteractionLocation];
339
340     RefPtr<API::Array> acceptMimeTypes = parameters->acceptMIMETypes();
341     NSMutableArray *mimeTypes = [NSMutableArray arrayWithCapacity:acceptMimeTypes->size()];
342     for (const auto& mimeType : acceptMimeTypes->elementsOfType<API::String>())
343         [mimeTypes addObject:mimeType->string()];
344     _mimeTypes = adoptNS([mimeTypes copy]);
345
346     // FIXME: Remove this check and the fallback code when a new SDK is available. <rdar://problem/20150072>
347     if ([UIDocumentMenuViewController instancesRespondToSelector:@selector(_setIgnoreApplicationEntitlementForImport:)]) {
348         [self _showDocumentPickerMenu];
349         return;
350     }
351
352     // Fall back to showing the old-style source selection sheet.
353     // If there is no camera or this is type=multiple, just show the image picker for the photo library.
354     // Otherwise, show an action sheet for the user to choose between camera or library.
355     if (_allowMultipleFiles || ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
356         [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
357     else
358         [self _showMediaSourceSelectionSheet];
359 }
360
361 - (void)dismiss
362 {
363     [self _dismissDisplayAnimated:NO];
364     [self _cancel];
365 }
366
367 - (void)_dismissDisplayAnimated:(BOOL)animated
368 {
369     if (_presentationPopover) {
370         [_presentationPopover dismissPopoverAnimated:animated];
371         [_presentationPopover setDelegate:nil];
372         _presentationPopover = nil;
373     }
374
375     if (_presentationViewController) {
376         [_presentationViewController dismissViewControllerAnimated:animated completion:^{
377             _presentationViewController = nil;
378         }];
379     }
380 }
381
382 #pragma mark - Media Types
383
384 static bool stringHasPrefixCaseInsensitive(NSString *str, NSString *prefix)
385 {
386     NSRange range = [str rangeOfString:prefix options:(NSCaseInsensitiveSearch | NSAnchoredSearch)];
387     return range.location != NSNotFound;
388 }
389
390 static NSArray *UTIsForMIMETypes(NSArray *mimeTypes)
391 {
392     // The HTML5 spec mentions the literal "image/*" and "video/*" strings.
393     // We support these and go a step further, if the MIME type starts with
394     // "image/" or "video/" we adjust the picker's image or video filters.
395     // So, "image/jpeg" would make the picker display all images types.
396     NSMutableSet *mediaTypes = [NSMutableSet set];
397     for (NSString *mimeType in mimeTypes) {
398         // FIXME: We should support more MIME type -> UTI mappings. <http://webkit.org/b/142614>
399         if (stringHasPrefixCaseInsensitive(mimeType, @"image/"))
400             [mediaTypes addObject:(NSString *)kUTTypeImage];
401         else if (stringHasPrefixCaseInsensitive(mimeType, @"video/"))
402             [mediaTypes addObject:(NSString *)kUTTypeMovie];
403     }
404
405     return mediaTypes.allObjects;
406 }
407
408 - (NSArray *)_mediaTypesForPickerSourceType:(UIImagePickerControllerSourceType)sourceType
409 {
410     NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get());
411     if (mediaTypes.count)
412         return mediaTypes;
413
414     // Fallback to every supported media type if there is no filter.
415     return [UIImagePickerController availableMediaTypesForSourceType:sourceType];
416 }
417
418 - (NSArray *)_documentPickerMenuMediaTypes
419 {
420     NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get());
421     if (mediaTypes.count)
422         return mediaTypes;
423
424     // Fallback to every supported media type if there is no filter.
425     return @[@"public.item"];
426 }
427
428 #pragma mark - Source selection menu
429
430 - (NSString *)_photoLibraryButtonLabel
431 {
432     return WEB_UI_STRING_KEY("Photo Library", "Photo Library (file upload action sheet)", "File Upload alert sheet button string for choosing an existing media item from the Photo Library");
433 }
434
435 - (NSString *)_cameraButtonLabel
436 {
437     if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
438         return nil;
439
440     // Choose the appropriate string for the camera button.
441     NSArray *filteredMediaTypes = [self _mediaTypesForPickerSourceType:UIImagePickerControllerSourceTypeCamera];
442     BOOL containsImageMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeImage];
443     BOOL containsVideoMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeMovie];
444     ASSERT(containsImageMediaType || containsVideoMediaType);
445     if (containsImageMediaType && containsVideoMediaType)
446         return WEB_UI_STRING_KEY("Take Photo or Video", "Take Photo or Video (file upload action sheet)", "File Upload alert sheet camera button string for taking photos or videos");
447
448     if (containsVideoMediaType)
449         return WEB_UI_STRING_KEY("Take Video", "Take Video (file upload action sheet)", "File Upload alert sheet camera button string for taking only videos");
450
451     return WEB_UI_STRING_KEY("Take Photo", "Take Photo (file upload action sheet)", "File Upload alert sheet camera button string for taking only photos");
452 }
453
454 - (void)_showMediaSourceSelectionSheet
455 {
456     NSString *existingString = [self _photoLibraryButtonLabel];
457     NSString *cameraString = [self _cameraButtonLabel];
458     NSString *cancelString = WEB_UI_STRING_KEY("Cancel", "Cancel (file upload action sheet)", "File Upload alert sheet button string to cancel");
459
460     _actionSheetController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
461
462     UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelString style:UIAlertActionStyleCancel handler:^(UIAlertAction *){
463         [self _cancel];
464         // We handled cancel ourselves. Prevent the popover controller delegate from cancelling when the popover dismissed.
465         [_presentationPopover setDelegate:nil];
466     }];
467
468     UIAlertAction *cameraAction = [UIAlertAction actionWithTitle:cameraString style:UIAlertActionStyleDefault handler:^(UIAlertAction *){
469         _usingCamera = YES;
470         [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypeCamera];
471     }];
472
473     UIAlertAction *photoLibraryAction = [UIAlertAction actionWithTitle:existingString style:UIAlertActionStyleDefault handler:^(UIAlertAction *){
474         [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
475     }];
476
477     [_actionSheetController addAction:cancelAction];
478     [_actionSheetController addAction:cameraAction];
479     [_actionSheetController addAction:photoLibraryAction];
480
481     [self _presentForCurrentInterfaceIdiom:_actionSheetController.get()];
482 }
483
484 - (void)_showDocumentPickerMenu
485 {
486     // FIXME: Support multiple file selection when implemented. <rdar://17177981>
487     // FIXME: We call -_setIgnoreApplicationEntitlementForImport: before initialization, because the assertion we're trying
488     // to suppress is in the initializer. <rdar://problem/20137692> tracks doing this with a private initializer.
489     _documentMenuController = adoptNS([UIDocumentMenuViewController alloc]);
490     [_documentMenuController _setIgnoreApplicationEntitlementForImport:YES];
491     [_documentMenuController initWithDocumentTypes:[self _documentPickerMenuMediaTypes] inMode:UIDocumentPickerModeImport];
492     [_documentMenuController setDelegate:self];
493
494     // FIXME: Need icons for Camera and Photo Library options.
495     [_documentMenuController addOptionWithTitle:[self _photoLibraryButtonLabel] image:nil order:UIDocumentMenuOrderFirst handler:^{
496         [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
497     }];
498
499     if (NSString *cameraString = [self _cameraButtonLabel]) {
500         [_documentMenuController addOptionWithTitle:cameraString image:nil order:UIDocumentMenuOrderFirst handler:^{
501             _usingCamera = YES;
502             [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypeCamera];
503         }];
504     }
505
506     [self _presentForCurrentInterfaceIdiom:_documentMenuController.get()];
507 }
508
509 #pragma mark - Image Picker
510
511 - (void)_showPhotoPickerWithSourceType:(UIImagePickerControllerSourceType)sourceType
512 {
513     _imagePicker = adoptNS([[UIImagePickerController alloc] init]);
514     [_imagePicker setDelegate:self];
515     [_imagePicker setSourceType:sourceType];
516     [_imagePicker setAllowsEditing:NO];
517     [_imagePicker setModalPresentationStyle:UIModalPresentationFullScreen];
518     [_imagePicker _setAllowsMultipleSelection:_allowMultipleFiles];
519     [_imagePicker setMediaTypes:[self _mediaTypesForPickerSourceType:sourceType]];
520
521     // Use a popover on the iPad if the source type is not the camera.
522     // The camera will use a fullscreen, modal view controller.
523     BOOL usePopover = UICurrentUserInterfaceIdiomIsPad() && sourceType != UIImagePickerControllerSourceTypeCamera;
524     if (usePopover)
525         [self _presentPopoverWithContentViewController:_imagePicker.get() animated:YES];
526     else
527         [self _presentFullscreenViewController:_imagePicker.get() animated:YES];
528 }
529
530 #pragma mark - Presenting View Controllers
531
532 - (void)_presentForCurrentInterfaceIdiom:(UIViewController *)viewController
533 {
534     if (UICurrentUserInterfaceIdiomIsPad())
535         [self _presentPopoverWithContentViewController:viewController animated:YES];
536     else
537         [self _presentFullscreenViewController:viewController animated:YES];
538 }
539
540 - (void)_presentPopoverWithContentViewController:(UIViewController *)contentViewController animated:(BOOL)animated
541 {
542     [self _dismissDisplayAnimated:animated];
543
544     _presentationPopover = adoptNS([[UIPopoverController alloc] initWithContentViewController:contentViewController]);
545     [_presentationPopover setDelegate:self];
546     [_presentationPopover presentPopoverFromRect:CGRectIntegral(CGRectMake(_interactionPoint.x, _interactionPoint.y, 1, 1)) inView:_view permittedArrowDirections:UIPopoverArrowDirectionAny animated:animated];
547 }
548
549 - (void)_presentFullscreenViewController:(UIViewController *)viewController animated:(BOOL)animated
550 {
551     [self _dismissDisplayAnimated:animated];
552
553     _presentationViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:_view];
554     [_presentationViewController presentViewController:viewController animated:animated completion:nil];
555 }
556
557 #pragma mark - UIPopoverControllerDelegate
558
559 - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
560 {
561     [self _cancel];
562 }
563
564 #pragma mark - UIDocumentMenuDelegate implementation
565
566 - (void)documentMenu:(UIDocumentMenuViewController *)documentMenu didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker
567 {
568     documentPicker.delegate = self;
569     documentPicker.modalPresentationStyle = UIModalPresentationFullScreen;
570
571     [self _presentForCurrentInterfaceIdiom:documentPicker];
572 }
573
574 - (void)documentMenuWasCancelled:(UIDocumentMenuViewController *)documentMenu
575 {
576     [self _dismissDisplayAnimated:YES];
577     [self _cancel];
578 }
579
580 #pragma mark - UIDocumentPickerControllerDelegate implementation
581
582 - (void)documentPicker:(UIDocumentPickerViewController *)documentPicker didPickDocumentAtURL:(NSURL *)url
583 {
584     [self _dismissDisplayAnimated:YES];
585     [self _chooseFiles:@[url] displayString:url.lastPathComponent iconImage:iconForFile(url)];
586 }
587
588 - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)documentPicker
589 {
590     [self _dismissDisplayAnimated:YES];
591     [self _cancel];
592 }
593
594 #pragma mark - UIImagePickerControllerDelegate implementation
595
596 - (BOOL)_willMultipleSelectionDelegateBeCalled
597 {
598     // The multiple selection delegate will not be called when the UIImagePicker was not multiple selection.
599     if (!_allowMultipleFiles)
600         return NO;
601
602     // The multiple selection delegate will not be called when we used the camera in the UIImagePicker.
603     if (_usingCamera)
604         return NO;
605
606     return YES;
607 }
608
609 - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMediaWithInfo:(NSDictionary *)info
610 {
611     // Sometimes both delegates get called, sometimes just one. Always let the
612     // multiple selection delegate handle everything if it will get called.
613     if ([self _willMultipleSelectionDelegateBeCalled])
614         return;
615
616     [self _dismissDisplayAnimated:YES];
617
618     [self _processMediaInfoDictionaries:[NSArray arrayWithObject:info]
619         successBlock:^(NSArray *processedResults, NSString *displayString) {
620             ASSERT([processedResults count] == 1);
621             _WKFileUploadItem *result = [processedResults objectAtIndex:0];
622             dispatch_async(dispatch_get_main_queue(), ^{
623                 [self _chooseFiles:[NSArray arrayWithObject:result.fileURL] displayString:displayString iconImage:result.displayImage];
624             });
625         }
626         failureBlock:^{
627             dispatch_async(dispatch_get_main_queue(), ^{
628                 [self _cancel];
629             });
630         }
631     ];
632 }
633
634 - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMultipleMediaWithInfo:(NSArray *)infos
635 {
636     [self _dismissDisplayAnimated:YES];
637
638     [self _processMediaInfoDictionaries:infos
639         successBlock:^(NSArray *processedResults, NSString *displayString) {
640             UIImage *iconImage = nil;
641             NSMutableArray *fileURLs = [NSMutableArray array];
642             for (_WKFileUploadItem *result in processedResults) {
643                 NSURL *fileURL = result.fileURL;
644                 if (!fileURL)
645                     continue;
646                 [fileURLs addObject:result.fileURL];
647                 if (!iconImage)
648                     iconImage = result.displayImage;
649             }
650
651             dispatch_async(dispatch_get_main_queue(), ^{
652                 [self _chooseFiles:fileURLs displayString:displayString iconImage:iconImage];
653             });
654         }
655         failureBlock:^{
656             dispatch_async(dispatch_get_main_queue(), ^{
657                 [self _cancel];
658             });
659         }
660     ];
661 }
662
663 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)imagePicker
664 {
665     [self _dismissDisplayAnimated:YES];
666     [self _cancel];
667 }
668
669 #pragma mark - Process UIImagePicker results
670
671 - (void)_processMediaInfoDictionaries:(NSArray *)infos successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)())failureBlock
672 {
673     [self _processMediaInfoDictionaries:infos atIndex:0 processedResults:[NSMutableArray array] processedImageCount:0 processedVideoCount:0 successBlock:successBlock failureBlock:failureBlock];
674 }
675
676 - (void)_processMediaInfoDictionaries:(NSArray *)infos atIndex:(NSUInteger)index processedResults:(NSMutableArray *)processedResults processedImageCount:(NSUInteger)processedImageCount processedVideoCount:(NSUInteger)processedVideoCount successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)())failureBlock
677 {
678     NSUInteger count = [infos count];
679     if (index == count) {
680         NSString *displayString = [self _displayStringForPhotos:processedImageCount videos:processedVideoCount];
681         successBlock(processedResults, displayString);
682         return;
683     }
684
685     NSDictionary *info = [infos objectAtIndex:index];
686     ASSERT(index < count);
687     index++;
688
689     auto uploadItemSuccessBlock = ^(_WKFileUploadItem *uploadItem) {
690         NSUInteger newProcessedVideoCount = processedVideoCount + (uploadItem.isVideo ? 1 : 0);
691         NSUInteger newProcessedImageCount = processedImageCount + (uploadItem.isVideo ? 0 : 1);
692         [processedResults addObject:uploadItem];
693         [self _processMediaInfoDictionaries:infos atIndex:index processedResults:processedResults processedImageCount:newProcessedImageCount processedVideoCount:newProcessedVideoCount successBlock:successBlock failureBlock:failureBlock];
694     };
695
696     [self _uploadItemFromMediaInfo:info successBlock:uploadItemSuccessBlock failureBlock:failureBlock];
697 }
698
699 - (void)_uploadItemFromMediaInfo:(NSDictionary *)info successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)())failureBlock
700 {
701     NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
702
703     // For videos from the existing library or camera, the media URL will give us a file path.
704     if (UTTypeConformsTo((CFStringRef)mediaType, kUTTypeMovie)) {
705         NSURL *mediaURL = [info objectForKey:UIImagePickerControllerMediaURL];
706         if (![mediaURL isFileURL]) {
707             LOG_ERROR("WKFileUploadPanel: Expected media URL to be a file path, it was not");
708             ASSERT_NOT_REACHED();
709             failureBlock();
710             return;
711         }
712
713         successBlock(adoptNS([[_WKVideoFileUploadItem alloc] initWithFileURL:mediaURL]).get());
714         return;
715     }
716
717     // For images, we create a temporary file path and use the original image.
718     if (UTTypeConformsTo((CFStringRef)mediaType, kUTTypeImage)) {
719         UIImage *originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
720         if (!originalImage) {
721             LOG_ERROR("WKFileUploadPanel: Expected image data but there was none");
722             ASSERT_NOT_REACHED();
723             failureBlock();
724             return;
725         }
726
727         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
728             NSString * const kTemporaryDirectoryName = @"WKWebFileUpload";
729             NSString * const kUploadImageName = @"image.jpg";
730
731             // Build temporary file path.
732             // FIXME: Should we get the ALAsset for the mediaURL and get the actual filename for the photo
733             // instead of naming each of the individual uploads image.jpg? This won't work for photos
734             // taken with the camera, but would work for photos picked from the library.
735             NSFileManager *fileManager = [NSFileManager defaultManager];
736             NSString *temporaryDirectory = [fileManager _webkit_createTemporaryDirectoryWithTemplatePrefix:kTemporaryDirectoryName];
737             NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:kUploadImageName];
738             if (!filePath) {
739                 LOG_ERROR("WKFileUploadPanel: Failed to create temporary directory to save image");
740                 failureBlock();
741                 return;
742             }
743
744             // Compress to JPEG format.
745             // FIXME: Different compression for different devices?
746             // FIXME: Different compression for different UIImage sizes?
747             // FIXME: Should EXIF data be maintained?
748             const CGFloat compression = 0.8;
749             NSData *jpeg = UIImageJPEGRepresentation(originalImage, compression);
750             if (!jpeg) {
751                 LOG_ERROR("WKFileUploadPanel: Failed to create JPEG representation for image");
752                 failureBlock();
753                 return;
754             }
755
756             // Save the image to the temporary file.
757             NSError *error = nil;
758             [jpeg writeToFile:filePath options:NSDataWritingAtomic error:&error];
759             if (error) {
760                 LOG_ERROR("WKFileUploadPanel: Error writing image data to temporary file: %@", error);
761                 failureBlock();
762                 return;
763             }
764
765             successBlock(adoptNS([[_WKImageFileUploadItem alloc] initWithFileURL:[NSURL fileURLWithPath:filePath] originalImage:originalImage]).get());
766         });
767         return;
768     }
769
770     // Unknown media type.
771     LOG_ERROR("WKFileUploadPanel: Unexpected media type. Expected image or video, got: %@", mediaType);
772     failureBlock();
773 }
774
775 - (NSString *)_displayStringForPhotos:(NSUInteger)imageCount videos:(NSUInteger)videoCount
776 {
777     if (!imageCount && !videoCount)
778         return nil;
779
780     NSString *title;
781     NSString *countString;
782     NSString *imageString;
783     NSString *videoString;
784     NSUInteger numberOfTypes = 2;
785
786     RetainPtr<NSNumberFormatter> countFormatter = adoptNS([[NSNumberFormatter alloc] init]);
787     [countFormatter setLocale:[NSLocale currentLocale]];
788     [countFormatter setGeneratesDecimalNumbers:YES];
789     [countFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
790
791     // Generate the individual counts for each type.
792     switch (imageCount) {
793     case 0:
794         imageString = nil;
795         --numberOfTypes;
796         break;
797     case 1:
798         imageString = WEB_UI_STRING_KEY("1 Photo", "1 Photo (file upload on page label for one photo)", "File Upload single photo label");
799         break;
800     default:
801         countString = [countFormatter stringFromNumber:@(imageCount)];
802         imageString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Photos", "# Photos (file upload on page label for multiple photos)", "File Upload multiple photos label"), countString];
803         break;
804     }
805
806     switch (videoCount) {
807     case 0:
808         videoString = nil;
809         --numberOfTypes;
810         break;
811     case 1:
812         videoString = WEB_UI_STRING_KEY("1 Video", "1 Video (file upload on page label for one video)", "File Upload single video label");
813         break;
814     default:
815         countString = [countFormatter stringFromNumber:@(videoCount)];
816         videoString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Videos", "# Videos (file upload on page label for multiple videos)", "File Upload multiple videos label"), countString];
817         break;
818     }
819
820     // Combine into a single result string if needed.
821     switch (numberOfTypes) {
822     case 2:
823         // FIXME: For localization we should build a complete string. We should have a localized string for each different combination.
824         title = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ and %@", "# Photos and # Videos (file upload on page label for image and videos)", "File Upload images and videos label"), imageString, videoString];
825         break;
826     case 1:
827         title = imageString ? imageString : videoString;
828         break;
829     default:
830         ASSERT_NOT_REACHED();
831         title = nil;
832         break;
833     }
834
835     return [title lowercaseString];
836 }
837
838 @end
839
840 #endif // PLATFORM(IOS)