diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d2b2f907a6fb..0089130b5e90 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+13 + +* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup. + ## 0.9.4+12 * Skips unnecessary AppDelegate setup for unit tests on iOS. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index ac39de2e3f84..5f788fc9b9f9 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -23,11 +23,12 @@ E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; }; E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; }; E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */; }; 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 */ @@ -85,11 +86,12 @@ E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = ""; }; E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = ""; }; - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleBufferQueueTests.m; 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 = ""; }; @@ -126,8 +128,9 @@ E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */, E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, @@ -396,12 +399,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */, 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m new file mode 100644 index 000000000000..fdb2abd4933e --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -0,0 +1,114 @@ +// 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 camera.Test; +@import AVFoundation; +@import XCTest; +#import + +@interface FLTCamPhotoCaptureTests : XCTestCase + +@end + +@implementation FLTCamPhotoCaptureTests + +- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError { + XCTestExpectation *errorExpectation = + [self expectationWithDescription: + @"Must send error to result if save photo delegate completes with error."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) { + [errorExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(nil, error); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath { + XCTestExpectation *pathExpectation = + [self expectationWithDescription: + @"Must send file path to result if save photo delegate completes with file path."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSString *filePath = @"test"; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) { + [pathExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(filePath, nil); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/// Creates an `FLTCam` that runs its operations on a given capture session queue. +- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { + id inputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) + .andReturn(inputMock); + + id sessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([sessionMock alloc]).andReturn(sessionMock); + OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + return [[FLTCam alloc] initWithCameraName:@"camera" + resolutionPreset:@"medium" + enableAudio:true + orientation:UIDeviceOrientationPortrait + captureSessionQueue:captureSessionQueue + error:nil]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m similarity index 93% rename from packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m rename to packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m index 19cead905f61..ccc8de5b23bd 100644 --- a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m @@ -8,11 +8,11 @@ @import XCTest; #import -@interface SampleBufferQueueTests : XCTestCase +@interface FLTCamSampleBufferTests : XCTestCase @end -@implementation SampleBufferQueueTests +@implementation FLTCamSampleBufferTests - (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { id inputMock = OCMClassMock([AVCaptureDeviceInput class]); diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index b6ea84da449c..9e8e2441f0b9 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -14,40 +14,44 @@ @interface FLTSavePhotoDelegateTests : XCTestCase @implementation FLTSavePhotoDelegateTests -- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture { - NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to capture photo."]; - [delegate handlePhotoCaptureResultWithError:error + NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(captureError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; + + [delegate handlePhotoCaptureResultWithError:captureError photoDataProvider:^NSData * { return nil; }]; - OCMVerify([mockResult sendError:error]); + [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); NSError *ioError = [NSError errorWithDomain:@"IOError" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; - - OCMStub([mockResult sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]) - .andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(ioError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. @@ -63,23 +67,25 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { - XCTestExpectation *resultExpectation = [self - expectationWithDescription:@"Must send file path to the result if success to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; - OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(filePath, path); + [completionExpectation fulfill]; + }]; // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) + OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) .andReturn(YES); [delegate handlePhotoCaptureResultWithError:nil @@ -94,16 +100,12 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue [self expectationWithDescription:@"Data provider must run on io queue."]; XCTestExpectation *writeFileQueueExpectation = [self expectationWithDescription:@"File writing must run on io queue"]; - XCTestExpectation *resultExpectation = [self - expectationWithDescription:@"Must send file path to the result if success to write file."]; + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); const char *ioQueueSpecific = "io_queue_specific"; dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. @@ -116,9 +118,14 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue }) .andReturn(YES); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + [completionExpectation fulfill]; + }]; + [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { if (dispatch_get_specific(ioQueueSpecific)) { diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 97e995429e14..fcea190de705 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -13,6 +13,7 @@ #import "FLTThreadSafeFlutterResult.h" #import "FLTThreadSafeMethodChannel.h" #import "FLTThreadSafeTextureRegistry.h" +#import "QueueHelper.h" @interface CameraPlugin () @property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @@ -38,6 +39,9 @@ - (instancetype)initWithRegistry:(NSObject *)registry _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + [self initDeviceEventMethodChannel]; [self startOrientationListener]; return self; diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 94f985066675..31a9decd59b9 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -5,6 +5,7 @@ #import "FLTCam.h" #import "FLTCam_Test.h" #import "FLTSavePhotoDelegate.h" +#import "QueueHelper.h" @import CoreMotion; #import @@ -50,7 +51,6 @@ @interface FLTCam () *inProgressSavePhotoDelegates; + @end diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h index a773b4613931..40e4562e4483 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h @@ -4,34 +4,35 @@ @import AVFoundation; @import Foundation; -@import Flutter; #import "FLTThreadSafeFlutterResult.h" NS_ASSUME_NONNULL_BEGIN +/// The completion handler block for save photo operations. +/// Can be called from either main queue or IO queue. +/// If success, `error` will be present and `path` will be nil. Otherewise, `error` will be nil and +/// `path` will be present. +/// @param path the path for successfully saved photo file. +/// @param error photo capture error or IO error. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSString *_Nullable path, + NSError *_Nullable error); + /** Delegate object that handles photo capture results. */ @interface FLTSavePhotoDelegate : NSObject -/// The file path for the captured photo. -@property(readonly, nonatomic) NSString *path; -/// The thread safe flutter result wrapper to report the result. -@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; -/// The queue on which captured photos are wrote to disk. -@property(strong, nonatomic) dispatch_queue_t ioQueue; -/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. -@property(strong, nonatomic, nullable) FLTSavePhotoDelegate *selfReference; /** * Initialize a photo capture delegate. * @param path the path for captured photo file. - * @param result the thread safe flutter result wrapper to report the result. - * @param ioQueue the queue on which captured photos are wrote to disk. + * @param ioQueue the queue on which captured photos are written to disk. + * @param completionHandler The completion handler block for save photo operations. Can + * be called from either main queue or IO queue. */ - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue; + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler; @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index 8dadfec7fecd..ced3cb5e407f 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -3,37 +3,41 @@ // found in the LICENSE file. #import "FLTSavePhotoDelegate.h" +#import "FLTSavePhotoDelegate_Test.h" + +@interface FLTSavePhotoDelegate () +/// The file path for the captured photo. +@property(readonly, nonatomic) NSString *path; +/// The queue on which captured photos are written to disk. +@property(readonly, nonatomic) dispatch_queue_t ioQueue; +@end @implementation FLTSavePhotoDelegate - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue { + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _selfReference = self; - _result = result; _ioQueue = ioQueue; + _completionHandler = completionHandler; return self; } - (void)handlePhotoCaptureResultWithError:(NSError *)error photoDataProvider:(NSData * (^)(void))photoDataProvider { - self.selfReference = nil; if (error) { - [self.result sendError:error]; + self.completionHandler(nil, error); return; } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); NSError *ioError; if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { - [self.result sendSuccessWithData:self.path]; + self.completionHandler(self.path, nil); } else { - [self.result sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]; + self.completionHandler(nil, ioError); } }); } diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h index c0b77c7bac83..2d0d4f96be9d 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h @@ -9,6 +9,11 @@ */ @interface FLTSavePhotoDelegate () +/// The completion handler block for capture and save photo operations. +/// Can be called from either main queue or IO queue. +/// Exposed for unit tests to manually trigger the completion. +@property(readonly, nonatomic) FLTSavePhotoDelegateCompletionHandler completionHandler; + /// Handler to write captured photo data into a file. /// @param error the capture error. /// @param photoDataProvider a closure that provides photo data. diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index c2548148c499..dc7373220521 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -6,8 +6,18 @@ NS_ASSUME_NONNULL_BEGIN +/// Queue-specific context data to be associated with the capture session queue. +extern const char *FLTCaptureSessionQueueSpecific; + +/// A class that contains dispatch queue related helper functions. @interface QueueHelper : NSObject + +/// Ensures the given block to be run on the main queue. +/// If caller site is already on the main queue, the block will be run synchronously. Otherwise, the +/// block will be dispatched asynchronously to the main queue. +/// @param block the block to be run on the main queue. + (void)ensureToRunOnMainQueue:(void (^)(void))block; + @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m index 194dfa9cfb95..2cef7b677bfa 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m @@ -4,7 +4,10 @@ #import "QueueHelper.h" +const char *FLTCaptureSessionQueueSpecific = "capture_session_queue"; + @implementation QueueHelper + + (void)ensureToRunOnMainQueue:(void (^)(void))block { if (!NSThread.isMainThread) { dispatch_async(dispatch_get_main_queue(), block); @@ -12,4 +15,5 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { block(); } } + @end diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7421a7738ddd..80d0393087d8 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+12 +version: 0.9.4+13 environment: sdk: ">=2.14.0 <3.0.0"