From dea50fcc5e50b368fb99e05b673fefbd40b17d6e Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 7 Jul 2021 13:39:00 +0200 Subject: [PATCH 01/32] Only run `initWithCameraName` on background thread. --- .../camera/camera/ios/Classes/CameraPlugin.m | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ebd5366ba78d..e2e37b8cc422 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1315,13 +1315,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); } - // Invoke the plugin on another dispatch queue to avoid blocking the UI. - dispatch_async(_dispatchQueue, ^{ - [self handleMethodCallAsync:call result:result]; - }); -} - -- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { if ([@"availableCameras" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession @@ -1358,27 +1351,30 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re NSString *cameraName = call.arguments[@"cameraName"]; NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; NSNumber *enableAudio = call.arguments[@"enableAudio"]; - NSError *error; - FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName - resolutionPreset:resolutionPreset - enableAudio:[enableAudio boolValue] - orientation:[[UIDevice currentDevice] orientation] - dispatchQueue:_dispatchQueue - error:&error]; - - if (error) { - result(getFlutterError(error)); - } else { - if (_camera) { - [_camera close]; - } - int64_t textureId = [_registry registerTexture:cam]; - _camera = cam; - - result(@{ - @"cameraId" : @(textureId), + dispatch_async(_dispatchQueue, ^{ + NSError *error; + FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName + resolutionPreset:resolutionPreset + enableAudio:[enableAudio boolValue] + orientation:[[UIDevice currentDevice] orientation] + dispatchQueue:self->_dispatchQueue + error:&error]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + result(getFlutterError(error)); + } else { + if (self->_camera) { + [self->_camera close]; + } + int64_t textureId = [self->_registry registerTexture:cam]; + self->_camera = cam; + + result(@{ + @"cameraId" : @(textureId), + }); + } }); - } + }); } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; result(nil); From da83b514b96797141218eb3cde6f3cf82ea77001 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 7 Jul 2021 14:02:45 +0200 Subject: [PATCH 02/32] run camera start async --- packages/camera/camera/ios/Classes/CameraPlugin.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index e2e37b8cc422..4aa01a2c9dfb 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1409,8 +1409,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), }]; [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; - [_camera start]; - result(nil); + dispatch_async(_dispatchQueue, ^{ + [self->_camera start]; + dispatch_async(dispatch_get_main_queue(), ^{ + result(nil); + }); + }); } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; From 277d92312f5c9d76055f7747e77edc6fbbc1eba8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 8 Jul 2021 09:28:36 +0200 Subject: [PATCH 03/32] Start with migrating to ThreadSafeFlutterResult --- .../camera/camera/ios/Classes/CameraPlugin.m | 30 +++++----- .../ios/Classes/FLTThreadSafeFlutterResult.h | 18 ++++++ .../ios/Classes/FLTThreadSafeFlutterResult.m | 58 +++++++++++++++++++ 3 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h create mode 100644 packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 4aa01a2c9dfb..7170df653022 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -8,6 +8,7 @@ #import #import #import +#import "FLTThreadSafeFlutterResult.h" static FlutterError *getFlutterError(NSError *error) { return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] @@ -1311,6 +1312,12 @@ - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + FLTThreadSafeFlutterResult *threadSafeResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; + [self handleMethodCallWithThreadSafeResult:call result:threadSafeResult]; +} +- (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); } @@ -1343,9 +1350,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result @"sensorOrientation" : @90, }]; } - result(reply); + [result successWithData:reply]; } else { - result(FlutterMethodNotImplemented); + [result notImplemented]; } } else if ([@"create" isEqualToString:call.method]) { NSString *cameraName = call.arguments[@"cameraName"]; @@ -1361,26 +1368,25 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ if (error) { - result(getFlutterError(error)); + [result error:error]; } else { if (self->_camera) { [self->_camera close]; } int64_t textureId = [self->_registry registerTexture:cam]; self->_camera = cam; - - result(@{ + [result successWithData:@{ @"cameraId" : @(textureId), - }); + }]; } }); }); } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; - result(nil); + [result successWithData:nil]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; - result(nil); + [result successWithData:nil]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -1411,21 +1417,19 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; dispatch_async(_dispatchQueue, ^{ [self->_camera start]; - dispatch_async(dispatch_get_main_queue(), ^{ - result(nil); - }); + [result successWithData:nil]; }); } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; } else { - result(FlutterMethodNotImplemented); + [result notImplemented]; } } else if ([@"dispose" isEqualToString:call.method]) { [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; - result(nil); + [result successWithData:nil]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; result(nil); diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..56365a91713c --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -0,0 +1,18 @@ +// +// Header.h +// camera +// +// Created by Rene Floor on 07/07/2021. +// + +#import + +@interface FLTThreadSafeFlutterResult : NSObject +- (id)initWithResult:(FlutterResult)result; +- (void)successWithData:(id _Nullable)data; +- (void)error:(NSError*)error; +- (void)notImplemented; +- (void)errorWithCode:(NSString*)code + message:(NSString* _Nullable)message + details:(id _Nullable)details; +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..d4e28ba486ba --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -0,0 +1,58 @@ +// +// FLTThreadSafeFlutterResult.m +// camera +// +// Created by Rene Floor on 07/07/2021. +// + +#import "FLTThreadSafeFlutterResult.h" +#import + +@interface FLTThreadSafeFlutterResult () +@property(readonly, nonatomic) FlutterResult flutterResult; +@end + +@implementation FLTThreadSafeFlutterResult { +} + +- (id)initWithResult:(FlutterResult)result { + self = [super init]; + if (!self) { + return nil; + } + _flutterResult = result; + return self; +} + +- (void)successWithData:(id _Nullable)data { + [self send:data]; +} + +- (void)error:(NSError*)error { + [self errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; +} + +- (void)errorWithCode:(NSString*)code + message:(NSString* _Nullable)message + details:(id _Nullable)details { + FlutterError* flutterError = [FlutterError errorWithCode:code message:message details:details]; + [self send:flutterError]; +} + +- (void)notImplemented { + [self send:FlutterMethodNotImplemented]; +} + +- (void)send:(id _Nullable)result { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_flutterResult(result); + }); + } else { + _flutterResult(result); + } +} + +@end From 9bd75bdf1164d2f3eebf52a5254ef5203705aeab Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 9 Jul 2021 15:01:39 +0200 Subject: [PATCH 04/32] Migrated all results to thread safe class --- .../camera/camera/ios/Classes/CameraPlugin.m | 157 +++++++++--------- 1 file changed, 77 insertions(+), 80 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 7170df653022..64fd1f8767d5 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -18,7 +18,7 @@ @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) FlutterResult result; +@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; @end @interface FLTImageStreamHandler : NSObject @@ -44,7 +44,7 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path result:(FlutterResult)result { +- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; @@ -61,7 +61,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(10)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result error:error]; return; } @@ -73,10 +73,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [data writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result errorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result successWithData:_path]; } - (void)captureOutput:(AVCapturePhotoOutput *)output @@ -84,7 +84,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(11.0)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result error:error]; return; } @@ -92,10 +92,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [photoData writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result errorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result successWithData:_path]; } @end @@ -458,7 +458,7 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation } } -- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { +- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; if (_resolutionPreset == max) { [settings setHighResolutionPhotoEnabled:YES]; @@ -474,7 +474,7 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { prefix:@"CAP_" error:error]; if (error) { - result(getFlutterError(error)); + [result error:error]; return; } @@ -811,7 +811,7 @@ - (CVPixelBufferRef)copyPixelBuffer { return pixelBuffer; } -- (void)startVideoRecordingWithResult:(FlutterResult)result { +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (!_isRecording) { NSError *error; _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" @@ -819,11 +819,11 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { prefix:@"REC_" error:error]; if (error) { - result(getFlutterError(error)); + [result error:error]; return; } if (![self setupWriterForPath:_videoRecordingPath]) { - result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); + [result errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; return; } _isRecording = YES; @@ -832,13 +832,13 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { _audioTimeOffset = CMTimeMake(0, 1); _videoIsDisconnected = NO; _audioIsDisconnected = NO; - result(nil); + [result successWithData:nil]; } else { - result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]); + [result errorWithCode:@"Error" message:@"Video is already recording" details:nil]; } } -- (void)stopVideoRecordingWithResult:(FlutterResult)result { +- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (_isRecording) { _isRecording = NO; @@ -846,12 +846,12 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [_videoWriter finishWritingWithCompletionHandler:^{ if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { [self updateOrientation]; - result(self->_videoRecordingPath); + [result successWithData:self->_videoRecordingPath]; self->_videoRecordingPath = nil; } else { - result([FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); + [result errorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]; } }]; } @@ -860,29 +860,29 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorResourceUnavailable userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - result(getFlutterError(error)); + [result error:error]; } } -- (void)pauseVideoRecordingWithResult:(FlutterResult)result { +- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; - result(nil); + [result successWithData:nil]; } -- (void)resumeVideoRecordingWithResult:(FlutterResult)result { +- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = NO; - result(nil); + [result successWithData:nil]; } -- (void)lockCaptureOrientationWithResult:(FlutterResult)result +- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result orientation:(NSString *)orientationStr { UIDeviceOrientation orientation; @try { orientation = getUIDeviceOrientationForString(orientationStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result error:e]; return; } @@ -891,34 +891,34 @@ - (void)lockCaptureOrientationWithResult:(FlutterResult)result [self updateOrientation]; } - result(nil); + [result successWithData:nil]; } -- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { +- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { _lockedCaptureOrientation = UIDeviceOrientationUnknown; [self updateOrientation]; - result(nil); + [result successWithData:nil]; } -- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FlashMode mode; @try { mode = getFlashModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result error:e]; return; } if (mode == FlashModeTorch) { if (!_captureDevice.hasTorch) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support torch mode" - details:nil]); + [result errorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]; return; } if (!_captureDevice.isTorchAvailable) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Torch mode is currently not available" - details:nil]); + [result errorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOn) { @@ -928,17 +928,17 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } else { if (!_captureDevice.hasFlash) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not have flash capabilities" - details:nil]); + [result errorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]; return; } AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); if (![_capturePhotoOutput.supportedFlashModes containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support this specific flash mode" - details:nil]); + [result errorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOff) { @@ -948,20 +948,20 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } _flashMode = mode; - result(nil); + [result successWithData:nil]; } -- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { ExposureMode mode; @try { mode = getExposureModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result error:e]; return; } _exposureMode = mode; [self applyExposureMode]; - result(nil); + [result successWithData:nil]; } - (void)applyExposureMode { @@ -981,17 +981,17 @@ - (void)applyExposureMode { [_captureDevice unlockForConfiguration]; } -- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FocusMode mode; @try { mode = getFocusModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result error:e]; return; } _focusMode = mode; [self applyFocusMode]; - result(nil); + [result successWithData:nil]; } - (void)applyFocusMode { @@ -1031,11 +1031,11 @@ - (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureD [captureDevice unlockForConfiguration]; } -- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { - result([FlutterError errorWithCode:@"setExposurePointFailed" - message:@"Device does not have exposure point capabilities" - details:nil]); + [result errorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]; return; } [_captureDevice lockForConfiguration:nil]; @@ -1043,14 +1043,14 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; - result(nil); + [result successWithData:nil]; } -- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isFocusPointOfInterestSupported) { - result([FlutterError errorWithCode:@"setFocusPointFailed" - message:@"Device does not have focus point capabilities" - details:nil]); + [result errorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]; return; } [_captureDevice lockForConfiguration:nil]; @@ -1058,14 +1058,14 @@ - (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; - result(nil); + [result successWithData:nil]; } -- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { +- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { [_captureDevice lockForConfiguration:nil]; [_captureDevice setExposureTargetBias:offset completionHandler:nil]; [_captureDevice unlockForConfiguration]; - result(@(offset)); + [result successWithData:@(offset)]; } - (void)startImageStreamWithMessenger:(NSObject *)messenger { @@ -1093,19 +1093,18 @@ - (void)stopImageStream { } } -- (void)getMaxZoomLevelWithResult:(FlutterResult)result { +- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; - result([NSNumber numberWithFloat:maxZoomFactor]); + [result successWithData:[NSNumber numberWithFloat:maxZoomFactor]]; } -- (void)getMinZoomLevelWithResult:(FlutterResult)result { +- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; - - result([NSNumber numberWithFloat:minZoomFactor]); + [result successWithData:[NSNumber numberWithFloat:minZoomFactor]]; } -- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { +- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; @@ -1113,22 +1112,20 @@ - (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { NSString *errorMessage = [NSString stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", minAvailableZoomFactor, maxAvailableZoomFactor]; - FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" - message:errorMessage - details:nil]; - result(error); + + [result errorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; return; } NSError *error = nil; if (![_captureDevice lockForConfiguration:&error]) { - result(getFlutterError(error)); + [result error:error]; return; } _captureDevice.videoZoomFactor = zoom; [_captureDevice unlockForConfiguration]; - result(nil); + [result successWithData:nil]; } - (CGFloat)getMinAvailableZoomFactor { @@ -1432,7 +1429,7 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call [result successWithData:nil]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; - result(nil); + [result successWithData:nil]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -1462,11 +1459,11 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call } [_camera setExposurePointWithResult:result x:x y:y]; } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.minExposureTargetBias)); + [result successWithData:@(_camera.captureDevice.minExposureTargetBias)]; } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.maxExposureTargetBias)); + [result successWithData:@(_camera.captureDevice.maxExposureTargetBias)]; } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { - result(@(0.0)); + [result successWithData:@(0.0)]; } else if ([@"setExposureOffset" isEqualToString:call.method]) { [_camera setExposureOffsetWithResult:result offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; @@ -1486,7 +1483,7 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call } [_camera setFocusPointWithResult:result x:x y:y]; } else { - result(FlutterMethodNotImplemented); + [result notImplemented]; } } } From a079af9b19d8f75d4f3accf358a7c04a2cef1a33 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 9 Jul 2021 15:30:00 +0200 Subject: [PATCH 05/32] make data nonNullable --- .../camera/camera/ios/Classes/CameraPlugin.m | 38 ++++++++----------- .../ios/Classes/FLTThreadSafeFlutterResult.h | 9 +++-- .../ios/Classes/FLTThreadSafeFlutterResult.m | 6 ++- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 64fd1f8767d5..c66b668d425d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,12 +10,6 @@ #import #import "FLTThreadSafeFlutterResult.h" -static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} - @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; @@ -832,7 +826,7 @@ - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _audioTimeOffset = CMTimeMake(0, 1); _videoIsDisconnected = NO; _audioIsDisconnected = NO; - [result successWithData:nil]; + [result success]; } else { [result errorWithCode:@"Error" message:@"Video is already recording" details:nil]; } @@ -868,12 +862,12 @@ - (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; - [result successWithData:nil]; + [result success]; } - (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = NO; - [result successWithData:nil]; + [result success]; } - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result @@ -891,13 +885,13 @@ - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result [self updateOrientation]; } - [result successWithData:nil]; + [result success]; } - (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { _lockedCaptureOrientation = UIDeviceOrientationUnknown; [self updateOrientation]; - [result successWithData:nil]; + [result success]; } - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { @@ -948,7 +942,7 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri } } _flashMode = mode; - [result successWithData:nil]; + [result success]; } - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { @@ -961,7 +955,7 @@ - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSS } _exposureMode = mode; [self applyExposureMode]; - [result successWithData:nil]; + [result success]; } - (void)applyExposureMode { @@ -991,7 +985,7 @@ - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri } _focusMode = mode; [self applyFocusMode]; - [result successWithData:nil]; + [result success]; } - (void)applyFocusMode { @@ -1043,7 +1037,7 @@ - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(doubl [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; - [result successWithData:nil]; + [result success]; } - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { @@ -1058,7 +1052,7 @@ - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; - [result successWithData:nil]; + [result success]; } - (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { @@ -1125,7 +1119,7 @@ - (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { _captureDevice.videoZoomFactor = zoom; [_captureDevice unlockForConfiguration]; - [result successWithData:nil]; + [result success]; } - (CGFloat)getMinAvailableZoomFactor { @@ -1380,10 +1374,10 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call }); } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; - [result successWithData:nil]; + [result success]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; - [result successWithData:nil]; + [result success]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -1414,7 +1408,7 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; dispatch_async(_dispatchQueue, ^{ [self->_camera start]; - [result successWithData:nil]; + [result success]; }); } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { @@ -1426,10 +1420,10 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; - [result successWithData:nil]; + [result success]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; - [result successWithData:nil]; + [result success]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 56365a91713c..e125ef6a77c8 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -8,11 +8,12 @@ #import @interface FLTThreadSafeFlutterResult : NSObject -- (id)initWithResult:(FlutterResult)result; -- (void)successWithData:(id _Nullable)data; -- (void)error:(NSError*)error; +- (id _Nonnull)initWithResult:(FlutterResult _Nonnull)result; +- (void)success; +- (void)successWithData:(id _Nonnull)data; +- (void)error:(NSError* _Nonnull)error; - (void)notImplemented; -- (void)errorWithCode:(NSString*)code +- (void)errorWithCode:(NSString* _Nonnull)code message:(NSString* _Nullable)message details:(id _Nullable)details; @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index d4e28ba486ba..60da0fffc6dd 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -24,7 +24,11 @@ - (id)initWithResult:(FlutterResult)result { return self; } -- (void)successWithData:(id _Nullable)data { +- (void)success { + [self send:nil]; +} + +- (void)successWithData:(id)data { [self send:data]; } From bd7d8ebe4341e7e19eb97cd5b9bacdf43fd45a5a Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 9 Jul 2021 15:54:31 +0200 Subject: [PATCH 06/32] copyright --- .../camera/ios/Classes/FLTThreadSafeFlutterResult.h | 9 +++------ .../camera/ios/Classes/FLTThreadSafeFlutterResult.m | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index e125ef6a77c8..7bf9504c5858 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -1,9 +1,6 @@ -// -// Header.h -// camera -// -// Created by Rene Floor on 07/07/2021. -// +// Copyright 2013 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 diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index 60da0fffc6dd..d0d954c25515 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -1,9 +1,6 @@ -// -// FLTThreadSafeFlutterResult.m -// camera -// -// Created by Rene Floor on 07/07/2021. -// +// Copyright 2013 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 "FLTThreadSafeFlutterResult.h" #import From 4125244522790418ddb7748acf2d3906fc461462 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 12 Jul 2021 14:37:42 +0200 Subject: [PATCH 07/32] Added method channel tests --- .../ios/Runner.xcodeproj/project.pbxproj | 4 ++ .../RunnerTests/CameraMethodChannelTests.m | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index aead167a5e99..4e73f3554e50 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; @@ -44,6 +45,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -96,6 +98,7 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -358,6 +361,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m new file mode 100644 index 000000000000..fa81a3e4d1bd --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -0,0 +1,68 @@ +// Copyright 2013 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 camera; +@import XCTest; +@import AVFoundation; + +@interface FLTThreadSafeFlutterResult : NSObject +@property(readonly, nonatomic) FlutterResult flutterResult; +@end + +@interface CameraMethodChannelTests : XCTestCase +@property(readonly, nonatomic) CameraPlugin *camera; +@end + +@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult +@property(nonatomic, copy, readonly) void (^resultCallback)(id result); +@end +@implementation MockFLTThreadSafeFlutterResult +- (id)initWithResultCallback:(void (^)(id))callback { + self = [super init]; + _resultCallback = callback; + return self; +} +- (void)send:(id)result { + NSLog(@"getting result"); + _resultCallback(result); +} +@end + +@interface CameraPlugin (Test) +- (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result; +@end + +@implementation CameraMethodChannelTests + +- (void)setUp { + _camera = [[CameraPlugin alloc] init]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. +} + +- (void)testCreate_ShouldCallResultOnMainThread { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" arguments:nil]; + __block id result = nil; + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithResultCallback:^(id actualResult) { + result = actualResult; + dispatch_semaphore_signal(semaphore); + }]; + + [_camera handleMethodCallWithThreadSafeResult:call result:resultObject]; + + while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; + } + XCTAssertNotNil(result); +} + +@end From 71e321e5dca8634129f414d9ba3923146f76eaaa Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 11:14:34 +0200 Subject: [PATCH 08/32] Extend unit test --- .../RunnerTests/CameraMethodChannelTests.m | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index fa81a3e4d1bd..7ecbeff62626 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -5,6 +5,7 @@ @import camera; @import XCTest; @import AVFoundation; +#import @interface FLTThreadSafeFlutterResult : NSObject @property(readonly, nonatomic) FlutterResult flutterResult; @@ -46,9 +47,21 @@ - (void)tearDown { } - (void)testCreate_ShouldCallResultOnMainThread { + // Setup mocks for initWithCameraName method + id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) + .andReturn([AVCaptureInput alloc]); + + id avCaptureSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); + OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + // Setup method call dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" arguments:nil]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; __block id result = nil; MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithResultCallback:^(id actualResult) { @@ -56,13 +69,23 @@ - (void)testCreate_ShouldCallResultOnMainThread { dispatch_semaphore_signal(semaphore); }]; + // Call handleMethodCall [_camera handleMethodCallWithThreadSafeResult:call result:resultObject]; + // Don't expect a result yet + XCTAssertNil(result); + while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; } + // Expect a result after waiting for thread to switch XCTAssertNotNil(result); + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)result; + XCTAssertNotNil(dictionaryResult); + XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); } @end From bdfde1aef35d896c3db15e21515d5f698e38029d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 11:52:15 +0200 Subject: [PATCH 09/32] replace dispatch loop with notifications --- .../RunnerTests/CameraMethodChannelTests.m | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 7ecbeff62626..d4eee9e76a98 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -11,13 +11,10 @@ @interface FLTThreadSafeFlutterResult : NSObject @property(readonly, nonatomic) FlutterResult flutterResult; @end -@interface CameraMethodChannelTests : XCTestCase -@property(readonly, nonatomic) CameraPlugin *camera; -@end - @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult @property(nonatomic, copy, readonly) void (^resultCallback)(id result); @end + @implementation MockFLTThreadSafeFlutterResult - (id)initWithResultCallback:(void (^)(id))callback { self = [super init]; @@ -35,10 +32,16 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; @end +@interface CameraMethodChannelTests : XCTestCase +@property(readonly, nonatomic) CameraPlugin *camera; +@property(readonly, nonatomic) NSNotificationCenter *notificationCenter; +@end + @implementation CameraMethodChannelTests - (void)setUp { _camera = [[CameraPlugin alloc] init]; + _notificationCenter = [[NSNotificationCenter alloc] init]; } - (void)tearDown { @@ -57,7 +60,11 @@ - (void)testCreate_ShouldCallResultOnMainThread { OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); // Setup method call - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + NSString *notificationName = @"resultNotification"; + XCTNSNotificationExpectation *notificationExpectation = + [[XCTNSNotificationExpectation alloc] initWithName:notificationName + object:nil + notificationCenter:_notificationCenter]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" @@ -66,7 +73,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithResultCallback:^(id actualResult) { result = actualResult; - dispatch_semaphore_signal(semaphore); + [self->_notificationCenter postNotificationName:notificationName object:nil]; }]; // Call handleMethodCall @@ -75,10 +82,8 @@ - (void)testCreate_ShouldCallResultOnMainThread { // Don't expect a result yet XCTAssertNil(result); - while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { - [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode - beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; - } + [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:1]; + // Expect a result after waiting for thread to switch XCTAssertNotNil(result); From e6b848c920557f510f6b5d941705cc432c988617 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 12:31:30 +0200 Subject: [PATCH 10/32] Made test much cleaner --- .../RunnerTests/CameraMethodChannelTests.m | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index d4eee9e76a98..cf2f64a39a9c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -12,18 +12,19 @@ @interface FLTThreadSafeFlutterResult : NSObject @end @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult -@property(nonatomic, copy, readonly) void (^resultCallback)(id result); +@property(readonly, nonatomic) NSNotificationCenter *notificationCenter; +@property(nonatomic, nullable) id receivedResult; @end @implementation MockFLTThreadSafeFlutterResult -- (id)initWithResultCallback:(void (^)(id))callback { +- (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { self = [super init]; - _resultCallback = callback; + _notificationCenter = notificationCenter; return self; } -- (void)send:(id)result { - NSLog(@"getting result"); - _resultCallback(result); +- (void)successWithData:(id)data { + _receivedResult = data; + [self->_notificationCenter postNotificationName:@"successWithData" object:nil]; } @end @@ -34,6 +35,7 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call @interface CameraMethodChannelTests : XCTestCase @property(readonly, nonatomic) CameraPlugin *camera; +@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject; @property(readonly, nonatomic) NSNotificationCenter *notificationCenter; @end @@ -42,14 +44,7 @@ @implementation CameraMethodChannelTests - (void)setUp { _camera = [[CameraPlugin alloc] init]; _notificationCenter = [[NSNotificationCenter alloc] init]; -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the - // class. -} -- (void)testCreate_ShouldCallResultOnMainThread { // Setup mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) @@ -59,36 +54,39 @@ - (void)testCreate_ShouldCallResultOnMainThread { OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + _resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithNotificationCenter:_notificationCenter]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. +} + +- (void)testCreate_ShouldCallResultOnMainThread { // Setup method call - NSString *notificationName = @"resultNotification"; XCTNSNotificationExpectation *notificationExpectation = - [[XCTNSNotificationExpectation alloc] initWithName:notificationName + [[XCTNSNotificationExpectation alloc] initWithName:@"successWithData" object:nil notificationCenter:_notificationCenter]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - __block id result = nil; - MockFLTThreadSafeFlutterResult *resultObject = - [[MockFLTThreadSafeFlutterResult alloc] initWithResultCallback:^(id actualResult) { - result = actualResult; - [self->_notificationCenter postNotificationName:notificationName object:nil]; - }]; // Call handleMethodCall - [_camera handleMethodCallWithThreadSafeResult:call result:resultObject]; + [_camera handleMethodCallWithThreadSafeResult:call result:_resultObject]; // Don't expect a result yet - XCTAssertNil(result); + XCTAssertNil(_resultObject.receivedResult); - [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:1]; + [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:0.1]; // Expect a result after waiting for thread to switch - XCTAssertNotNil(result); + XCTAssertNotNil(_resultObject.receivedResult); // Verify the result - NSDictionary *dictionaryResult = (NSDictionary *)result; + NSDictionary *dictionaryResult = (NSDictionary *)_resultObject.receivedResult; XCTAssertNotNil(dictionaryResult); XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); } From d59a80de96b83fee63bc8f22c4f86a4a7895bd44 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 14:42:21 +0200 Subject: [PATCH 11/32] add return for error --- packages/camera/camera/ios/Classes/CameraPlugin.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index c66b668d425d..226c7e985cc9 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -358,6 +358,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _resolutionPreset = getResolutionPresetForString(resolutionPreset); } @catch (NSError *e) { *error = e; + return nil; } _enableAudio = enableAudio; _dispatchQueue = dispatchQueue; From 0f35095e9c2b9b666a7f69f9493b2935aa9f6c61 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 14:45:51 +0200 Subject: [PATCH 12/32] changelog and pubspec --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 2cab8e123ae6..732b40ac2bcf 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1+4 + +* Fix registerTexture and result being called on background thread on iOS. + ## 0.8.1+3 * Do not change camera orientation when iOS device is flat. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a7df9e0d51be..789910e2c79b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+3 +version: 0.8.1+4 environment: sdk: ">=2.12.0 <3.0.0" From 0b63dea129eeb25ae61e13f551a30df820e79ec8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 13 Jul 2021 15:12:56 +0200 Subject: [PATCH 13/32] Add documentation --- .../RunnerTests/CameraMethodChannelTests.m | 7 ++++ .../ios/Classes/FLTThreadSafeFlutterResult.h | 32 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index cf2f64a39a9c..01a1c2ed4f22 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -17,11 +17,18 @@ @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult @end @implementation MockFLTThreadSafeFlutterResult +/** + Initialize with a notification center. + */ - (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { self = [super init]; _notificationCenter = notificationCenter; return self; } + +/** + Called when result is successful. Sends "successWithData" to the notification center. + */ - (void)successWithData:(id)data { _receivedResult = data; [self->_notificationCenter postNotificationName:@"successWithData" object:nil]; diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 7bf9504c5858..493eacf52841 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -4,13 +4,43 @@ #import +/** + Wrapper object that always delivers the result on the main thread. + */ @interface FLTThreadSafeFlutterResult : NSObject + +/** + Initialize with a FlutterResult object. + @param result The FlutterResult object that the result will be given to. + */ - (id _Nonnull)initWithResult:(FlutterResult _Nonnull)result; + +/** + Send a successful result without any data. + */ - (void)success; + +/** + Send a successful result with data. + @param data Result data that is send to the Flutter Dart side. + */ - (void)successWithData:(id _Nonnull)data; + +/** + Send an error as result + @param error Error that will be send as FlutterError. + */ - (void)error:(NSError* _Nonnull)error; -- (void)notImplemented; + +/** + Send a FlutterError as result. + */ - (void)errorWithCode:(NSString* _Nonnull)code message:(NSString* _Nullable)message details:(id _Nullable)details; + +/** + Send FlutterMethodNotImplemented as result. + */ +- (void)notImplemented; @end From b53ab3ec3547b4570a84c8ea02fcc7dbd1bd0ee0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 14 Jul 2021 09:17:26 +0200 Subject: [PATCH 14/32] Make interface an extension Co-authored-by: Maurits van Beusekom --- .../camera/example/ios/RunnerTests/CameraMethodChannelTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 01a1c2ed4f22..d1a3383e5a64 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -7,7 +7,7 @@ @import AVFoundation; #import -@interface FLTThreadSafeFlutterResult : NSObject +@interface FLTThreadSafeFlutterResult () @property(readonly, nonatomic) FlutterResult flutterResult; @end From b6acf82326f556ec038f02ff8b2d4cf185e6c4c1 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 14 Jul 2021 09:41:20 +0200 Subject: [PATCH 15/32] Increase timeout --- .../camera/example/ios/RunnerTests/CameraMethodChannelTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index d1a3383e5a64..967792c58b3a 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -87,7 +87,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { // Don't expect a result yet XCTAssertNil(_resultObject.receivedResult); - [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:0.1]; + [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:1]; // Expect a result after waiting for thread to switch XCTAssertNotNil(_resultObject.receivedResult); From a4cf79ee79e4deb6916f429c4cf3b590d22eee60 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 16 Aug 2021 15:19:34 +0200 Subject: [PATCH 16/32] update broken test --- .../camera/camera/example/ios/RunnerTests/CameraFocusTests.m | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m index 27537e7ebdac..88974b2d011b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -19,7 +19,7 @@ @interface FLTCam : NSObject Date: Mon, 16 Aug 2021 16:18:08 +0200 Subject: [PATCH 17/32] Documentation improvements --- .../RunnerTests/CameraMethodChannelTests.m | 4 +-- .../camera/camera/ios/Classes/CameraPlugin.m | 1 - .../ios/Classes/FLTThreadSafeFlutterResult.h | 32 +++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 967792c58b3a..0372248aa6f3 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -18,7 +18,7 @@ @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult @implementation MockFLTThreadSafeFlutterResult /** - Initialize with a notification center. + * Initializes with a notification center. */ - (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { self = [super init]; @@ -27,7 +27,7 @@ - (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { } /** - Called when result is successful. Sends "successWithData" to the notification center. + * Called when result is successful. Sends "successWithData" to the notification center. */ - (void)successWithData:(id)data { _receivedResult = data; diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ec66572744fe..316b1d30149d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -358,7 +358,6 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _resolutionPreset = getResolutionPresetForString(resolutionPreset); } @catch (NSError *e) { *error = e; - return nil; } _enableAudio = enableAudio; _dispatchQueue = dispatchQueue; diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 493eacf52841..b957abb87c88 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -5,42 +5,42 @@ #import /** - Wrapper object that always delivers the result on the main thread. + * Wrapper for FlutterResult that always delivers the result on the main thread. */ @interface FLTThreadSafeFlutterResult : NSObject /** - Initialize with a FlutterResult object. - @param result The FlutterResult object that the result will be given to. + * Initializes with a FlutterResult object. + * @param result The FlutterResult object that the result will be given to. */ -- (id _Nonnull)initWithResult:(FlutterResult _Nonnull)result; +- (nonnull id)initWithResult:(nonnull FlutterResult)result; /** - Send a successful result without any data. + * Sends a successful result without any data. */ - (void)success; /** - Send a successful result with data. - @param data Result data that is send to the Flutter Dart side. + * Sends a successful result with data. + * @param data Result data that is send to the Flutter Dart side. */ -- (void)successWithData:(id _Nonnull)data; +- (void)successWithData:(nonnull id)data; /** - Send an error as result - @param error Error that will be send as FlutterError. + * Sends an NSError as result + * @param error Error that will be send as FlutterError. */ -- (void)error:(NSError* _Nonnull)error; +- (void)error:(nonnull NSError*)error; /** - Send a FlutterError as result. + * Sends a FlutterError as result. */ -- (void)errorWithCode:(NSString* _Nonnull)code - message:(NSString* _Nullable)message - details:(id _Nullable)details; +- (void)errorWithCode:(nonnull NSString*)code + message:(nullable NSString*)message + details:(nullable id)details; /** - Send FlutterMethodNotImplemented as result. + * Sends FlutterMethodNotImplemented as result. */ - (void)notImplemented; @end From 92c2d00789c154487111d549449629adea229e62 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 16 Aug 2021 16:37:04 +0200 Subject: [PATCH 18/32] Make result methods verbs --- .../RunnerTests/CameraMethodChannelTests.m | 2 +- .../camera/camera/ios/Classes/CameraPlugin.m | 106 +++++++++--------- .../ios/Classes/FLTThreadSafeFlutterResult.h | 10 +- .../ios/Classes/FLTThreadSafeFlutterResult.m | 12 +- 4 files changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 0372248aa6f3..76eed9af7250 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -29,7 +29,7 @@ - (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { /** * Called when result is successful. Sends "successWithData" to the notification center. */ -- (void)successWithData:(id)data { +- (void)sendSuccessWithData:(id)data { _receivedResult = data; [self->_notificationCenter postNotificationName:@"successWithData" object:nil]; } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 316b1d30149d..99f209ec1b0a 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -55,7 +55,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(10)) { selfReference = nil; if (error) { - [_result error:error]; + [_result sendError:error]; return; } @@ -67,10 +67,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [data writeToFile:_path atomically:YES]; if (!success) { - [_result errorWithCode:@"IOError" message:@"Unable to write file" details:nil]; + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - [_result successWithData:_path]; + [_result sendSuccessWithData:_path]; } - (void)captureOutput:(AVCapturePhotoOutput *)output @@ -78,7 +78,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(11.0)) { selfReference = nil; if (error) { - [_result error:error]; + [_result sendError:error]; return; } @@ -86,10 +86,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [photoData writeToFile:_path atomically:YES]; if (!success) { - [_result errorWithCode:@"IOError" message:@"Unable to write file" details:nil]; + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - [_result successWithData:_path]; + [_result sendSuccessWithData:_path]; } @end @@ -468,7 +468,7 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) prefix:@"CAP_" error:error]; if (error) { - [result error:error]; + [result sendError:error]; return; } @@ -813,11 +813,11 @@ - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { prefix:@"REC_" error:error]; if (error) { - [result error:error]; + [result sendError:error]; return; } if (![self setupWriterForPath:_videoRecordingPath]) { - [result errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; + [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; return; } _isRecording = YES; @@ -826,9 +826,9 @@ - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _audioTimeOffset = CMTimeMake(0, 1); _videoIsDisconnected = NO; _audioIsDisconnected = NO; - [result success]; + [result sendSuccess]; } else { - [result errorWithCode:@"Error" message:@"Video is already recording" details:nil]; + [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil]; } } @@ -840,10 +840,10 @@ - (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { [_videoWriter finishWritingWithCompletionHandler:^{ if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { [self updateOrientation]; - [result successWithData:self->_videoRecordingPath]; + [result sendSuccessWithData:self->_videoRecordingPath]; self->_videoRecordingPath = nil; } else { - [result errorWithCode:@"IOError" + [result sendErrorWithCode:@"IOError" message:@"AVAssetWriter could not finish writing!" details:nil]; } @@ -854,7 +854,7 @@ - (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorResourceUnavailable userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - [result error:error]; + [result sendError:error]; } } @@ -862,12 +862,12 @@ - (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; - [result success]; + [result sendSuccess]; } - (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = NO; - [result success]; + [result sendSuccess]; } - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result @@ -876,7 +876,7 @@ - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result @try { orientation = getUIDeviceOrientationForString(orientationStr); } @catch (NSError *e) { - [result error:e]; + [result sendError:e]; return; } @@ -885,13 +885,13 @@ - (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result [self updateOrientation]; } - [result success]; + [result sendSuccess]; } - (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { _lockedCaptureOrientation = UIDeviceOrientationUnknown; [self updateOrientation]; - [result success]; + [result sendSuccess]; } - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { @@ -899,18 +899,18 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri @try { mode = getFlashModeForString(modeStr); } @catch (NSError *e) { - [result error:e]; + [result sendError:e]; return; } if (mode == FlashModeTorch) { if (!_captureDevice.hasTorch) { - [result errorWithCode:@"setFlashModeFailed" + [result sendErrorWithCode:@"setFlashModeFailed" message:@"Device does not support torch mode" details:nil]; return; } if (!_captureDevice.isTorchAvailable) { - [result errorWithCode:@"setFlashModeFailed" + [result sendErrorWithCode:@"setFlashModeFailed" message:@"Torch mode is currently not available" details:nil]; return; @@ -922,7 +922,7 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri } } else { if (!_captureDevice.hasFlash) { - [result errorWithCode:@"setFlashModeFailed" + [result sendErrorWithCode:@"setFlashModeFailed" message:@"Device does not have flash capabilities" details:nil]; return; @@ -930,7 +930,7 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); if (![_capturePhotoOutput.supportedFlashModes containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { - [result errorWithCode:@"setFlashModeFailed" + [result sendErrorWithCode:@"setFlashModeFailed" message:@"Device does not support this specific flash mode" details:nil]; return; @@ -942,7 +942,7 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri } } _flashMode = mode; - [result success]; + [result sendSuccess]; } - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { @@ -950,12 +950,12 @@ - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSS @try { mode = getExposureModeForString(modeStr); } @catch (NSError *e) { - [result error:e]; + [result sendError:e]; return; } _exposureMode = mode; [self applyExposureMode]; - [result success]; + [result sendSuccess]; } - (void)applyExposureMode { @@ -980,12 +980,12 @@ - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri @try { mode = getFocusModeForString(modeStr); } @catch (NSError *e) { - [result error:e]; + [result sendError:e]; return; } _focusMode = mode; [self applyFocusMode]; - [result success]; + [result sendSuccess]; } - (void)applyFocusMode { @@ -1052,7 +1052,7 @@ - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { - [result errorWithCode:@"setExposurePointFailed" + [result sendErrorWithCode:@"setExposurePointFailed" message:@"Device does not have exposure point capabilities" details:nil]; return; @@ -1065,12 +1065,12 @@ - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(doubl [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; - [result success]; + [result sendSuccess]; } - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isFocusPointOfInterestSupported) { - [result errorWithCode:@"setFocusPointFailed" + [result sendErrorWithCode:@"setFocusPointFailed" message:@"Device does not have focus point capabilities" details:nil]; return; @@ -1084,14 +1084,14 @@ - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; - [result success]; + [result sendSuccess]; } - (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { [_captureDevice lockForConfiguration:nil]; [_captureDevice setExposureTargetBias:offset completionHandler:nil]; [_captureDevice unlockForConfiguration]; - [result successWithData:@(offset)]; + [result sendSuccessWithData:@(offset)]; } - (void)startImageStreamWithMessenger:(NSObject *)messenger { @@ -1122,12 +1122,12 @@ - (void)stopImageStream { - (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; - [result successWithData:[NSNumber numberWithFloat:maxZoomFactor]]; + [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]]; } - (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; - [result successWithData:[NSNumber numberWithFloat:minZoomFactor]]; + [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]]; } - (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { @@ -1139,19 +1139,19 @@ - (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", minAvailableZoomFactor, maxAvailableZoomFactor]; - [result errorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; + [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; return; } NSError *error = nil; if (![_captureDevice lockForConfiguration:&error]) { - [result error:error]; + [result sendError:error]; return; } _captureDevice.videoZoomFactor = zoom; [_captureDevice unlockForConfiguration]; - [result success]; + [result sendSuccess]; } - (CGFloat)getMinAvailableZoomFactor { @@ -1373,9 +1373,9 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call @"sensorOrientation" : @90, }]; } - [result successWithData:reply]; + [result sendSuccessWithData:reply]; } else { - [result notImplemented]; + [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { NSString *cameraName = call.arguments[@"cameraName"]; @@ -1391,14 +1391,14 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ if (error) { - [result error:error]; + [result sendError:error]; } else { if (self->_camera) { [self->_camera close]; } int64_t textureId = [self->_registry registerTexture:cam]; self->_camera = cam; - [result successWithData:@{ + [result sendSuccessWithData:@{ @"cameraId" : @(textureId), }]; } @@ -1406,10 +1406,10 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call }); } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; - [result success]; + [result sendSuccess]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; - [result success]; + [result sendSuccess]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -1440,22 +1440,22 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; dispatch_async(_dispatchQueue, ^{ [self->_camera start]; - [result success]; + [result sendSuccess]; }); } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; } else { - [result notImplemented]; + [result sendNotImplemented]; } } else if ([@"dispose" isEqualToString:call.method]) { [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; - [result success]; + [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; - [result success]; + [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -1485,11 +1485,11 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call } [_camera setExposurePointWithResult:result x:x y:y]; } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { - [result successWithData:@(_camera.captureDevice.minExposureTargetBias)]; + [result sendSuccessWithData:@(_camera.captureDevice.minExposureTargetBias)]; } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { - [result successWithData:@(_camera.captureDevice.maxExposureTargetBias)]; + [result sendSuccessWithData:@(_camera.captureDevice.maxExposureTargetBias)]; } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { - [result successWithData:@(0.0)]; + [result sendSuccessWithData:@(0.0)]; } else if ([@"setExposureOffset" isEqualToString:call.method]) { [_camera setExposureOffsetWithResult:result offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; @@ -1509,7 +1509,7 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call } [_camera setFocusPointWithResult:result x:x y:y]; } else { - [result notImplemented]; + [result sendNotImplemented]; } } } diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index b957abb87c88..981f4cc90e23 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -18,29 +18,29 @@ /** * Sends a successful result without any data. */ -- (void)success; +- (void)sendSuccess; /** * Sends a successful result with data. * @param data Result data that is send to the Flutter Dart side. */ -- (void)successWithData:(nonnull id)data; +- (void)sendSuccessWithData:(nonnull id)data; /** * Sends an NSError as result * @param error Error that will be send as FlutterError. */ -- (void)error:(nonnull NSError*)error; +- (void)sendError:(nonnull NSError*)error; /** * Sends a FlutterError as result. */ -- (void)errorWithCode:(nonnull NSString*)code +- (void)sendErrorWithCode:(nonnull NSString*)code message:(nullable NSString*)message details:(nullable id)details; /** * Sends FlutterMethodNotImplemented as result. */ -- (void)notImplemented; +- (void)sendNotImplemented; @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index d0d954c25515..8af3fc717acf 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -21,28 +21,28 @@ - (id)initWithResult:(FlutterResult)result { return self; } -- (void)success { +- (void)sendSuccess { [self send:nil]; } -- (void)successWithData:(id)data { +- (void)sendSuccessWithData:(id)data { [self send:data]; } -- (void)error:(NSError*)error { - [self errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] +- (void)sendError:(NSError*)error { + [self sendErrorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] message:error.localizedDescription details:error.domain]; } -- (void)errorWithCode:(NSString*)code +- (void)sendErrorWithCode:(NSString*)code message:(NSString* _Nullable)message details:(id _Nullable)details { FlutterError* flutterError = [FlutterError errorWithCode:code message:message details:details]; [self send:flutterError]; } -- (void)notImplemented { +- (void)sendNotImplemented { [self send:FlutterMethodNotImplemented]; } From 2722cf40b0050a36af3b815a369ede33baffd2ac Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 16 Aug 2021 16:40:21 +0200 Subject: [PATCH 19/32] small doc changes --- .../example/ios/RunnerTests/CameraMethodChannelTests.m | 10 ++-------- .../camera/ios/Classes/FLTThreadSafeFlutterResult.m | 3 +++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 76eed9af7250..aeba3184b01a 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -52,7 +52,7 @@ - (void)setUp { _camera = [[CameraPlugin alloc] init]; _notificationCenter = [[NSNotificationCenter alloc] init]; - // Setup mocks for initWithCameraName method + // Set up mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) .andReturn([AVCaptureInput alloc]); @@ -65,13 +65,8 @@ - (void)setUp { [[MockFLTThreadSafeFlutterResult alloc] initWithNotificationCenter:_notificationCenter]; } -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the - // class. -} - - (void)testCreate_ShouldCallResultOnMainThread { - // Setup method call + // Set up method call XCTNSNotificationExpectation *notificationExpectation = [[XCTNSNotificationExpectation alloc] initWithName:@"successWithData" object:nil @@ -81,7 +76,6 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - // Call handleMethodCall [_camera handleMethodCallWithThreadSafeResult:call result:_resultObject]; // Don't expect a result yet diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index 8af3fc717acf..0a4159e7f3b5 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -46,6 +46,9 @@ - (void)sendNotImplemented { [self send:FlutterMethodNotImplemented]; } +/** + * Sends result to flutterResult on the main thread. + */ - (void)send:(id _Nullable)result { if (!NSThread.isMainThread) { dispatch_async(dispatch_get_main_queue(), ^{ From 4a1155a9ce2e2ffb7582220e03fe0dcdd59ed297 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 17 Aug 2021 10:49:32 +0200 Subject: [PATCH 20/32] remove notification center --- .../RunnerTests/CameraMethodChannelTests.m | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index aeba3184b01a..c5d61a1efd43 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -11,8 +11,12 @@ @interface FLTThreadSafeFlutterResult () @property(readonly, nonatomic) FlutterResult flutterResult; @end +/** + * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and + * read the received result. + */ @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult -@property(readonly, nonatomic) NSNotificationCenter *notificationCenter; +@property(readonly, nonatomic) XCTestExpectation *expectation; @property(nonatomic, nullable) id receivedResult; @end @@ -20,18 +24,18 @@ @implementation MockFLTThreadSafeFlutterResult /** * Initializes with a notification center. */ -- (id)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter { +- (id)initWithExpectation:(XCTestExpectation *)expectation { self = [super init]; - _notificationCenter = notificationCenter; + _expectation = expectation; return self; } /** - * Called when result is successful. Sends "successWithData" to the notification center. + * Called when result is successful. Fulfills the expectation. */ - (void)sendSuccessWithData:(id)data { _receivedResult = data; - [self->_notificationCenter postNotificationName:@"successWithData" object:nil]; + [self->_expectation fulfill]; } @end @@ -43,14 +47,13 @@ - (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call @interface CameraMethodChannelTests : XCTestCase @property(readonly, nonatomic) CameraPlugin *camera; @property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject; -@property(readonly, nonatomic) NSNotificationCenter *notificationCenter; @end @implementation CameraMethodChannelTests - (void)setUp { _camera = [[CameraPlugin alloc] init]; - _notificationCenter = [[NSNotificationCenter alloc] init]; + XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; // Set up mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); @@ -62,16 +65,11 @@ - (void)setUp { OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); _resultObject = - [[MockFLTThreadSafeFlutterResult alloc] initWithNotificationCenter:_notificationCenter]; + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; } - (void)testCreate_ShouldCallResultOnMainThread { // Set up method call - XCTNSNotificationExpectation *notificationExpectation = - [[XCTNSNotificationExpectation alloc] initWithName:@"successWithData" - object:nil - notificationCenter:_notificationCenter]; - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; @@ -81,7 +79,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { // Don't expect a result yet XCTAssertNil(_resultObject.receivedResult); - [self waitForExpectations:[NSArray arrayWithObject:notificationExpectation] timeout:1]; + [self waitForExpectations:[NSArray arrayWithObject:_resultObject.expectation] timeout:1]; // Expect a result after waiting for thread to switch XCTAssertNotNil(_resultObject.receivedResult); From 04566aee2ca16879eb8884e581a61f142a56be10 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 17 Aug 2021 15:41:03 +0200 Subject: [PATCH 21/32] Added unit test for thread safe result --- .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/CameraFocusTests.m | 9 +- .../RunnerTests/CameraMethodChannelTests.m | 6 +- .../ThreadSafeFlutterResultTests.m | 122 ++++++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 28 ++-- .../ios/Classes/FLTThreadSafeFlutterResult.h | 4 +- .../ios/Classes/FLTThreadSafeFlutterResult.m | 8 +- 7 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 4e73f3554e50..883d46d4e9ff 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -50,6 +51,7 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -99,6 +101,7 @@ 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -361,6 +364,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m index 88974b2d011b..fdc2be9901a4 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -128,10 +128,11 @@ - (void)testSetFocusPointWithResult_SetsFocusPointOfInterest { [_camera setValue:_mockDevice forKey:@"captureDevice"]; // Run test - [_camera - setFocusPointWithResult:[[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) {}] - x:1 - y:1]; + [_camera setFocusPointWithResult:[[FLTThreadSafeFlutterResult alloc] + initWithResult:^(id _Nullable result){ + }] + x:1 + y:1]; // Verify the focus point of interest has been set OCMVerify([_mockDevice setFocusPointOfInterest:CGPointMake(1, 1)]); diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index c5d61a1efd43..321b1e3f8b3b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -53,7 +53,8 @@ @implementation CameraMethodChannelTests - (void)setUp { _camera = [[CameraPlugin alloc] init]; - XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; // Set up mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); @@ -64,8 +65,7 @@ - (void)setUp { OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - _resultObject = - [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + _resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; } - (void)testCreate_ShouldCallResultOnMainThread { diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m new file mode 100644 index 000000000000..8cd4b8bc8c2a --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m @@ -0,0 +1,122 @@ +// Copyright 2013 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 camera; +@import XCTest; + +@interface ThreadSafeFlutterResultTests : XCTestCase +@end + +@implementation ThreadSafeFlutterResultTests +- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccess]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + [threadSafeFlutterResult sendSuccess]; + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterMethodNotImplemented.class]); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendNotImplemented]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult { + NSString* errorCode = @"errorCode"; + NSString* errorMessage = @"message"; + NSString* errorDetails = @"error details"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + XCTAssertEqualObjects(error.code, errorCode); + XCTAssertEqualObjects(error.message, errorMessage); + XCTAssertEqualObjects(error.details, errorDetails); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNSError_ShouldSendErrorToFlutterResult { + NSError* originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil]; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + NSString* constructedErrorCode = + [NSString stringWithFormat:@"Error %d", (int)originalError.code]; + XCTAssertEqualObjects(error.code, constructedErrorCode); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendError:originalError]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendResult_ShouldSendResultToFlutterResult { + NSString* resultData = @"resultData"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssertEqualObjects(result, resultData); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccessWithData:resultData]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 99f209ec1b0a..9314edc87941 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -844,8 +844,8 @@ - (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { self->_videoRecordingPath = nil; } else { [result sendErrorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]; + message:@"AVAssetWriter could not finish writing!" + details:nil]; } }]; } @@ -905,14 +905,14 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri if (mode == FlashModeTorch) { if (!_captureDevice.hasTorch) { [result sendErrorWithCode:@"setFlashModeFailed" - message:@"Device does not support torch mode" - details:nil]; + message:@"Device does not support torch mode" + details:nil]; return; } if (!_captureDevice.isTorchAvailable) { [result sendErrorWithCode:@"setFlashModeFailed" - message:@"Torch mode is currently not available" - details:nil]; + message:@"Torch mode is currently not available" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOn) { @@ -923,16 +923,16 @@ - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSStri } else { if (!_captureDevice.hasFlash) { [result sendErrorWithCode:@"setFlashModeFailed" - message:@"Device does not have flash capabilities" - details:nil]; + message:@"Device does not have flash capabilities" + details:nil]; return; } AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); if (![_capturePhotoOutput.supportedFlashModes containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { [result sendErrorWithCode:@"setFlashModeFailed" - message:@"Device does not support this specific flash mode" - details:nil]; + message:@"Device does not support this specific flash mode" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOff) { @@ -1053,8 +1053,8 @@ - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { [result sendErrorWithCode:@"setExposurePointFailed" - message:@"Device does not have exposure point capabilities" - details:nil]; + message:@"Device does not have exposure point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; @@ -1071,8 +1071,8 @@ - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(doubl - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isFocusPointOfInterestSupported) { [result sendErrorWithCode:@"setFocusPointFailed" - message:@"Device does not have focus point capabilities" - details:nil]; + message:@"Device does not have focus point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 981f4cc90e23..6cd99ccef583 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -36,8 +36,8 @@ * Sends a FlutterError as result. */ - (void)sendErrorWithCode:(nonnull NSString*)code - message:(nullable NSString*)message - details:(nullable id)details; + message:(nullable NSString*)message + details:(nullable id)details; /** * Sends FlutterMethodNotImplemented as result. diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index 0a4159e7f3b5..d2eedb36cb50 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -31,13 +31,13 @@ - (void)sendSuccessWithData:(id)data { - (void)sendError:(NSError*)error { [self sendErrorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; + message:error.localizedDescription + details:error.domain]; } - (void)sendErrorWithCode:(NSString*)code - message:(NSString* _Nullable)message - details:(id _Nullable)details { + message:(NSString* _Nullable)message + details:(id _Nullable)details { FlutterError* flutterError = [FlutterError errorWithCode:code message:message details:details]; [self send:flutterError]; } From 57c63ae1f031314dec254ac2f1347353a783e045 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 17 Aug 2021 15:44:53 +0200 Subject: [PATCH 22/32] add extra comment --- .../camera/example/ios/RunnerTests/CameraMethodChannelTests.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 321b1e3f8b3b..1b3461c48290 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -77,6 +77,8 @@ - (void)testCreate_ShouldCallResultOnMainThread { [_camera handleMethodCallWithThreadSafeResult:call result:_resultObject]; // Don't expect a result yet + // The initWithCameraName method should do some heavy operations on a second thread. This verifies + // the dispatch is not removed. XCTAssertNil(_resultObject.receivedResult); [self waitForExpectations:[NSArray arrayWithObject:_resultObject.expectation] timeout:1]; From e2ba654c8a0cc6b29dcc19fd50a4cc2fcad87500 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 8 Oct 2021 17:55:15 +0200 Subject: [PATCH 23/32] Revert removing handleMethodCallAsync. --- packages/camera/camera/ios/Classes/CameraPlugin.m | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 035774cabdf3..4d2fd1c68053 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1351,14 +1351,19 @@ - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { FLTThreadSafeFlutterResult *threadSafeResult = [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; - [self handleMethodCallWithThreadSafeResult:call result:threadSafeResult]; -} -- (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call - result:(FLTThreadSafeFlutterResult *)result { + if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); } + // Invoke the plugin on another dispatch queue to avoid blocking the UI. + dispatch_async(_dispatchQueue, ^{ + [self handleMethodCallAsync:call result:threadSafeResult]; + }); +} + +- (void)handleMethodCallAsync:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { if ([@"availableCameras" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession From 17d1f5d7e4a8a60f832f7dd6bc0b3b09665ca7b2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 11 Oct 2021 15:31:44 +0200 Subject: [PATCH 24/32] Fix unit tests --- .../ios/Runner.xcodeproj/project.pbxproj | 95 ++++++++++--------- .../xcshareddata/xcschemes/Runner.xcscheme | 12 ++- .../RunnerTests/CameraMethodChannelTests.m | 48 +--------- .../ios/RunnerTests/CameraPreviewPauseTests.m | 26 ++--- .../MockFLTThreadSafeFlutterResult.h | 27 ++++++ .../MockFLTThreadSafeFlutterResult.m | 39 ++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 49 +++++----- 7 files changed, 166 insertions(+), 130 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h create mode 100644 packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 5dd10da26d88..98b3109892eb 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */; }; 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -18,9 +20,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,14 +56,14 @@ 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 89D82918721FABF772705DB0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -71,9 +72,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -81,7 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */, + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,7 +92,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */, + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,15 +108,17 @@ 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, ); path = RunnerTests; sourceTree = ""; }; - 78D1009194BD06C03BED950D /* Frameworks */ = { + 3242FD2B467C15C62200632F /* Frameworks */ = { isa = PBXGroup; children = ( - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */, - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */, + 89D82918721FABF772705DB0 /* libPods-Runner.a */, + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -137,7 +142,7 @@ 03BB76692665316900CE5A93 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, FD386F00E98D73419C929072 /* Pods */, - 78D1009194BD06C03BED950D /* Frameworks */, + 3242FD2B467C15C62200632F /* Frameworks */, ); sourceTree = ""; }; @@ -177,10 +182,10 @@ FD386F00E98D73419C929072 /* Pods */ = { isa = PBXGroup; children = ( - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */, - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */, - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */, - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */, + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */, + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */, + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */, + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -192,7 +197,7 @@ isa = PBXNativeTarget; buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */, + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */, 03BB76642665316900CE5A93 /* Sources */, 03BB76652665316900CE5A93 /* Frameworks */, 03BB76662665316900CE5A93 /* Resources */, @@ -211,7 +216,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */, + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -244,6 +249,7 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; }; }; @@ -288,7 +294,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -303,28 +323,28 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */ = { + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -339,27 +359,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -371,6 +377,7 @@ 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -417,7 +424,7 @@ /* Begin XCBuildConfiguration section */ 03BB766F2665316900CE5A93 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -442,7 +449,7 @@ }; 03BB76702665316900CE5A93 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -575,6 +582,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -596,6 +604,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1447e08231be..869ea983895a 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,7 +26,9 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + enableAddressSanitizer = "YES" + enableASanStackUseAfterReturn = "YES"> + + + + @@ -53,6 +62,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableASanStackUseAfterReturn = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 1b3461c48290..8cae9eb2f4a2 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -6,41 +6,10 @@ @import XCTest; @import AVFoundation; #import - -@interface FLTThreadSafeFlutterResult () -@property(readonly, nonatomic) FlutterResult flutterResult; -@end - -/** - * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and - * read the received result. - */ -@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult -@property(readonly, nonatomic) XCTestExpectation *expectation; -@property(nonatomic, nullable) id receivedResult; -@end - -@implementation MockFLTThreadSafeFlutterResult -/** - * Initializes with a notification center. - */ -- (id)initWithExpectation:(XCTestExpectation *)expectation { - self = [super init]; - _expectation = expectation; - return self; -} - -/** - * Called when result is successful. Fulfills the expectation. - */ -- (void)sendSuccessWithData:(id)data { - _receivedResult = data; - [self->_expectation fulfill]; -} -@end +#import "MockFLTThreadSafeFlutterResult.h" @interface CameraPlugin (Test) -- (void)handleMethodCallWithThreadSafeResult:(FlutterMethodCall *)call +- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; @end @@ -74,17 +43,8 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [_camera handleMethodCallWithThreadSafeResult:call result:_resultObject]; - - // Don't expect a result yet - // The initWithCameraName method should do some heavy operations on a second thread. This verifies - // the dispatch is not removed. - XCTAssertNil(_resultObject.receivedResult); - - [self waitForExpectations:[NSArray arrayWithObject:_resultObject.expectation] timeout:1]; - - // Expect a result after waiting for thread to switch - XCTAssertNotNil(_resultObject.receivedResult); + + [self->_camera handleMethodCallAsync:call result:self->_resultObject]; // Verify the result NSDictionary *dictionaryResult = (NSDictionary *)_resultObject.receivedResult; diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m index 549b40a52e46..05d8f6e9e920 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -6,44 +6,38 @@ @import XCTest; @import AVFoundation; #import +#import "MockFLTThreadSafeFlutterResult.h" @interface FLTCam : NSObject @property(assign, nonatomic) BOOL isPreviewPaused; -- (void)pausePreviewWithResult:(FlutterResult)result; -- (void)resumePreviewWithResult:(FlutterResult)result; +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; @end @interface CameraPreviewPauseTests : XCTestCase @property(readonly, nonatomic) FLTCam* camera; +@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject; @end @implementation CameraPreviewPauseTests - (void)setUp { _camera = [[FLTCam alloc] init]; + + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + _resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; } - (void)testPausePreviewWithResult_shouldPausePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera pausePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; + [_camera pausePreviewWithResult:_resultObject]; XCTAssertTrue(_camera.isPreviewPaused); } - (void)testResumePreviewWithResult_shouldResumePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera resumePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; + [_camera resumePreviewWithResult:_resultObject]; XCTAssertFalse(_camera.isPreviewPaused); } diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..e99ea3ac6f2f --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h @@ -0,0 +1,27 @@ +// +// MockFLTThreadSafeFlutterResult.h +// Runner +// +// Created by Maurits van Beusekom on 11/10/2021. +// Copyright © 2021 The Flutter Authors. All rights reserved. +// + +#ifndef MockFLTThreadSafeFlutterResult_h +#define MockFLTThreadSafeFlutterResult_h + +@interface FLTThreadSafeFlutterResult () +@property(readonly, nonatomic) FlutterResult flutterResult; +@end + +/** + * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and + * read the received result. + */ +@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult +@property(readonly, nonatomic) XCTestExpectation *expectation; +@property(nonatomic, nullable) id receivedResult; + +-(id)initWithExpectation:(XCTestExpectation *)expectation; +@end + +#endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..bb0db987b91d --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m @@ -0,0 +1,39 @@ +// Copyright 2013 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 camera; +@import XCTest; + +#import "MockFLTThreadSafeFlutterResult.h" + +@implementation MockFLTThreadSafeFlutterResult +/** + * Initializes with a notification center. + */ +- (id)initWithExpectation:(XCTestExpectation *)expectation { + self = [super init]; + _expectation = expectation; + return self; +} + +/** + * Called when result is successful. + * + * Stores the data in the `receivedResult` property and fulfills the expectation. + */ +- (void)sendSuccessWithData:(id)data { + _receivedResult = data; + [self->_expectation fulfill]; +} + +/** + * Called when result is successful. + * + * Fulfills the expectation. + */ +- (void)sendSuccess { + _receivedResult = nil; + [self->_expectation fulfill]; +} +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 4d2fd1c68053..a7f195b6696a 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1349,15 +1349,15 @@ - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - FLTThreadSafeFlutterResult *threadSafeResult = - [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; - if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); } // Invoke the plugin on another dispatch queue to avoid blocking the UI. dispatch_async(_dispatchQueue, ^{ + FLTThreadSafeFlutterResult *threadSafeResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; + [self handleMethodCallAsync:call result:threadSafeResult]; }); } @@ -1400,29 +1400,26 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call NSString *cameraName = call.arguments[@"cameraName"]; NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; NSNumber *enableAudio = call.arguments[@"enableAudio"]; - dispatch_async(_dispatchQueue, ^{ - NSError *error; - FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName - resolutionPreset:resolutionPreset - enableAudio:[enableAudio boolValue] - orientation:[[UIDevice currentDevice] orientation] - dispatchQueue:self->_dispatchQueue - error:&error]; - dispatch_async(dispatch_get_main_queue(), ^{ - if (error) { - [result sendError:error]; - } else { - if (self->_camera) { - [self->_camera close]; - } - int64_t textureId = [self->_registry registerTexture:cam]; - self->_camera = cam; - [result sendSuccessWithData:@{ - @"cameraId" : @(textureId), - }]; - } - }); - }); + NSError *error; + FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName + resolutionPreset:resolutionPreset + enableAudio:[enableAudio boolValue] + orientation:[[UIDevice currentDevice] orientation] + dispatchQueue:self->_dispatchQueue + error:&error]; + + if (error) { + [result sendError:error]; + } else { + if (self->_camera) { + [self->_camera close]; + } + int64_t textureId = [self->_registry registerTexture:cam]; + self->_camera = cam; + [result sendSuccessWithData:@{ + @"cameraId" : @(textureId), + }]; + } } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; [result sendSuccess]; From ee453bc9ea0faba0e3dbbd6b22b769af33c86006 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 07:30:08 +0200 Subject: [PATCH 25/32] Removed left over GIT merge tag --- packages/camera/camera/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 5b09eb2a73ed..d210b413a465 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -46,7 +46,6 @@ * Android Flash mode works with full precapture sequence. * Updated Android lint settings. ->>>>>>> 174f140651e9ca9c958f2ae75960684c27772a07 ## 0.8.1+7 * Fix device orientation sometimes not affecting the camera preview orientation. From cd48ddd26c819e140e22c8f20a2212b5544f0d97 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 07:56:13 +0200 Subject: [PATCH 26/32] Applied feedback on PR --- .../camera/camera/ios/Classes/CameraPlugin.m | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index fc01214d613c..61c0f32649a2 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1405,17 +1405,17 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call resolutionPreset:resolutionPreset enableAudio:[enableAudio boolValue] orientation:[[UIDevice currentDevice] orientation] - dispatchQueue:self->_dispatchQueue + dispatchQueue:_dispatchQueue error:&error]; if (error) { [result sendError:error]; } else { - if (self->_camera) { - [self->_camera close]; + if (_camera) { + [_camera close]; } - int64_t textureId = [self->_registry registerTexture:cam]; - self->_camera = cam; + int64_t textureId = [self.registry registerTexture:cam]; + _camera = cam; [result sendSuccessWithData:@{ @"cameraId" : @(textureId), }]; @@ -1456,10 +1456,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), }]; [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; - dispatch_async(_dispatchQueue, ^{ - [self->_camera start]; - [result sendSuccess]; - }); + [_camera start]; + [result sendSuccess]; } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; From a4e5262601b979b45a2057cf8d9b60c37193ea03 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 13:22:41 +0200 Subject: [PATCH 27/32] Remove development team from project file --- .../camera/example/ios/Runner.xcodeproj/project.pbxproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 98b3109892eb..feb789f2ecba 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -249,7 +249,6 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 7624MWN53C; }; }; }; @@ -434,6 +433,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -459,6 +459,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -582,7 +583,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 7624MWN53C; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -604,7 +605,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 7624MWN53C; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", From 39d14388697869e431fa412a12ca39cf7a503570 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 13:25:50 +0200 Subject: [PATCH 28/32] Remove custom setting from XCScheme --- .../xcshareddata/xcschemes/Runner.xcscheme | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 869ea983895a..1447e08231be 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,9 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - enableAddressSanitizer = "YES" - enableASanStackUseAfterReturn = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - @@ -62,7 +53,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableASanStackUseAfterReturn = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" From 3a12cbf54386137672844f8ac4abd975d110f1c2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 21:15:46 +0200 Subject: [PATCH 29/32] Clean up mock --- .../camera/example/ios/RunnerTests/CameraOrientationTests.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index 246eab90a919..ac994eaba351 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -23,6 +23,11 @@ - (void)setUp { self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger]; } +- (void)tearDown { + [self.mockMessenger stopMocking]; + self.mockMessenger = nil; +} + - (void)testOrientationNotifications { id mockMessenger = self.mockMessenger; [mockMessenger setExpectationOrderMatters:YES]; From 8e8bdfc71969bfacb0d8aeb1c743fb7af538d3f7 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Thu, 14 Oct 2021 21:29:52 +0200 Subject: [PATCH 30/32] Clean reference to CameraPlugin --- .../camera/example/ios/RunnerTests/CameraOrientationTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index ac994eaba351..f4d1bb6cef35 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -24,8 +24,8 @@ - (void)setUp { } - (void)tearDown { - [self.mockMessenger stopMocking]; self.mockMessenger = nil; + self.cameraPlugin = nil; } - (void)testOrientationNotifications { From 3e86910b04f937b4e46051987db5ff20840258df Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 18 Oct 2021 09:43:52 +0200 Subject: [PATCH 31/32] Apply feedback from PR. --- .../RunnerTests/CameraMethodChannelTests.m | 18 ++++----- .../ios/RunnerTests/CameraOrientationTests.m | 40 +++++++------------ .../ios/RunnerTests/CameraPreviewPauseTests.m | 24 +++++------ .../MockFLTThreadSafeFlutterResult.h | 9 ++++- .../MockFLTThreadSafeFlutterResult.m | 28 ++----------- .../camera/ios/Classes/CameraPlugin_Test.h | 9 +++++ .../ios/Classes/FLTThreadSafeFlutterResult.h | 4 +- 7 files changed, 56 insertions(+), 76 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 219fa9624cb3..4312b3e4690b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -10,15 +10,13 @@ #import "MockFLTThreadSafeFlutterResult.h" @interface CameraMethodChannelTests : XCTestCase -@property(readonly, nonatomic) CameraPlugin *camera; -@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject; @end @implementation CameraMethodChannelTests -- (void)setUp { - _camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; - +- (void)testCreate_ShouldCallResultOnMainThread { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; @@ -31,19 +29,17 @@ - (void)setUp { OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - _resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; -} - -- (void)testCreate_ShouldCallResultOnMainThread { + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + // Set up method call FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [self->_camera handleMethodCallAsync:call result:self->_resultObject]; + [camera handleMethodCallAsync:call result:resultObject]; // Verify the result - NSDictionary *dictionaryResult = (NSDictionary *)_resultObject.receivedResult; + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; XCTAssertNotNil(dictionaryResult); XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); } diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index f4d1bb6cef35..d16f02dce530 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -10,51 +10,41 @@ #import @interface CameraOrientationTests : XCTestCase -@property(strong, nonatomic) id mockMessenger; -@property(strong, nonatomic) CameraPlugin *cameraPlugin; @end @implementation CameraOrientationTests -- (void)setUp { - [super setUp]; - - self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger]; -} - -- (void)tearDown { - self.mockMessenger = nil; - self.cameraPlugin = nil; -} - - (void)testOrientationNotifications { - id mockMessenger = self.mockMessenger; + id mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:mockMessenger]; + [mockMessenger setExpectationOrderMatters:YES]; - [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown"]; - [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp"]; - [self rotate:UIDeviceOrientationLandscapeRight expectedChannelOrientation:@"landscapeLeft"]; - [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight"]; + [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown" cameraPlugin:cameraPlugin messenger: mockMessenger]; + [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp" cameraPlugin:cameraPlugin messenger: mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeRight expectedChannelOrientation:@"landscapeLeft" cameraPlugin:cameraPlugin messenger: mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight" cameraPlugin:cameraPlugin messenger: mockMessenger]; OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); // No notification when flat. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceUp]]; // No notification when facedown. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceDown]]; OCMVerifyAll(mockMessenger); } - (void)rotate:(UIDeviceOrientation)deviceOrientation - expectedChannelOrientation:(NSString *)channelOrientation { - id mockMessenger = self.mockMessenger; + expectedChannelOrientation:(NSString *)channelOrientation + cameraPlugin: (CameraPlugin *)cameraPlugin + messenger: (NSObject *) messenger { + XCTestExpectation *orientationExpectation = [self expectationWithDescription:channelOrientation]; - OCMExpect([mockMessenger + OCMExpect([messenger sendOnChannel:[OCMArg any] message:[OCMArg checkWithBlock:^BOOL(NSData *data) { NSObject *codec = [FlutterStandardMethodCodec sharedInstance]; @@ -65,7 +55,7 @@ - (void)rotate:(UIDeviceOrientation)deviceOrientation [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; }]]); - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; } diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m index dc5eb477226b..28389d828e71 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -12,31 +12,31 @@ @interface FLTCam : NSObject @property(assign, nonatomic) BOOL isPreviewPaused; + - (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; + - (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; @end @interface CameraPreviewPauseTests : XCTestCase -@property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) MockFLTThreadSafeFlutterResult *resultObject; @end @implementation CameraPreviewPauseTests -- (void)setUp { - _camera = [[FLTCam alloc] init]; - - _resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; -} - - (void)testPausePreviewWithResult_shouldPausePreview { - [_camera pausePreviewWithResult:_resultObject]; - XCTAssertTrue(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera pausePreviewWithResult:resultObject]; + XCTAssertTrue(camera.isPreviewPaused); } - (void)testResumePreviewWithResult_shouldResumePreview { - [_camera resumePreviewWithResult:_resultObject]; - XCTAssertFalse(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera resumePreviewWithResult:resultObject]; + XCTAssertFalse(camera.isPreviewPaused); } @end diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h index 83098415777b..72980f82bcaa 100644 --- a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h @@ -10,10 +10,15 @@ * read the received result. */ @interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult -@property(readonly, nonatomic) XCTestExpectation *_Nonnull expectation; +@property(readonly, nonatomic, nonnull) XCTestExpectation *expectation; @property(nonatomic, nullable) id receivedResult; -- (instancetype _Nonnull)initWithExpectation:(XCTestExpectation *_Nonnull)expectation; +/** + * Initializes the MockFLTThreadSafeFlutterResult with an expectation. + * + * The expectation is fullfilled when a result is called allowing tests to await the result in an asynchronous manner. + */ +- (instancetype _Nonnull) initWithExpectation:(nonnull XCTestExpectation *)expectation; @end #endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m index ac82e6feef36..da2fc2d936ba 100644 --- a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m @@ -8,40 +8,20 @@ #import "MockFLTThreadSafeFlutterResult.h" @implementation MockFLTThreadSafeFlutterResult -/** - * Initializes the MockFLTThreadSafeFlutterResult. - */ -- (instancetype)init { - self = [super init]; - return self; -} -/** - * Initializes the MockFLTThreadSafeFlutterResult with an expectation. - */ - (instancetype)initWithExpectation:(XCTestExpectation *)expectation { self = [super init]; _expectation = expectation; return self; } -/** - * Called when result is successful. - * - * Stores the data in the `receivedResult` property and fulfills the expectation. - */ - (void)sendSuccessWithData:(id)data { - _receivedResult = data; - [self->_expectation fulfill]; + self.receivedResult = data; + [self.expectation fulfill]; } -/** - * Called when result is successful. - * - * Fulfills the expectation. - */ - (void)sendSuccess { - _receivedResult = nil; - [self->_expectation fulfill]; + self.receivedResult = nil; + [self.expectation fulfill]; } @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index a79522eebeda..06b85a8a967e 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -10,12 +10,21 @@ /// Methods exposed for unit testing. @interface CameraPlugin () +/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger NS_DESIGNATED_INITIALIZER; + +/// Hide the default public constructor. - (instancetype)init NS_UNAVAILABLE; +/// Exposes the [CameraPlugin handleMethodCallAsync:result:] method for unit testing. +/// +/// This method should always be dispatched on a background queue to prevent deadlocks. + - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; + +/// Exposes the [CameraPlugin orientationChanged:] method for unit testing. - (void)orientationChanged:(NSNotification *)notification; @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 17fd8ae953d1..f290ca0fcd05 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -12,13 +12,13 @@ /** * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. */ -@property(readonly, nonatomic) FlutterResult _Nonnull flutterResult; +@property(readonly, nonatomic, nonnull) FlutterResult flutterResult; /** * Initializes with a FlutterResult object. * @param result The FlutterResult object that the result will be given to. */ -- (nonnull id)initWithResult:(nonnull FlutterResult)result; +- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result; /** * Sends a successful result without any data. From 1d51203584453d754a71d8537d011a685dd0a9aa Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 18 Oct 2021 21:57:06 +0200 Subject: [PATCH 32/32] Apply review feedback. --- .../ios/RunnerTests/MockFLTThreadSafeFlutterResult.h | 2 +- .../camera/camera/ios/Classes/CameraPlugin_Test.h | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h index 31521c971f22..8685f3fd610b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h @@ -19,7 +19,7 @@ * The expectation is fullfilled when a result is called allowing tests to await the result in an * asynchronous manner. */ -- (instancetype _Nonnull)initWithExpectation:(nonnull XCTestExpectation *)expectation; +- (nonnull instancetype)initWithExpectation:(nonnull XCTestExpectation *)expectation; @end #endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index 4faf13d8d184..afbf6864a1f8 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -18,13 +18,17 @@ /// Hide the default public constructor. - (instancetype)init NS_UNAVAILABLE; -/// Exposes the [CameraPlugin handleMethodCallAsync:result:] method for unit testing. +/// Handles `FlutterMethodCall`s and ensures result is send on the main dispatch queue. /// -/// This method should always be dispatched on a background queue to prevent deadlocks. - +/// @param call The method call command object. +/// @param result A wrapper around the `FlutterResult` callback which ensures the callback is called +/// on the main dispatch queue. - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; -/// Exposes the [CameraPlugin orientationChanged:] method for unit testing. +/// Called by the @c NSNotificationManager each time the device's orientation is changed. +/// +/// @param notification @c NSNotification instance containing a reference to the `UIDevice` object +/// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; @end