Skip to content

Commit

Permalink
[image_picker] Removes use of PHAsset on IOS 14+ (#8190)
Browse files Browse the repository at this point in the history
Original PR #8020
Fixes issue flutter/flutter#90373

Currently on IOS 14+, images are picked using `PHPickerViewController` which does not need photo permissions and also gets the full image metadata regardless of `requestFullMetadata`. However, it currently retrieves the metadata using `PHAsset` which does require permission and causes the gallery opening twice issue. Another issue is that an error is thrown when permission is denied even if none are required.
  • Loading branch information
WenHaozhan authored Jan 3, 2025
1 parent 4dd8ea3 commit 733869c
Show file tree
Hide file tree
Showing 9 changed files with 14 additions and 159 deletions.
3 changes: 2 additions & 1 deletion packages/image_picker/image_picker_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.8.12+2

* Removes the need for user permissions to pick an image on iOS 14+.
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.8.12+1
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.4.0
Expand Down

0 comments on commit 733869c

Please sign in to comment.