Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[image_picker_ios] Pass through error message from image saving #6858

Merged
merged 4 commits into from
Dec 19, 2022

Conversation

jmagman
Copy link
Member

@jmagman jmagman commented Dec 17, 2022

Previously, if there was an error loading the image, the result callback would never be called, and the callContext would not be nil'd out. When the next request happened, the user would get a confusing multiple_request error because the last state wasn't cleared out.

if (self.callContext) {
[self sendCallResultWithError:[FlutterError errorWithCode:@"multiple_request"
message:@"Cancelled by a second request"
details:nil]];
self.callContext = nil;

Now, when there is an error loading an image, pipe an error back to the flutter result instead of silently failing and never calling the result callback. This also nils out the callContext so the next request has a chance of succeeding.

self.callContext.result(nil, error);
self.callContext = nil;

As before, save all the picked images on a background thread, and when complete call the callback with either all the saved images, or with the image error. Use the NSOperationQueue API instead of GCD to take advantage of dependency operations (the final main callback is dependent on the image save operations completing).

If any of the picked images fail to load, return an error back to the dart side. Ideally there would be a mapping between successfully picked images and the images that failed to load, but I believe that would require an API change like flutter/flutter#95268.

Also

  • Minor __bridge cleanup
  • Use XCTest expectations instead of semaphores.

Fixes flutter/flutter#98569
Fixes flutter/flutter#82602
Will make flutter/flutter#49780 actually show the underlying error.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@interface FLTImagePickerPlugin : NSObject <FlutterPlugin>

// For testing only.
- (UIViewController *)viewControllerWithWindow:(UIWindow *)window;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to existing FLTImagePickerPlugin_Test.h

@@ -29,10 +29,7 @@ - (instancetype)initWithResult:(nonnull FlutterResultAdapter)result {

#pragma mark -

@interface FLTImagePickerPlugin () <UINavigationControllerDelegate,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also moved to FLTImagePickerPlugin_Test.h so the tests can directly call the PHPickerViewControllerDelegate methods.

@@ -102,10 +101,18 @@ - (void)start {
UIImage *image = [[UIImage alloc] initWithData:data];
[self processImage:image];
} else {
os_log_error(OS_LOG_DEFAULT, "Could not process image: %@",
error);
FlutterError *flutterError =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the fix--when the image fails to load, send back an error.

}
}];
} else {
FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if it's not an image, also send back an error.

Comment on lines 520 to 521
[sendListOperation addDependency:saveOperation];
[backgroundQueue addOperation:saveOperation];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use NSOperationQueue so the main thread callback operation can be scheduled ASAP, but won't run until all the dependent save operations have completed.

dispatch_async(dispatch_get_main_queue(), ^{
__block NSOperationQueue *saveQueue = [[NSOperationQueue alloc] init];
saveQueue.name = @"Flutter Save Image Queue";
saveQueue.qualityOfService = NSQualityOfServiceUserInitiated;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This queue should not be background QoS, that's for app maintenance like clearing caches and can take awhile to schedule, or sometimes not scheduled at all until the OS is idle. But this image save is in reaction to the user tapping on image, who is waiting for the app to continue.
https://developer.apple.com/documentation/dispatch/dispatchqos/qosclass

Comment on lines 504 to 505
// NSNull means it hasn't saved yet.
pathList[index] = [NSNull null];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really needed anymore, either all the images will save and this will be densely populated, or it will not be returned at all and only the error will be returned.
However if flutter/flutter#95268 is implemented then this should return all the saved images, and for the failing ones some object with the error attached.

Copy link
Contributor

@vashworth vashworth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -269,37 +271,130 @@ - (void)testViewController {
- (void)testPluginMultiImagePathHasNullItem {
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init];

dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

@@ -120,7 +120,7 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data
options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF;

CGImageSourceRef imageSource =
CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options);
CGImageSourceCreateWithData((__bridge CFDataRef)data, (CFDictionaryRef)options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(__bridge CFDictionaryRef)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah missed that.

NSNumber *imageQuality = currentCallContext.imageQuality;
NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality];
BOOL requestFullMetadata = currentCallContext.requestFullMetadata;
NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to populate the array like before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, maybe im missing something. I was saying if pathList is not populated (i.e. size=0, capacity=count), then will pathList[0] = foo throw index out of bounds?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, good catch. I'm going to make this a NSPointerArray.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually just populating the array with NSNull as was done before is probably safer. A NSPointerArray won't have the out-of-bounds problem, since it "populates" up to count with nil, and will compact those out during allObjects. So if there was a bug where all indices weren't set to NSNull then we would get wacky index values and the array length would be incorrect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes what I have here is correct, the first thing it's doing is populating the array with NSNull, in index order:

  NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count];
...
  [results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) {
    // NSNull means it hasn't saved yet.
    pathList[index] = [NSNull null];
...

This is exactly the same as the previous logic, it just replaced a for loop with fast enumeration over the results.

  NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size];
  for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++)
    ;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, the previous logic is using addObject, which increases the size though. iirc I think pathList[index] = [NSNull null] will simply crash.

Copy link
Contributor

@hellohuanlin hellohuanlin Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did an experiment. Very interesting/strange behavior of NSMutableArray:

Experiment 1: This works:

  NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
  arr[0] = @"foo0";
  arr[1] = @"foo1";
  arr[2] = @"bar2";
  NSLog(@"print out: %@", arr);

Experiment 2: This crashes:

  NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
  arr[0] = @"foo0";
  arr[2] = @"bar2";
  NSLog(@"print out: %@", arr);

Experiment 3: This also crashes:

  NSMutableArray *arr = [NSMutableArray arrayWithCapacity:100];
  arr[1] = @"foo1";
  arr[2] = @"bar2";
  NSLog(@"print out: %@", arr);

Looks like as long as you add from index 0, one by one, NSMutableArray will just auto increment the size one by one. I wasn't able to find this behavior documented anywhere. However, the doc of setObject API explicitly says it should crash:

If index is beyond the end of the array (that is, if index is greater than or equal to the value returned by count), an NSRangeException is raised.

The above contradicts with actual behavior of Experiment 1, so it feels like a bug in NSMutableArray. I recommend just do addObject instead of setObject API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is doing experiment 1 because enumerateObjectsUsingBlock: starts at 0 and goes through the whole array (unless *stop = YES is set).

https://developer.apple.com/documentation/foundation/nsarray/1415846-enumerateobjectsusingblock

Executes a given block using each object in the array, starting with the first object and continuing through the array to the last object.

I can swap to addObject: though since it is incrementing by one every time.

Copy link
Contributor

@hellohuanlin hellohuanlin Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can swap to addObject: though since it is incrementing by one every time.

Yep, I think experiment 1 is a bug in NSMutableArray that's supposed to crash, since it contradicts with the doc.


// This operation will be executed on the main queue after
// all selected files have been saved.
NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weak self

// This operation will be executed on the main queue after
// all selected files have been saved.
NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{
if (saveError != nil) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may worth commenting that "No race condition here since sendListOperation is only called after all saveOperations are complete"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It says a few lines above this:

  // This operation will be executed on the main queue after
  // all selected files have been saved.

saveError = error;
}
}];
[sendListOperation addDependency:saveOperation];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice API. Yeah it's weird to mix GCD and NSOperationQueue - in pure GCD I would use DispatchGroup for this.

