blob: 5ac31a695a5836324e483b71c4d34be08ea7a104 [file] [log] [blame]
// 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