Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[image_picker] Removes use of PHAsset on IOS 14+ #8190

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.8.12+2

* Removes the need for user permissions to pick an image on iOS 14+.

## 0.8.12+1

* Updates Pigeon for non-nullable collection type support.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,11 +495,11 @@ - (void)testSavesImages API_AVAILABLE(ios(14)) {
[self waitForExpectationsWithTimeout:30 handler:nil];
}

- (void)testPickImageRequestAuthorization API_AVAILABLE(ios(14)) {
- (void)testPickImageDoesntRequestAuthorization API_AVAILABLE(ios(14)) {
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
.andReturn(PHAuthorizationStatusNotDetermined);
OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
OCMReject([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
handler:OCMOCK_ANY]);

FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
Expand All @@ -514,29 +514,6 @@ - (void)testPickImageRequestAuthorization API_AVAILABLE(ios(14)) {
OCMVerifyAll(mockPhotoLibrary);
}

- (void)testPickImageAuthorizationDenied API_AVAILABLE(ios(14)) {
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
.andReturn(PHAuthorizationStatusDenied);

FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];

XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];

[plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery
camera:FLTSourceCameraFront]
maxSize:[[FLTMaxSize alloc] init]
quality:nil
fullMetadata:YES
completion:^(NSString *result, FlutterError *error) {
XCTAssertNil(result);
XCTAssertEqualObjects(error.code, @"photo_access_denied");
XCTAssertEqualObjects(error.message, @"The user did not allow photo access.");
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:30 handler:nil];
}

- (void)testPickMultiImageDuplicateCallCancels API_AVAILABLE(ios(14)) {
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,6 @@ - (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable {
XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]);
}

- (void)testGetAssetFromPHPickerResultShouldReturnNilIfNotAvailable API_AVAILABLE(ios(14)) {
if (@available(iOS 14, *)) {
PHPickerResult *mockData;
[mockData.itemProvider
loadObjectOfClass:[UIImage class]
completionHandler:^(__kindof id<NSItemProviderReading> _Nullable image,
NSError *_Nullable error) {
XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:mockData]);
}];
}
}

- (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData {
// test jpg
NSData *dataJPG = ImagePickerTestImages.JPGTestData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ - (void)testSelectingFromGallery API_AVAILABLE(ios(14)) {
}
[pickButton tap];

[self handlePermissionInterruption];

// Find an image and tap on it.
NSPredicate *imagePredicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH 'Photo, '"];
XCUIElementQuery *imageQuery = [self.app.images matchingPredicate:imagePredicate];
Expand All @@ -119,25 +117,6 @@ - (void)testSelectingFromGallery API_AVAILABLE(ios(14)) {

[aImage tap];

// Find and tap on the `Done` button.
XCUIElement *doneButton = self.app.buttons[@"Done"].firstMatch;
if (![doneButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription);
XCTSkip(@"Permissions popup could not fired so the test is skipped...");
}
[doneButton tap];

// Find an image and tap on it to have access to selected photos.
aImage = imageQuery.firstMatch;

os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription);
if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription);
XCTFail(@"Failed due to not able to find an image with %@ seconds",
@(kLimitedElementWaitingTime));
}
[aImage tap];

// Find the picked image.
XCUIElement *pickedImage = self.app.images[@"image_picker_example_picked_image"].firstMatch;
if (![pickedImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info {
return info[UIImagePickerControllerPHAsset];
}

+ (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[ result.assetIdentifier ]
options:nil];
return fetchResult.firstObject;
}

+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL {
if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) {
return nil;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,7 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con
pickerViewController.presentationController.delegate = self;
self.callContext = context;

if (context.requestFullMetadata) {
[self checkPhotoAuthorizationWithPHPicker:pickerViewController];
} else {
[self showPhotoLibraryWithPHPicker:pickerViewController];
}
[self showPhotoLibraryWithPHPicker:pickerViewController];
}

- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source
Expand Down Expand Up @@ -390,40 +386,6 @@ - (void)checkPhotoAuthorizationWithImagePicker:(UIImagePickerController *)imageP
}
}