@jmagman jmagman added the autosubmit Merge PR when tree becomes green via auto submit App label Dec 19, 2022
@auto-submit auto-submit bot merged commit 697c8b3 into flutter:main Dec 19, 2022
@jmagman jmagman deleted the ip-error-operation branch December 19, 2022 23:15
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Dec 20, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Dec 21, 2022
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Dec 21, 2022
* 697c8b3ee [image_picker_ios] Pass through error message from image saving (flutter/plugins#6858)

* 72f810851 [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" (flutter/plugins#6848)

* acbe9b452 [gh_actions]: Bump github/codeql-action from 2.1.35 to 2.1.37 (flutter/plugins#6860)

* 3d8b73bf0 [camera] Remove deprecated Optional type (flutter/plugins#6870)

* c5220efae [in_app_purchase] Add support for macOS (flutter/plugins#6519)

* 54fc2066d Roll Flutter from 028c6e2 to dbc9306 (11 revisions) (flutter/plugins#6849)
gspencergoog pushed a commit to gspencergoog/flutter that referenced this pull request Jan 19, 2023
…#117456)

* 697c8b3ee [image_picker_ios] Pass through error message from image saving (flutter/plugins#6858)

* 72f810851 [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" (flutter/plugins#6848)

* acbe9b452 [gh_actions]: Bump github/codeql-action from 2.1.35 to 2.1.37 (flutter/plugins#6860)

* 3d8b73bf0 [camera] Remove deprecated Optional type (flutter/plugins#6870)

* c5220efae [in_app_purchase] Add support for macOS (flutter/plugins#6519)

* 54fc2066d Roll Flutter from 028c6e2 to dbc9306 (11 revisions) (flutter/plugins#6849)
mauricioluz pushed a commit to mauricioluz/plugins that referenced this pull request Jan 26, 2023
…ter#6858)

* [image_picker_ios] Pass through error message from image saving

* Review edits

* Format

* addObject
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: image_picker platform-ios
Projects
None yet
3 participants