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