- (void)checkPhotoAuthorizationWithPHPicker:(PHPickerViewController *)pickerViewController
API_AVAILABLE(ios(14)) {
PHAccessLevel requestedAccessLevel = PHAccessLevelReadWrite;
PHAuthorizationStatus status =
[PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel];
switch (status) {
case PHAuthorizationStatusNotDetermined: {
[PHPhotoLibrary
requestAuthorizationForAccessLevel:requestedAccessLevel
handler:^(PHAuthorizationStatus status) {
dispatch_async(dispatch_get_main_queue(), ^{
if (status == PHAuthorizationStatusAuthorized) {
[self showPhotoLibraryWithPHPicker:pickerViewController];
} else if (status == PHAuthorizationStatusLimited) {
[self showPhotoLibraryWithPHPicker:pickerViewController];
} else {
[self errorNoPhotoAccess:status];
}
});
}];
break;
}
case PHAuthorizationStatusAuthorized:
case PHAuthorizationStatusLimited:
[self showPhotoLibraryWithPHPicker:pickerViewController];
break;
case PHAuthorizationStatusDenied:
case PHAuthorizationStatusRestricted:
default:
[self errorNoPhotoAccess:status];
break;
}
}

- (void)errorNoCameraAccess:(AVAuthorizationStatus)status {
switch (status) {
case AVAuthorizationStatusRestricted:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,64 +128,20 @@ - (void)start {
- (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) {
UIImage *localImage = [[UIImage alloc] initWithData:pickerImageData];

PHAsset *originalAsset;
// Only if requested, fetch the full "PHAsset" metadata, which requires "Photo Library Usage"
// permissions.
if (self.requestFullMetadata) {
originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result];
}

if (self.maxWidth != nil || self.maxHeight != nil) {
localImage = [FLTImagePickerImageUtil scaledImage:localImage
maxWidth:self.maxWidth
maxHeight:self.maxHeight
isMetadataAvailable:YES];
}
if (originalAsset) {
void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) =
^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, NSDictionary *_Nullable info) {
// maxWidth and maxHeight are used only for GIF images.
NSString *savedPath = [FLTImagePickerPhotoAssetUtil
saveImageWithOriginalImageData:imageData
image:localImage
maxWidth:self.maxWidth
maxHeight:self.maxHeight
imageQuality:self.desiredImageQuality];
[self completeOperationWithPath:savedPath error:nil];
};
if (@available(iOS 13.0, *)) {
[[PHImageManager defaultManager]
requestImageDataAndOrientationForAsset:originalAsset
options:nil
resultHandler:^(NSData *_Nullable imageData,
NSString *_Nullable dataUTI,
CGImagePropertyOrientation orientation,
NSDictionary *_Nullable info) {
resultHandler(imageData, dataUTI, info);
}];
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[PHImageManager defaultManager]
requestImageDataForAsset:originalAsset
options:nil
resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI,
UIImageOrientation orientation, NSDictionary *_Nullable info) {
resultHandler(imageData, dataUTI, info);
}];
#pragma clang diagnostic pop
}
} else {
// Image picked without an original asset (e.g. User pick image without permission)
// maxWidth and maxHeight are used only for GIF images.
NSString *savedPath =
[FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData
image:localImage
maxWidth:self.maxWidth
maxHeight:self.maxHeight
imageQuality:self.desiredImageQuality];
[self completeOperationWithPath:savedPath error:nil];
}
// maxWidth and maxHeight are used only for GIF images.
NSString *savedPath =
[FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData
image:localImage
maxWidth:self.maxWidth
maxHeight:self.maxHeight
imageQuality:self.desiredImageQuality];
[self completeOperationWithPath:savedPath error:nil];
}

/// Processes the video.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ NS_ASSUME_NONNULL_BEGIN

+ (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info;

+ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14));

// Saves video to temporary URL. Returns nil on failure;
+ (NSURL *)saveVideoFromURL:(NSURL *)videoURL;

Expand Down
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker_ios
description: iOS implementation of the image_picker plugin.
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 0.8.12+1
version: 0.8.12+2

environment:
sdk: ^3.3.0
Expand Down