| // Copyright 2019 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ImagePickerPlugin.h" |
| |
| #import <AVFoundation/AVFoundation.h> |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <Photos/Photos.h> |
| #import <UIKit/UIKit.h> |
| |
| #import "FLTImagePickerImageUtil.h" |
| #import "FLTImagePickerMetaDataUtil.h" |
| #import "FLTImagePickerPhotoAssetUtil.h" |
| |
| @interface FLTImagePickerPlugin () <UINavigationControllerDelegate, UIImagePickerControllerDelegate> |
| |
| @property(copy, nonatomic) FlutterResult result; |
| |
| @end |
| |
| static const int SOURCE_CAMERA = 0; |
| static const int SOURCE_GALLERY = 1; |
| |
| @implementation FLTImagePickerPlugin { |
| NSDictionary *_arguments; |
| UIImagePickerController *_imagePickerController; |
| UIViewController *_viewController; |
| } |
| |
| + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { |
| FlutterMethodChannel *channel = |
| [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" |
| binaryMessenger:[registrar messenger]]; |
| UIViewController *viewController = |
| [UIApplication sharedApplication].delegate.window.rootViewController; |
| FLTImagePickerPlugin *instance = |
| [[FLTImagePickerPlugin alloc] initWithViewController:viewController]; |
| [registrar addMethodCallDelegate:instance channel:channel]; |
| } |
| |
| - (instancetype)initWithViewController:(UIViewController *)viewController { |
| self = [super init]; |
| if (self) { |
| _viewController = viewController; |
| } |
| return self; |
| } |
| |
| - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { |
| if (self.result) { |
| self.result([FlutterError errorWithCode:@"multiple_request" |
| message:@"Cancelled by a second request" |
| details:nil]); |
| self.result = nil; |
| } |
| |
| if ([@"pickImage" isEqualToString:call.method]) { |
| _imagePickerController = [[UIImagePickerController alloc] init]; |
| _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; |
| _imagePickerController.delegate = self; |
| _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; |
| |
| self.result = result; |
| _arguments = call.arguments; |
| |
| int imageSource = [[_arguments objectForKey:@"source"] intValue]; |
| |
| switch (imageSource) { |
| case SOURCE_CAMERA: |
| [self checkCameraAuthorization]; |
| break; |
| case SOURCE_GALLERY: |
| [self checkPhotoAuthorization]; |
| break; |
| default: |
| result([FlutterError errorWithCode:@"invalid_source" |
| message:@"Invalid image source." |
| details:nil]); |
| break; |
| } |
| } else if ([@"pickVideo" isEqualToString:call.method]) { |
| _imagePickerController = [[UIImagePickerController alloc] init]; |
| _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; |
| _imagePickerController.delegate = self; |
| _imagePickerController.mediaTypes = @[ |
| (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, |
| (NSString *)kUTTypeMPEG4 |
| ]; |
| _imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; |
| |
| self.result = result; |
| _arguments = call.arguments; |
| |
| int imageSource = [[_arguments objectForKey:@"source"] intValue]; |
| |
| switch (imageSource) { |
| case SOURCE_CAMERA: |
| [self checkCameraAuthorization]; |
| break; |
| case SOURCE_GALLERY: |
| [self checkPhotoAuthorization]; |
| break; |
| default: |
| result([FlutterError errorWithCode:@"invalid_source" |
| message:@"Invalid video source." |
| details:nil]); |
| break; |
| } |
| } else { |
| result(FlutterMethodNotImplemented); |
| } |
| } |
| |
| - (void)showCamera { |
| @synchronized(self) { |
| if (_imagePickerController.beingPresented) { |
| return; |
| } |
| } |
| // Camera is not available on simulators |
| if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { |
| _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; |
| [_viewController presentViewController:_imagePickerController animated:YES completion:nil]; |
| } else { |
| [[[UIAlertView alloc] initWithTitle:@"Error" |
| message:@"Camera not available." |
| delegate:nil |
| cancelButtonTitle:@"OK" |
| otherButtonTitles:nil] show]; |
| self.result(nil); |
| self.result = nil; |
| _arguments = nil; |
| } |
| } |
| |
| - (void)checkCameraAuthorization { |
| AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; |
| |
| switch (status) { |
| case AVAuthorizationStatusAuthorized: |
| [self showCamera]; |
| break; |
| case AVAuthorizationStatusNotDetermined: { |
| [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo |
| completionHandler:^(BOOL granted) { |
| if (granted) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| if (granted) { |
| [self showCamera]; |
| } |
| }); |
| } else { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self errorNoCameraAccess:AVAuthorizationStatusDenied]; |
| }); |
| } |
| }]; |
| }; break; |
| case AVAuthorizationStatusDenied: |
| case AVAuthorizationStatusRestricted: |
| default: |
| [self errorNoCameraAccess:status]; |
| break; |
| } |
| } |
| |
| - (void)checkPhotoAuthorization { |
| PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; |
| switch (status) { |
| case PHAuthorizationStatusNotDetermined: { |
| [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { |
| if (status == PHAuthorizationStatusAuthorized) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self showPhotoLibrary]; |
| }); |
| } else { |
| [self errorNoPhotoAccess:status]; |
| } |
| }]; |
| break; |
| } |
| case PHAuthorizationStatusAuthorized: |
| [self showPhotoLibrary]; |
| break; |
| case PHAuthorizationStatusDenied: |
| case PHAuthorizationStatusRestricted: |
| default: |
| [self errorNoPhotoAccess:status]; |
| break; |
| } |
| } |
| |
| - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { |
| switch (status) { |
| case AVAuthorizationStatusRestricted: |
| self.result([FlutterError errorWithCode:@"camera_access_restricted" |
| message:@"The user is not allowed to use the camera." |
| details:nil]); |
| break; |
| case AVAuthorizationStatusDenied: |
| default: |
| self.result([FlutterError errorWithCode:@"camera_access_denied" |
| message:@"The user did not allow camera access." |
| details:nil]); |
| break; |
| } |
| } |
| |
| - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { |
| switch (status) { |
| case PHAuthorizationStatusRestricted: |
| self.result([FlutterError errorWithCode:@"photo_access_restricted" |
| message:@"The user is not allowed to use the photo." |
| details:nil]); |
| break; |
| case PHAuthorizationStatusDenied: |
| default: |
| self.result([FlutterError errorWithCode:@"photo_access_denied" |
| message:@"The user did not allow photo access." |
| details:nil]); |
| break; |
| } |
| } |
| |
| - (void)showPhotoLibrary { |
| // No need to check if SourceType is available. It always is. |
| _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; |
| [_viewController presentViewController:_imagePickerController animated:YES completion:nil]; |
| } |
| |
| - (void)imagePickerController:(UIImagePickerController *)picker |
| didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info { |
| NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL]; |
| [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; |
| // The method dismissViewControllerAnimated does not immediately prevent |
| // further didFinishPickingMediaWithInfo invocations. A nil check is necessary |
| // to prevent below code to be unwantly executed multiple times and cause a |
| // crash. |
| if (!self.result) { |
| return; |
| } |
| if (videoURL != nil) { |
| if (@available(iOS 13.0, *)) { |
| NSString *fileName = [videoURL lastPathComponent]; |
| NSURL *destination = |
| [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; |
| |
| if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { |
| NSError *error; |
| if (![[videoURL path] isEqualToString:[destination path]]) { |
| [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; |
| |
| if (error) { |
| self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" |
| message:@"Could not cache the video file." |
| details:nil]); |
| self.result = nil; |
| return; |
| } |
| } |
| videoURL = destination; |
| } |
| } |
| self.result(videoURL.path); |
| self.result = nil; |
| |
| } else { |
| UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; |
| if (image == nil) { |
| image = [info objectForKey:UIImagePickerControllerOriginalImage]; |
| } |
| |
| NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; |
| NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; |
| NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; |
| |
| if (![imageQuality isKindOfClass:[NSNumber class]]) { |
| imageQuality = @1; |
| } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { |
| imageQuality = [NSNumber numberWithInt:1]; |
| } else { |
| imageQuality = @([imageQuality floatValue] / 100); |
| } |
| |
| if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { |
| image = [FLTImagePickerImageUtil scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; |
| } |
| |
| PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; |
| if (!originalAsset) { |
| // Image picked without an original asset (e.g. User took a photo directly) |
| [self saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; |
| } else { |
| __weak typeof(self) weakSelf = self; |
| [[PHImageManager defaultManager] |
| requestImageDataForAsset:originalAsset |
| options:nil |
| resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, |
| UIImageOrientation orientation, NSDictionary *_Nullable info) { |
| // maxWidth and maxHeight are used only for GIF images. |
| [weakSelf saveImageWithOriginalImageData:imageData |
| image:image |
| maxWidth:maxWidth |
| maxHeight:maxHeight |
| imageQuality:imageQuality]; |
| }]; |
| } |
| } |
| _arguments = nil; |
| } |
| |
| - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { |
| [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; |
| self.result(nil); |
| |
| self.result = nil; |
| _arguments = nil; |
| } |
| |
| - (void)saveImageWithOriginalImageData:(NSData *)originalImageData |
| image:(UIImage *)image |
| maxWidth:(NSNumber *)maxWidth |
| maxHeight:(NSNumber *)maxHeight |
| imageQuality:(NSNumber *)imageQuality { |
| NSString *savedPath = |
| [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:originalImageData |
| image:image |
| maxWidth:maxWidth |
| maxHeight:maxHeight |
| imageQuality:imageQuality]; |
| [self handleSavedPath:savedPath]; |
| } |
| |
| - (void)saveImageWithPickerInfo:(NSDictionary *)info |
| image:(UIImage *)image |
| imageQuality:(NSNumber *)imageQuality { |
| NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info |
| image:image |
| imageQuality:imageQuality]; |
| [self handleSavedPath:savedPath]; |
| } |
| |
| - (void)handleSavedPath:(NSString *)path { |
| if (path) { |
| self.result(path); |
| } else { |
| self.result([FlutterError errorWithCode:@"create_error" |
| message:@"Temporary file could not be created" |
| details:nil]); |
| } |
| self.result = nil; |
| } |
| |
| @end |