From 21a3eb62650507179f79cf89ff09127dabe7f91f Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 8 Feb 2022 15:05:28 -0800 Subject: [PATCH 01/10] [camera]remove self ref pointer for save photo delegate, and make sure thread safety" --- packages/camera/camera/CHANGELOG.md | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 14 +- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 172 ++++++++++++++++++ ...QueueTests.m => FLTCamSampleBufferTests.m} | 4 +- .../RunnerTests/FLTSavePhotoDelegateTests.m | 93 +++++----- packages/camera/camera/ios/Classes/FLTCam.m | 28 ++- .../camera/camera/ios/Classes/FLTCam_Test.h | 15 ++ .../camera/ios/Classes/FLTSavePhotoDelegate.h | 25 +-- .../camera/ios/Classes/FLTSavePhotoDelegate.m | 24 ++- .../ios/Classes/FLTSavePhotoDelegate_Test.h | 5 + 10 files changed, 303 insertions(+), 78 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m rename packages/camera/camera/example/ios/RunnerTests/{SampleBufferQueueTests.m => FLTCamSampleBufferTests.m} (93%) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d2b2f907a6fb..f14522c5ab40 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.9.4+12 +* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup. * Skips unnecessary AppDelegate setup for unit tests on iOS. * Internal code cleanup for stricter analysis options. 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..8f6f6494dbf8 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -0,0 +1,172 @@ +// 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_savePhotoDelegateReferencesMustBeAccessedOnCaptureSessionQueue { + XCTestExpectation *setReferenceExpectation = + [self expectationWithDescription: + @"FLTSavePhotoDelegate references must be set on capture session queue."]; + XCTestExpectation *clearReferenceExpectation = + [self expectationWithDescription: + @"FLTSavePhotoDelegate references must be cleared on capture session queue."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + const char *captureSessionQueueSpecific = "capture_session_queue"; + dispatch_queue_set_specific(captureSessionQueue, captureSessionQueueSpecific, + (void *)captureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + + // settings.uniqueID is used as the key for `inProgressSavePhotoDelegates` dictionary + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + // We need to make sure the delegate reference is actually saved in a real dictionary, so that we + // can call its completion handler later. Must use a partial mock, in order to forward invocation + // to the real object. + id mockDelegates = OCMPartialMock([NSMutableDictionary dictionary]); + OCMStub([mockDelegates setObject:OCMOCK_ANY forKeyedSubscript:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + if (dispatch_get_specific(captureSessionQueueSpecific)) { + FLTSavePhotoDelegate *delegate; + // Index 0 and 1 are `self` and `_cmd`. + [invocation getArgument:&delegate atIndex:2]; + if (delegate) { + [setReferenceExpectation fulfill]; + } else { + [clearReferenceExpectation fulfill]; + } + } + }) + .andForwardToRealObject(); + cam.inProgressSavePhotoDelegates = mockDelegates; + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + XCTAssertNotNil(delegate, @"Delegate reference must be saved to the dictionary."); + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + NSString *filePath = @"test"; + delegate.completionHandler(nil, filePath); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:OCMClassMock([FLTThreadSafeFlutterResult class])]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (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); + 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(error, nil); + }); + }); + 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); + 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(nil, filePath); + }); + }); + 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..cb8ae3f1742f 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:^(NSError *_Nullable error, NSString *_Nullable path) { + 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:^(NSError *_Nullable error, NSString *_Nullable path) { + 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:^(NSError *_Nullable error, NSString *_Nullable path) { + 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:^(NSError *_Nullable error, NSString *_Nullable path) { + [completionExpectation fulfill]; + }]; + [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { if (dispatch_get_specific(ioQueueSpecific)) { diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 94f985066675..d5a30561ce95 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -50,7 +50,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..9536dfbe4343 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 error photo capture error or IO error. +/// @param path the path for successfully saved photo file. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSError *_Nullable error, + NSString *_Nullable path); + /** 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 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..76ce55fcface 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 wrote 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(error, nil); 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(nil, self.path); } else { - [self.result sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]; + self.completionHandler(ioError, nil); } }); } 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. From 8cc0bce108a96c5f032017924b4641cfcf283dc3 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 11 Feb 2022 11:07:24 -0800 Subject: [PATCH 02/10] remove queue test --- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index 8f6f6494dbf8..85e92453cab7 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -14,67 +14,6 @@ @interface FLTCamPhotoCaptureTests : XCTestCase @implementation FLTCamPhotoCaptureTests -- (void)testCaptureToFile_savePhotoDelegateReferencesMustBeAccessedOnCaptureSessionQueue { - XCTestExpectation *setReferenceExpectation = - [self expectationWithDescription: - @"FLTSavePhotoDelegate references must be set on capture session queue."]; - XCTestExpectation *clearReferenceExpectation = - [self expectationWithDescription: - @"FLTSavePhotoDelegate references must be cleared on capture session queue."]; - - dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); - const char *captureSessionQueueSpecific = "capture_session_queue"; - dispatch_queue_set_specific(captureSessionQueue, captureSessionQueueSpecific, - (void *)captureSessionQueueSpecific, NULL); - FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; - - // settings.uniqueID is used as the key for `inProgressSavePhotoDelegates` dictionary - AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; - id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); - OCMStub([mockSettings photoSettings]).andReturn(settings); - - // We need to make sure the delegate reference is actually saved in a real dictionary, so that we - // can call its completion handler later. Must use a partial mock, in order to forward invocation - // to the real object. - id mockDelegates = OCMPartialMock([NSMutableDictionary dictionary]); - OCMStub([mockDelegates setObject:OCMOCK_ANY forKeyedSubscript:OCMOCK_ANY]) - .andDo(^(NSInvocation *invocation) { - if (dispatch_get_specific(captureSessionQueueSpecific)) { - FLTSavePhotoDelegate *delegate; - // Index 0 and 1 are `self` and `_cmd`. - [invocation getArgument:&delegate atIndex:2]; - if (delegate) { - [setReferenceExpectation fulfill]; - } else { - [clearReferenceExpectation fulfill]; - } - } - }) - .andForwardToRealObject(); - cam.inProgressSavePhotoDelegates = mockDelegates; - - id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); - OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) - .andDo(^(NSInvocation *invocation) { - FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; - XCTAssertNotNil(delegate, @"Delegate reference must be saved to the dictionary."); - // Completion runs on IO queue. - dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); - dispatch_async(ioQueue, ^{ - NSString *filePath = @"test"; - delegate.completionHandler(nil, filePath); - }); - }); - cam.capturePhotoOutput = mockOutput; - - // `FLTCam::captureToFile` runs on capture session queue. - dispatch_async(captureSessionQueue, ^{ - [cam captureToFile:OCMClassMock([FLTThreadSafeFlutterResult class])]; - }); - - [self waitForExpectationsWithTimeout:1 handler:nil]; -} - - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError { XCTestExpectation *errorExpectation = [self expectationWithDescription: From 384b4cb4e3ad117c71b9e3f4fb6cef16971fe0f5 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 11 Feb 2022 11:09:09 -0800 Subject: [PATCH 03/10] swap path error order --- .../example/ios/RunnerTests/FLTCamPhotoCaptureTests.m | 4 ++-- .../example/ios/RunnerTests/FLTSavePhotoDelegateTests.m | 8 ++++---- packages/camera/camera/ios/Classes/FLTCam.m | 7 ++++--- packages/camera/camera/ios/Classes/FLTCam_Test.h | 4 +--- packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h | 8 ++++---- packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m | 8 ++++---- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index 85e92453cab7..03c68fb514a2 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -39,7 +39,7 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW // Completion runs on IO queue. dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); dispatch_async(ioQueue, ^{ - delegate.completionHandler(error, nil); + delegate.completionHandler(nil, error); }); }); cam.capturePhotoOutput = mockOutput; @@ -77,7 +77,7 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi // Completion runs on IO queue. dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); dispatch_async(ioQueue, ^{ - delegate.completionHandler(nil, filePath); + delegate.completionHandler(filePath, nil); }); }); cam.capturePhotoOutput = mockOutput; diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index cb8ae3f1742f..9e8e2441f0b9 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -23,7 +23,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" ioQueue:ioQueue - completionHandler:^(NSError *_Nullable error, NSString *_Nullable path) { + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { XCTAssertEqualObjects(captureError, error); XCTAssertNil(path); [completionExpectation fulfill]; @@ -47,7 +47,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" ioQueue:ioQueue - completionHandler:^(NSError *_Nullable error, NSString *_Nullable path) { + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { XCTAssertEqualObjects(ioError, error); XCTAssertNil(path); [completionExpectation fulfill]; @@ -76,7 +76,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:filePath ioQueue:ioQueue - completionHandler:^(NSError *_Nullable error, NSString *_Nullable path) { + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { XCTAssertNil(error); XCTAssertEqualObjects(filePath, path); [completionExpectation fulfill]; @@ -122,7 +122,7 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:filePath ioQueue:ioQueue - completionHandler:^(NSError *_Nullable error, NSString *_Nullable path) { + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { [completionExpectation fulfill]; }]; diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index d5a30561ce95..b85d2e2af017 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -75,8 +75,8 @@ @interface FLTCam () *inProgressSavePhotoDelegates; @end diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h index 9536dfbe4343..40e4562e4483 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h @@ -13,10 +13,10 @@ NS_ASSUME_NONNULL_BEGIN /// 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 error photo capture error or IO error. /// @param path the path for successfully saved photo file. -typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSError *_Nullable error, - NSString *_Nullable path); +/// @param error photo capture error or IO error. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSString *_Nullable path, + NSError *_Nullable error); /** Delegate object that handles photo capture results. @@ -26,7 +26,7 @@ typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSError *_Nullable error, /** * Initialize a photo capture delegate. * @param path the path for captured photo file. - * @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. */ diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index 76ce55fcface..ced3cb5e407f 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -8,7 +8,7 @@ @interface FLTSavePhotoDelegate () /// The file path for the captured photo. @property(readonly, nonatomic) NSString *path; -/// The queue on which captured photos are wrote to disk. +/// The queue on which captured photos are written to disk. @property(readonly, nonatomic) dispatch_queue_t ioQueue; @end @@ -28,16 +28,16 @@ - (instancetype)initWithPath:(NSString *)path - (void)handlePhotoCaptureResultWithError:(NSError *)error photoDataProvider:(NSData * (^)(void))photoDataProvider { if (error) { - self.completionHandler(error, nil); + self.completionHandler(nil, error); return; } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); NSError *ioError; if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { - self.completionHandler(nil, self.path); + self.completionHandler(self.path, nil); } else { - self.completionHandler(ioError, nil); + self.completionHandler(nil, ioError); } }); } From 3e9a5bb696ae5d9a3a4e2af3a0fae4fa52e1a408 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 11 Feb 2022 11:59:32 -0800 Subject: [PATCH 04/10] add asserts capture queue specific --- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 3 ++- .../ios/RunnerTests/QueueHelperTests.m | 15 +++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 3 +++ packages/camera/camera/ios/Classes/FLTCam.m | 4 +++- .../camera/camera/ios/Classes/QueueHelper.h | 19 +++++++++++++++++++ .../camera/camera/ios/Classes/QueueHelper.m | 12 ++++++++++++ 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index 03c68fb514a2..9fcb69362955 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -20,8 +20,8 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW @"Must send error to result if save photo delegate completes with error."]; dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:captureSessionQueue]; FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; - AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); OCMStub([mockSettings photoSettings]).andReturn(settings); @@ -58,6 +58,7 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi @"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); + [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:captureSessionQueue]; FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m index c5f377f7efa9..7078103bd12c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m @@ -35,4 +35,19 @@ - (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testSetAndCheckQueueSpecific { + XCTestExpectation *expectation = [self expectationWithDescription:@"Complete test"]; + const char *specific = "specific"; + dispatch_queue_t queue = dispatch_queue_create("test", NULL); + [QueueHelper setSpecific: specific forQueue:queue]; + + XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], @"Must not be on the test queue before dispatched to it."); + dispatch_async(queue, ^{ + XCTAssert([QueueHelper isCurrentlyOnQueueWithSpecific:specific], @"Must be on the test queue after dispatched to it."); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 97e995429e14..1b58cf9d7128 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,8 @@ - (instancetype)initWithRegistry:(NSObject *)registry _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:_captureSessionQueue]; + [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 b85d2e2af017..a93846b1fa11 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 @@ -201,6 +202,7 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation } - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; if (_resolutionPreset == FLTResolutionPresetMax) { [settings setHighResolutionPhotoEnabled:YES]; @@ -239,7 +241,7 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) } }]; - // Already on capture session queue. + NSAssert([QueueHelper isCurrentlyOnQueueWithSpecific:FLTCaptureSessionQueueSpecific], @"save photo delegate references must be updated on the capture session queue"); self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate; [self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate]; } diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index c2548148c499..f7410e8d039f 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -6,8 +6,27 @@ 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 + +/// Ensure 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 dispatch asynchronously to the main queue. +/// @param block the block to be run on the main queue. + (void)ensureToRunOnMainQueue:(void (^)(void))block; + + +/// Sets the queue-specific context data for a given queue. +/// @param specific the queue-specific context data. +/// @param queue the queue to be associated with the context data. ++ (void)setSpecific: (const char *)specific forQueue: (dispatch_queue_t) queue; + +/// Check if the caller is on a certain queue specified by its queue-specifc context data. +/// @returns true if the queue is on a certain queue specified by its queue-specific context data. false otherwise. ++ (BOOL)isCurrentlyOnQueueWithSpecific: (const char *)specific; + @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..7c09d0234e9c 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,13 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { block(); } } + ++ (void)setSpecific: (const char *)specific forQueue: (dispatch_queue_t) queue { + dispatch_queue_set_specific(queue, specific, (void *)specific, NULL); +} + ++ (BOOL)isCurrentlyOnQueueWithSpecific: (const char *)specific { + return dispatch_get_specific(specific); +} + @end From 3c6dc574d60e56520ac7697dfe1c35e042fec495 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 11 Feb 2022 11:59:56 -0800 Subject: [PATCH 05/10] format format --- .../ios/RunnerTests/QueueHelperTests.m | 23 +++++++++++-------- .../camera/camera/ios/Classes/CameraPlugin.m | 2 +- packages/camera/camera/ios/Classes/FLTCam.m | 7 +++--- .../camera/camera/ios/Classes/QueueHelper.h | 11 +++++---- .../camera/camera/ios/Classes/QueueHelper.m | 4 ++-- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m index 7078103bd12c..40e4737bcd24 100644 --- a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m @@ -36,18 +36,21 @@ - (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue { } - (void)testSetAndCheckQueueSpecific { - XCTestExpectation *expectation = [self expectationWithDescription:@"Complete test"]; - const char *specific = "specific"; dispatch_queue_t queue = dispatch_queue_create("test", NULL); - [QueueHelper setSpecific: specific forQueue:queue]; - - XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], @"Must not be on the test queue before dispatched to it."); - dispatch_async(queue, ^{ - XCTAssert([QueueHelper isCurrentlyOnQueueWithSpecific:specific], @"Must be on the test queue after dispatched to it."); - [expectation fulfill]; + const char *specific = "specific"; + [QueueHelper setSpecific:specific forQueue:queue]; + + XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], + @"Must not be on the test queue before dispatching to it."); + + // Note: sync call + dispatch_sync(queue, ^{ + XCTAssert([QueueHelper isCurrentlyOnQueueWithSpecific:specific], + @"Must be on the test queue after dispatching to it."); }); - - [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], + @"Must not be on the test queue outside of dispatch_sync block."); } @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 1b58cf9d7128..10d2af9bbb5f 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -40,7 +40,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry _messenger = messenger; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:_captureSessionQueue]; - + [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 a93846b1fa11..639df41e2eed 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -202,7 +202,6 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation } - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { - AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; if (_resolutionPreset == FLTResolutionPresetMax) { [settings setHighResolutionPhotoEnabled:YES]; @@ -229,7 +228,8 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) dispatch_async(self.captureSessionQueue, ^{ // Dispatch back to capture session queue to delete reference. // Retain cycle is broken after the dictionary entry is cleared. - // Retain cycle is to keep the original behavior with our previous `selfReference` approach in the FLTSavePhotoDelegate, where delegate is released only after capture completion. + // This is to keep the behavior with the previous `selfReference` approach in the + // FLTSavePhotoDelegate, where delegate is released only after capture completion. self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = nil; }); @@ -241,7 +241,8 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) } }]; - NSAssert([QueueHelper isCurrentlyOnQueueWithSpecific:FLTCaptureSessionQueueSpecific], @"save photo delegate references must be updated on the capture session queue"); + NSAssert([QueueHelper isCurrentlyOnQueueWithSpecific:FLTCaptureSessionQueueSpecific], + @"save photo delegate references must be updated on the capture session queue"); self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate; [self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate]; } diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index f7410e8d039f..751905ab325e 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -13,19 +13,20 @@ extern const char *FLTCaptureSessionQueueSpecific; @interface QueueHelper : NSObject /// Ensure 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 dispatch asynchronously to the main queue. +/// If caller site is already on the main queue, the block will be run synchronously. Otherwise, the +/// block will be dispatch asynchronously to the main queue. /// @param block the block to be run on the main queue. + (void)ensureToRunOnMainQueue:(void (^)(void))block; - /// Sets the queue-specific context data for a given queue. /// @param specific the queue-specific context data. /// @param queue the queue to be associated with the context data. -+ (void)setSpecific: (const char *)specific forQueue: (dispatch_queue_t) queue; ++ (void)setSpecific:(const char *)specific forQueue:(dispatch_queue_t)queue; /// Check if the caller is on a certain queue specified by its queue-specifc context data. -/// @returns true if the queue is on a certain queue specified by its queue-specific context data. false otherwise. -+ (BOOL)isCurrentlyOnQueueWithSpecific: (const char *)specific; +/// @returns YES if the caller is on a certain queue specified by its queue-specific context data. +/// NO otherwise. ++ (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific; @end diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m index 7c09d0234e9c..d6f73f0a12ed 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m @@ -16,11 +16,11 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { } } -+ (void)setSpecific: (const char *)specific forQueue: (dispatch_queue_t) queue { ++ (void)setSpecific:(const char *)specific forQueue:(dispatch_queue_t)queue { dispatch_queue_set_specific(queue, specific, (void *)specific, NULL); } -+ (BOOL)isCurrentlyOnQueueWithSpecific: (const char *)specific { ++ (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific { return dispatch_get_specific(specific); } From 04de9947cab546f6d2782798721bb478328477ce Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 11 Feb 2022 12:34:30 -0800 Subject: [PATCH 06/10] create queue helper --- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 10 ++++++---- .../example/ios/RunnerTests/QueueHelperTests.m | 13 ++++++++++--- .../camera/camera/ios/Classes/CameraPlugin.m | 4 ++-- .../camera/camera/ios/Classes/QueueHelper.h | 18 ++++++++++-------- .../camera/camera/ios/Classes/QueueHelper.m | 4 +++- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index 9fcb69362955..26e8a0516b38 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -19,8 +19,9 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW [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); - [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:captureSessionQueue]; + dispatch_queue_t captureSessionQueue = + [QueueHelper createQueueWithLabel:"capture_session_queue" + specific:FLTCaptureSessionQueueSpecific]; FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); @@ -57,8 +58,9 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi [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); - [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:captureSessionQueue]; + dispatch_queue_t captureSessionQueue = + [QueueHelper createQueueWithLabel:"capture_session_queue" + specific:FLTCaptureSessionQueueSpecific]; FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m index 40e4737bcd24..1c894e454082 100644 --- a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m @@ -35,11 +35,18 @@ - (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue { [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testSetAndCheckQueueSpecific { - dispatch_queue_t queue = dispatch_queue_create("test", NULL); +- (void)testCreateQueue { + const char *label = "label"; const char *specific = "specific"; - [QueueHelper setSpecific:specific forQueue:queue]; + dispatch_queue_t queue = [QueueHelper createQueueWithLabel:label specific:specific]; + XCTAssert(0 == strcmp(label, dispatch_queue_get_label(queue)), "Must set the correct label."); + XCTAssert(0 == strcmp(specific, dispatch_queue_get_specific(queue, specific)), + "Must set the correct specific."); +} +- (void)testIsCurrentlyOnQueueWithSpecific { + const char *specific = "specific"; + dispatch_queue_t queue = [QueueHelper createQueueWithLabel:"test" specific:specific]; XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], @"Must not be on the test queue before dispatching to it."); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 10d2af9bbb5f..9b629679567f 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -38,8 +38,8 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; - _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); - [QueueHelper setSpecific:FLTCaptureSessionQueueSpecific forQueue:_captureSessionQueue]; + _captureSessionQueue = [QueueHelper createQueueWithLabel:"io.flutter.camera.captureSessionQueue" + specific:FLTCaptureSessionQueueSpecific]; [self initDeviceEventMethodChannel]; [self startOrientationListener]; diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index 751905ab325e..42bc0d71ef1c 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -12,19 +12,21 @@ extern const char *FLTCaptureSessionQueueSpecific; /// A class that contains dispatch queue related helper functions. @interface QueueHelper : NSObject -/// Ensure the given block to be run on the main queue. +/// 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 dispatch asynchronously to the main queue. +/// 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; -/// Sets the queue-specific context data for a given queue. -/// @param specific the queue-specific context data. -/// @param queue the queue to be associated with the context data. -+ (void)setSpecific:(const char *)specific forQueue:(dispatch_queue_t)queue; +/// Creates a dispatch queue with a label and queue-specific context data. +/// @param label label for debugging purpose. +/// @param specific queue-specific context data. +/// @return the created dispatch queue with the given label and queue-specific context data. ++ (dispatch_queue_t)createQueueWithLabel:(const char *)label specific:(const char *)specific; -/// Check if the caller is on a certain queue specified by its queue-specifc context data. -/// @returns YES if the caller is on a certain queue specified by its queue-specific context data. +/// Checks if the caller is on a certain queue specified by its queue-specifc context data. +/// @param specific queue-specific context data. +/// @return YES if the caller is on a certain queue specified by its queue-specific context data; /// NO otherwise. + (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific; diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m index d6f73f0a12ed..673ad0cfba8b 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m @@ -16,8 +16,10 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { } } -+ (void)setSpecific:(const char *)specific forQueue:(dispatch_queue_t)queue { ++ (dispatch_queue_t)createQueueWithLabel:(const char *)label specific:(const char *)specific { + dispatch_queue_t queue = dispatch_queue_create(label, NULL); dispatch_queue_set_specific(queue, specific, (void *)specific, NULL); + return queue; } + (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific { From e561222f7ca88c0501e11cec661b701153e16d90 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 15 Feb 2022 21:05:56 -0800 Subject: [PATCH 07/10] remove queue helper functions that sets and getes queue specific data --- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 12 ++++----- .../ios/RunnerTests/QueueHelperTests.m | 25 ------------------- .../camera/camera/ios/Classes/CameraPlugin.m | 5 ++-- packages/camera/camera/ios/Classes/FLTCam.m | 2 +- .../camera/camera/ios/Classes/QueueHelper.h | 12 --------- .../camera/camera/ios/Classes/QueueHelper.m | 10 -------- 6 files changed, 10 insertions(+), 56 deletions(-) diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index 26e8a0516b38..fdb2abd4933e 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -19,9 +19,9 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW [self expectationWithDescription: @"Must send error to result if save photo delegate completes with error."]; - dispatch_queue_t captureSessionQueue = - [QueueHelper createQueueWithLabel:"capture_session_queue" - specific:FLTCaptureSessionQueueSpecific]; + 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]); @@ -58,9 +58,9 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi [self expectationWithDescription: @"Must send file path to result if save photo delegate completes with file path."]; - dispatch_queue_t captureSessionQueue = - [QueueHelper createQueueWithLabel:"capture_session_queue" - specific:FLTCaptureSessionQueueSpecific]; + 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]; diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m index 1c894e454082..c5f377f7efa9 100644 --- a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m @@ -35,29 +35,4 @@ - (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue { [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testCreateQueue { - const char *label = "label"; - const char *specific = "specific"; - dispatch_queue_t queue = [QueueHelper createQueueWithLabel:label specific:specific]; - XCTAssert(0 == strcmp(label, dispatch_queue_get_label(queue)), "Must set the correct label."); - XCTAssert(0 == strcmp(specific, dispatch_queue_get_specific(queue, specific)), - "Must set the correct specific."); -} - -- (void)testIsCurrentlyOnQueueWithSpecific { - const char *specific = "specific"; - dispatch_queue_t queue = [QueueHelper createQueueWithLabel:"test" specific:specific]; - XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], - @"Must not be on the test queue before dispatching to it."); - - // Note: sync call - dispatch_sync(queue, ^{ - XCTAssert([QueueHelper isCurrentlyOnQueueWithSpecific:specific], - @"Must be on the test queue after dispatching to it."); - }); - - XCTAssertFalse([QueueHelper isCurrentlyOnQueueWithSpecific:specific], - @"Must not be on the test queue outside of dispatch_sync block."); -} - @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 9b629679567f..14d94d0760cb 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -38,8 +38,9 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; - _captureSessionQueue = [QueueHelper createQueueWithLabel:"io.flutter.camera.captureSessionQueue" - specific:FLTCaptureSessionQueueSpecific]; + _captureSessionQueue = dispatch_queue_create(io.flutter.camera.captureSessionQueue, NULL); + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); [self initDeviceEventMethodChannel]; [self startOrientationListener]; diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 639df41e2eed..7d637a2d1c01 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -241,7 +241,7 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) } }]; - NSAssert([QueueHelper isCurrentlyOnQueueWithSpecific:FLTCaptureSessionQueueSpecific], + NSAssert(dispatch_get_specific(FLTCaptureSessionQueueSpecific), @"save photo delegate references must be updated on the capture session queue"); self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate; [self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate]; diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index 42bc0d71ef1c..dc7373220521 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -18,18 +18,6 @@ extern const char *FLTCaptureSessionQueueSpecific; /// @param block the block to be run on the main queue. + (void)ensureToRunOnMainQueue:(void (^)(void))block; -/// Creates a dispatch queue with a label and queue-specific context data. -/// @param label label for debugging purpose. -/// @param specific queue-specific context data. -/// @return the created dispatch queue with the given label and queue-specific context data. -+ (dispatch_queue_t)createQueueWithLabel:(const char *)label specific:(const char *)specific; - -/// Checks if the caller is on a certain queue specified by its queue-specifc context data. -/// @param specific queue-specific context data. -/// @return YES if the caller is on a certain queue specified by its queue-specific context data; -/// NO otherwise. -+ (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific; - @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m index 673ad0cfba8b..2cef7b677bfa 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m @@ -16,14 +16,4 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { } } -+ (dispatch_queue_t)createQueueWithLabel:(const char *)label specific:(const char *)specific { - dispatch_queue_t queue = dispatch_queue_create(label, NULL); - dispatch_queue_set_specific(queue, specific, (void *)specific, NULL); - return queue; -} - -+ (BOOL)isCurrentlyOnQueueWithSpecific:(const char *)specific { - return dispatch_get_specific(specific); -} - @end From 9ca4084595f5bb5ce23b0204fa9641b37566ebf9 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Wed, 16 Feb 2022 12:17:58 -0800 Subject: [PATCH 08/10] Update packages/camera/camera/ios/Classes/FLTCam.m Co-authored-by: Jenn Magder --- packages/camera/camera/ios/Classes/FLTCam.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 7d637a2d1c01..31a9decd59b9 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -230,7 +230,7 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) // Retain cycle is broken after the dictionary entry is cleared. // This is to keep the behavior with the previous `selfReference` approach in the // FLTSavePhotoDelegate, where delegate is released only after capture completion. - self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = nil; + [self.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)]; }); if (error) { From 99a0b87ab5cf23bd682f56e12474cfcce220cf6a Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 16 Feb 2022 13:33:11 -0800 Subject: [PATCH 09/10] fix compiler error --- packages/camera/camera/ios/Classes/CameraPlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 14d94d0760cb..fcea190de705 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -38,7 +38,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; - _captureSessionQueue = dispatch_queue_create(io.flutter.camera.captureSessionQueue, NULL); + _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); From 2d4a4ab1ac7a33483c65761812a61bffd15e60f0 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 16 Feb 2022 15:26:59 -0800 Subject: [PATCH 10/10] bump version --- packages/camera/camera/CHANGELOG.md | 5 ++++- packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index f14522c5ab40..0089130b5e90 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,6 +1,9 @@ -## 0.9.4+12 +## 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. * Internal code cleanup for stricter analysis options. 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"