Skip to content

Commit c9fec61

Browse files
[image_picker] Prevent multiple active calls on iOS (#5272)
The image picker plugin's implementation doesn't currently handle multiple calls correctly due to the use of an ivar to track the response object; the original entry point handles that by cancelling earlier requests when new ones come in, but as we added more entry points we didn't replicate that logic. This adds it to all picker entry points. (Longer term, we should instead handle multiple concurrent calls, but this is consistent with historical behavior, and is better than having some `await`s on the Dart side never return as can happen now.) The newer PHPicker code path not only didn't cancel, but used an ivar for the picker view controller, which in some cases could result in the same controller being presented multiple times, crashing the app (see referenced issue). While the new cancel calls will prevent that case from happening, to prevent anything similar from happening in the future this removes the ivar entirely, since we can just pass the controller to the necessary methods (as is already being done with the `UIImagePickerController` paths). Fixes flutter/flutter#108900
1 parent b236d83 commit c9fec61

File tree

4 files changed

+98
-20
lines changed

4 files changed

+98
-20
lines changed

packages/image_picker/image_picker_ios/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.8+3
2+
3+
* Fixes a possible crash when calling a picker method while another is waiting on permissions.
4+
15
## 0.8.8+2
26

37
* Adds pub topics to package metadata.

packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,83 @@ - (void)testPickImageAuthorizationDenied API_AVAILABLE(ios(14)) {
531531
[self waitForExpectationsWithTimeout:30 handler:nil];
532532
}
533533

534+
- (void)testPickMultiImageDuplicateCallCancels API_AVAILABLE(ios(14)) {
535+
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
536+
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
537+
.andReturn(PHAuthorizationStatusNotDetermined);
538+
OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
539+
handler:OCMOCK_ANY]);
540+
541+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
542+
543+
XCTestExpectation *firstCallExpectation = [self expectationWithDescription:@"first call"];
544+
[plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@100 height:@100]
545+
quality:nil
546+
fullMetadata:@YES
547+
completion:^(NSArray<NSString *> *result, FlutterError *error) {
548+
XCTAssertNotNil(error);
549+
XCTAssertEqualObjects(error.code, @"multiple_request");
550+
[firstCallExpectation fulfill];
551+
}];
552+
[plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@100 height:@100]
553+
quality:nil
554+
fullMetadata:@YES
555+
completion:^(NSArray<NSString *> *result, FlutterError *error){
556+
}];
557+
[self waitForExpectationsWithTimeout:30 handler:nil];
558+
}
559+
560+
- (void)testPickMediaDuplicateCallCancels API_AVAILABLE(ios(14)) {
561+
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
562+
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
563+
.andReturn(PHAuthorizationStatusNotDetermined);
564+
OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
565+
handler:OCMOCK_ANY]);
566+
567+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
568+
569+
FLTMediaSelectionOptions *options =
570+
[FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)]
571+
imageQuality:@(50)
572+
requestFullMetadata:@YES
573+
allowMultiple:@YES];
574+
XCTestExpectation *firstCallExpectation = [self expectationWithDescription:@"first call"];
575+
[plugin pickMediaWithMediaSelectionOptions:options
576+
completion:^(NSArray<NSString *> *result, FlutterError *error) {
577+
XCTAssertNotNil(error);
578+
XCTAssertEqualObjects(error.code, @"multiple_request");
579+
[firstCallExpectation fulfill];
580+
}];
581+
[plugin pickMediaWithMediaSelectionOptions:options
582+
completion:^(NSArray<NSString *> *result, FlutterError *error){
583+
}];
584+
[self waitForExpectationsWithTimeout:30 handler:nil];
585+
}
586+
587+
- (void)testPickVideoDuplicateCallCancels API_AVAILABLE(ios(14)) {
588+
id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]);
589+
OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite])
590+
.andReturn(PHAuthorizationStatusNotDetermined);
591+
OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite
592+
handler:OCMOCK_ANY]);
593+
594+
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];
595+
596+
FLTSourceSpecification *source = [FLTSourceSpecification makeWithType:FLTSourceTypeCamera
597+
camera:FLTSourceCameraRear];
598+
XCTestExpectation *firstCallExpectation = [self expectationWithDescription:@"first call"];
599+
[plugin pickVideoWithSource:source
600+
maxDuration:nil
601+
completion:^(NSString *result, FlutterError *error) {
602+
XCTAssertNotNil(error);
603+
XCTAssertEqualObjects(error.code, @"multiple_request");
604+
[firstCallExpectation fulfill];
605+
}];
606+
[plugin pickVideoWithSource:source
607+
maxDuration:nil
608+
completion:^(NSString *result, FlutterError *error){
609+
}];
610+
[self waitForExpectationsWithTimeout:30 handler:nil];
611+
}
612+
534613
@end

packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ - (instancetype)initWithResult:(nonnull FlutterResultAdapter)result {
3131

3232
@interface FLTImagePickerPlugin ()
3333

34-
/**
35-
* The PHPickerViewController instance used to pick multiple
36-
* images.
37-
*/
38-
@property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14));
39-
4034
/**
4135
* The UIImagePickerController instances that will be used when a new
4236
* controller would normally be created. Each call to
@@ -117,15 +111,16 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con
117111
config.filter = [PHPickerFilter imagesFilter];
118112
}
119113

120-
_pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config];
121-
_pickerViewController.delegate = self;
122-
_pickerViewController.presentationController.delegate = self;
114+
PHPickerViewController *pickerViewController =
115+
[[PHPickerViewController alloc] initWithConfiguration:config];
116+
pickerViewController.delegate = self;
117+
pickerViewController.presentationController.delegate = self;
123118
self.callContext = context;
124119

125120
if (context.requestFullMetadata) {
126-
[self checkPhotoAuthorizationForAccessLevel];
121+
[self checkPhotoAuthorizationWithPHPicker:pickerViewController];
127122
} else {
128-
[self showPhotoLibraryWithPHPicker:_pickerViewController];
123+
[self showPhotoLibraryWithPHPicker:pickerViewController];
129124
}
130125
}
131126

@@ -201,6 +196,7 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize
201196
fullMetadata:(NSNumber *)fullMetadata
202197
completion:(nonnull void (^)(NSArray<NSString *> *_Nullable,
203198
FlutterError *_Nullable))completion {
199+
[self cancelInProgressCall];
204200
FLTImagePickerMethodCallContext *context =
205201
[[FLTImagePickerMethodCallContext alloc] initWithResult:completion];
206202
context.maxSize = maxSize;
@@ -220,6 +216,7 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize
220216
- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions
221217
completion:(nonnull void (^)(NSArray<NSString *> *_Nullable,
222218
FlutterError *_Nullable))completion {
219+
[self cancelInProgressCall];
223220
FLTImagePickerMethodCallContext *context =
224221
[[FLTImagePickerMethodCallContext alloc] initWithResult:completion];
225222
context.maxSize = [mediaSelectionOptions maxSize];
@@ -244,6 +241,7 @@ - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source
244241
maxDuration:(nullable NSNumber *)maxDurationSeconds
245242
completion:
246243
(nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
244+
[self cancelInProgressCall];
247245
FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc]
248246
initWithResult:^void(NSArray<NSString *> *paths, FlutterError *error) {
249247
if (paths.count > 1) {
@@ -393,7 +391,8 @@ - (void)checkPhotoAuthorizationWithImagePicker:(UIImagePickerController *)imageP
393391
}
394392
}
395393

396-
- (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) {
394+
- (void)checkPhotoAuthorizationWithPHPicker:(PHPickerViewController *)pickerViewController
395+
API_AVAILABLE(ios(14)) {
397396
PHAccessLevel requestedAccessLevel = PHAccessLevelReadWrite;
398397
PHAuthorizationStatus status =
399398
[PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel];
@@ -404,13 +403,9 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) {
404403
handler:^(PHAuthorizationStatus status) {
405404
dispatch_async(dispatch_get_main_queue(), ^{
406405
if (status == PHAuthorizationStatusAuthorized) {
407-
[self
408-
showPhotoLibraryWithPHPicker:self->
409-
_pickerViewController];
406+
[self showPhotoLibraryWithPHPicker:pickerViewController];
410407
} else if (status == PHAuthorizationStatusLimited) {
411-
[self
412-
showPhotoLibraryWithPHPicker:self->
413-
_pickerViewController];
408+
[self showPhotoLibraryWithPHPicker:pickerViewController];
414409
} else {
415410
[self errorNoPhotoAccess:status];
416411
}
@@ -420,7 +415,7 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) {
420415
}
421416
case PHAuthorizationStatusAuthorized:
422417
case PHAuthorizationStatusLimited:
423-
[self showPhotoLibraryWithPHPicker:_pickerViewController];
418+
[self showPhotoLibraryWithPHPicker:pickerViewController];
424419
break;
425420
case PHAuthorizationStatusDenied:
426421
case PHAuthorizationStatusRestricted:

packages/image_picker/image_picker_ios/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: image_picker_ios
22
description: iOS implementation of the image_picker plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
5-
version: 0.8.8+2
5+
version: 0.8.8+3
66

77
environment:
88
sdk: ">=2.19.0 <4.0.0"

0 commit comments

Comments
 (0)