diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index dc83a4fabca2..e6495a8fc796 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+3 + +* Fix registerTexture and result being called on background thread on iOS. + ## 0.9.4+2 * Updated package description; diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 8520bb00fb2f..feb789f2ecba 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,8 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */; }; 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -16,9 +20,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,20 +48,22 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 89D82918721FABF772705DB0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -67,9 +72,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,7 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */, + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +92,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */, + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -98,16 +105,20 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, ); path = RunnerTests; sourceTree = ""; }; - 78D1009194BD06C03BED950D /* Frameworks */ = { + 3242FD2B467C15C62200632F /* Frameworks */ = { isa = PBXGroup; children = ( - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */, - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */, + 89D82918721FABF772705DB0 /* libPods-Runner.a */, + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -131,7 +142,7 @@ 03BB76692665316900CE5A93 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, FD386F00E98D73419C929072 /* Pods */, - 78D1009194BD06C03BED950D /* Frameworks */, + 3242FD2B467C15C62200632F /* Frameworks */, ); sourceTree = ""; }; @@ -171,10 +182,10 @@ FD386F00E98D73419C929072 /* Pods */ = { isa = PBXGroup; children = ( - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */, - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */, - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */, - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */, + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */, + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */, + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */, + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -186,7 +197,7 @@ isa = PBXNativeTarget; buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */, + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */, 03BB76642665316900CE5A93 /* Sources */, 03BB76652665316900CE5A93 /* Frameworks */, 03BB76662665316900CE5A93 /* Resources */, @@ -205,7 +216,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */, + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -282,7 +293,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -297,28 +322,28 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */ = { + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -333,27 +358,13 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -361,8 +372,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -409,7 +423,7 @@ /* Begin XCBuildConfiguration section */ 03BB766F2665316900CE5A93 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -419,6 +433,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -434,7 +449,7 @@ }; 03BB76702665316900CE5A93 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -444,6 +459,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -567,6 +583,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -588,6 +605,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m index 27537e7ebdac..fdc2be9901a4 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -19,7 +19,7 @@ @interface FLTCam : NSObject +#import "MockFLTThreadSafeFlutterResult.h" + +@interface CameraMethodChannelTests : XCTestCase +@end + +@implementation CameraMethodChannelTests + +- (void)testCreate_ShouldCallResultOnMainThread { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + // Set up mocks for initWithCameraName method + id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) + .andReturn([AVCaptureInput alloc]); + + id avCaptureSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); + OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + + // Set up method call + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; + + [camera handleMethodCallAsync:call result:resultObject]; + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; + XCTAssertNotNil(dictionaryResult); + XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m index 246eab90a919..efd65a47dff8 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -10,46 +10,52 @@ #import @interface CameraOrientationTests : XCTestCase -@property(strong, nonatomic) id mockMessenger; -@property(strong, nonatomic) CameraPlugin *cameraPlugin; @end @implementation CameraOrientationTests -- (void)setUp { - [super setUp]; - - self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - self.cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:self.mockMessenger]; -} - - (void)testOrientationNotifications { - id mockMessenger = self.mockMessenger; + id mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:mockMessenger]; + [mockMessenger setExpectationOrderMatters:YES]; - [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown"]; - [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp"]; - [self rotate:UIDeviceOrientationLandscapeRight expectedChannelOrientation:@"landscapeLeft"]; - [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight"]; + [self rotate:UIDeviceOrientationPortraitUpsideDown + expectedChannelOrientation:@"portraitDown" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationPortrait + expectedChannelOrientation:@"portraitUp" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeRight + expectedChannelOrientation:@"landscapeLeft" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeLeft + expectedChannelOrientation:@"landscapeRight" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); // No notification when flat. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceUp]]; // No notification when facedown. - [self.cameraPlugin + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceDown]]; OCMVerifyAll(mockMessenger); } - (void)rotate:(UIDeviceOrientation)deviceOrientation - expectedChannelOrientation:(NSString *)channelOrientation { - id mockMessenger = self.mockMessenger; + expectedChannelOrientation:(NSString *)channelOrientation + cameraPlugin:(CameraPlugin *)cameraPlugin + messenger:(NSObject *)messenger { XCTestExpectation *orientationExpectation = [self expectationWithDescription:channelOrientation]; - OCMExpect([mockMessenger + OCMExpect([messenger sendOnChannel:[OCMArg any] message:[OCMArg checkWithBlock:^BOOL(NSData *data) { NSObject *codec = [FlutterStandardMethodCodec sharedInstance]; @@ -60,8 +66,7 @@ - (void)rotate:(UIDeviceOrientation)deviceOrientation [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; }]]); - [self.cameraPlugin - orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; } diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m index 549b40a52e46..eb6c0079322c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -6,45 +6,37 @@ @import XCTest; @import AVFoundation; #import +#import "MockFLTThreadSafeFlutterResult.h" @interface FLTCam : NSObject @property(assign, nonatomic) BOOL isPreviewPaused; -- (void)pausePreviewWithResult:(FlutterResult)result; -- (void)resumePreviewWithResult:(FlutterResult)result; + +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; + +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; @end @interface CameraPreviewPauseTests : XCTestCase -@property(readonly, nonatomic) FLTCam* camera; @end @implementation CameraPreviewPauseTests -- (void)setUp { - _camera = [[FLTCam alloc] init]; -} - - (void)testPausePreviewWithResult_shouldPausePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera pausePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertTrue(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera pausePreviewWithResult:resultObject]; + XCTAssertTrue(camera.isPreviewPaused); } - (void)testResumePreviewWithResult_shouldResumePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera resumePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertFalse(_camera.isPreviewPaused); + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera resumePreviewWithResult:resultObject]; + XCTAssertFalse(camera.isPreviewPaused); } @end diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..8685f3fd610b --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef MockFLTThreadSafeFlutterResult_h +#define MockFLTThreadSafeFlutterResult_h + +/** + * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and + * read the received result. + */ +@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult +@property(readonly, nonatomic, nonnull) XCTestExpectation *expectation; +@property(nonatomic, nullable) id receivedResult; + +/** + * Initializes the MockFLTThreadSafeFlutterResult with an expectation. + * + * The expectation is fullfilled when a result is called allowing tests to await the result in an + * asynchronous manner. + */ +- (nonnull instancetype)initWithExpectation:(nonnull XCTestExpectation *)expectation; +@end + +#endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..da2fc2d936ba --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; + +#import "MockFLTThreadSafeFlutterResult.h" + +@implementation MockFLTThreadSafeFlutterResult + +- (instancetype)initWithExpectation:(XCTestExpectation *)expectation { + self = [super init]; + _expectation = expectation; + return self; +} + +- (void)sendSuccessWithData:(id)data { + self.receivedResult = data; + [self.expectation fulfill]; +} + +- (void)sendSuccess { + self.receivedResult = nil; + [self.expectation fulfill]; +} +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m new file mode 100644 index 000000000000..8cd4b8bc8c2a --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m @@ -0,0 +1,122 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; + +@interface ThreadSafeFlutterResultTests : XCTestCase +@end + +@implementation ThreadSafeFlutterResultTests +- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccess]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + [threadSafeFlutterResult sendSuccess]; + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult { + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterMethodNotImplemented.class]); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendNotImplemented]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult { + NSString* errorCode = @"errorCode"; + NSString* errorMessage = @"message"; + NSString* errorDetails = @"error details"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + XCTAssertEqualObjects(error.code, errorCode); + XCTAssertEqualObjects(error.message, errorMessage); + XCTAssertEqualObjects(error.details, errorDetails); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendNSError_ShouldSendErrorToFlutterResult { + NSError* originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil]; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError* error = (FlutterError*)result; + NSString* constructedErrorCode = + [NSString stringWithFormat:@"Error %d", (int)originalError.code]; + XCTAssertEqualObjects(error.code, constructedErrorCode); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendError:originalError]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} + +- (void)testSendResult_ShouldSendResultToFlutterResult { + NSString* resultData = @"resultData"; + XCTestExpectation* expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult* threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssertEqualObjects(result, resultData); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccessWithData:resultData]; + }); + + [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:1]; +} +@end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 78c5a34ab05a..2c12081da807 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -10,16 +10,11 @@ #import #import #import - -static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} +#import "FLTThreadSafeFlutterResult.h" @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) FlutterResult result; +@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; @end @interface FLTImageStreamHandler : NSObject @@ -45,7 +40,7 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path result:(FlutterResult)result { +- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; @@ -62,7 +57,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(10)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result sendError:error]; return; } @@ -74,10 +69,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [data writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result sendSuccessWithData:_path]; } - (void)captureOutput:(AVCapturePhotoOutput *)output @@ -85,7 +80,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output error:(NSError *)error API_AVAILABLE(ios(11.0)) { selfReference = nil; if (error) { - _result(getFlutterError(error)); + [_result sendError:error]; return; } @@ -93,10 +88,10 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output bool success = [photoData writeToFile:_path atomically:YES]; if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; return; } - _result(_path); + [_result sendSuccessWithData:_path]; } @end @@ -460,7 +455,7 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation } } -- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { +- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; if (_resolutionPreset == max) { [settings setHighResolutionPhotoEnabled:YES]; @@ -476,7 +471,7 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { prefix:@"CAP_" error:error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; return; } @@ -816,7 +811,7 @@ - (CVPixelBufferRef)copyPixelBuffer { return pixelBuffer; } -- (void)startVideoRecordingWithResult:(FlutterResult)result { +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (!_isRecording) { NSError *error; _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" @@ -824,11 +819,11 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { prefix:@"REC_" error:error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; return; } if (![self setupWriterForPath:_videoRecordingPath]) { - result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); + [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; return; } _isRecording = YES; @@ -837,13 +832,13 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result { _audioTimeOffset = CMTimeMake(0, 1); _videoIsDisconnected = NO; _audioIsDisconnected = NO; - result(nil); + [result sendSuccess]; } else { - result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]); + [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil]; } } -- (void)stopVideoRecordingWithResult:(FlutterResult)result { +- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { if (_isRecording) { _isRecording = NO; @@ -851,12 +846,12 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [_videoWriter finishWritingWithCompletionHandler:^{ if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { [self updateOrientation]; - result(self->_videoRecordingPath); + [result sendSuccessWithData:self->_videoRecordingPath]; self->_videoRecordingPath = nil; } else { - result([FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); + [result sendErrorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]; } }]; } @@ -865,29 +860,29 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorResourceUnavailable userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - result(getFlutterError(error)); + [result sendError:error]; } } -- (void)pauseVideoRecordingWithResult:(FlutterResult)result { +- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; - result(nil); + [result sendSuccess]; } -- (void)resumeVideoRecordingWithResult:(FlutterResult)result { +- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { _isRecordingPaused = NO; - result(nil); + [result sendSuccess]; } -- (void)lockCaptureOrientationWithResult:(FlutterResult)result +- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result orientation:(NSString *)orientationStr { UIDeviceOrientation orientation; @try { orientation = getUIDeviceOrientationForString(orientationStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } @@ -896,34 +891,34 @@ - (void)lockCaptureOrientationWithResult:(FlutterResult)result [self updateOrientation]; } - result(nil); + [result sendSuccess]; } -- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { +- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { _lockedCaptureOrientation = UIDeviceOrientationUnknown; [self updateOrientation]; - result(nil); + [result sendSuccess]; } -- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FlashMode mode; @try { mode = getFlashModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } if (mode == FlashModeTorch) { if (!_captureDevice.hasTorch) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support torch mode" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]; return; } if (!_captureDevice.isTorchAvailable) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Torch mode is currently not available" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOn) { @@ -933,17 +928,17 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } else { if (!_captureDevice.hasFlash) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not have flash capabilities" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]; return; } AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); if (![_capturePhotoOutput.supportedFlashModes containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support this specific flash mode" - details:nil]); + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]; return; } if (_captureDevice.torchMode != AVCaptureTorchModeOff) { @@ -953,20 +948,20 @@ - (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { } } _flashMode = mode; - result(nil); + [result sendSuccess]; } -- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { ExposureMode mode; @try { mode = getExposureModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } _exposureMode = mode; [self applyExposureMode]; - result(nil); + [result sendSuccess]; } - (void)applyExposureMode { @@ -986,17 +981,17 @@ - (void)applyExposureMode { [_captureDevice unlockForConfiguration]; } -- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { +- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FocusMode mode; @try { mode = getFocusModeForString(modeStr); } @catch (NSError *e) { - result(getFlutterError(e)); + [result sendError:e]; return; } _focusMode = mode; [self applyFocusMode]; - result(nil); + [result sendSuccess]; } - (void)applyFocusMode { @@ -1036,14 +1031,14 @@ - (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureD [captureDevice unlockForConfiguration]; } -- (void)pausePreviewWithResult:(FlutterResult)result { +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result { _isPreviewPaused = true; - result(nil); + [result sendSuccess]; } -- (void)resumePreviewWithResult:(FlutterResult)result { +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result { _isPreviewPaused = false; - result(nil); + [result sendSuccess]; } - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation @@ -1071,11 +1066,11 @@ - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation return CGPointMake(x, y); } -- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isExposurePointOfInterestSupported) { - result([FlutterError errorWithCode:@"setExposurePointFailed" - message:@"Device does not have exposure point capabilities" - details:nil]); + [result sendErrorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; @@ -1086,14 +1081,14 @@ - (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y [_captureDevice unlockForConfiguration]; // Retrigger auto exposure [self applyExposureMode]; - result(nil); + [result sendSuccess]; } -- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { +- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { if (!_captureDevice.isFocusPointOfInterestSupported) { - result([FlutterError errorWithCode:@"setFocusPointFailed" - message:@"Device does not have focus point capabilities" - details:nil]); + [result sendErrorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]; return; } UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; @@ -1105,15 +1100,14 @@ - (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { [_captureDevice unlockForConfiguration]; // Retrigger auto focus [self applyFocusMode]; - - result(nil); + [result sendSuccess]; } -- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { +- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { [_captureDevice lockForConfiguration:nil]; [_captureDevice setExposureTargetBias:offset completionHandler:nil]; [_captureDevice unlockForConfiguration]; - result(@(offset)); + [result sendSuccessWithData:@(offset)]; } - (void)startImageStreamWithMessenger:(NSObject *)messenger { @@ -1141,19 +1135,18 @@ - (void)stopImageStream { } } -- (void)getMaxZoomLevelWithResult:(FlutterResult)result { +- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; - result([NSNumber numberWithFloat:maxZoomFactor]); + [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]]; } -- (void)getMinZoomLevelWithResult:(FlutterResult)result { +- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; - - result([NSNumber numberWithFloat:minZoomFactor]); + [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]]; } -- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { +- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; @@ -1161,22 +1154,20 @@ - (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { NSString *errorMessage = [NSString stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", minAvailableZoomFactor, maxAvailableZoomFactor]; - FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" - message:errorMessage - details:nil]; - result(error); + + [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; return; } NSError *error = nil; if (![_captureDevice lockForConfiguration:&error]) { - result(getFlutterError(error)); + [result sendError:error]; return; } _captureDevice.videoZoomFactor = zoom; [_captureDevice unlockForConfiguration]; - result(nil); + [result sendSuccess]; } - (CGFloat)getMinAvailableZoomFactor { @@ -1303,6 +1294,7 @@ @interface CameraPlugin () @implementation CameraPlugin { dispatch_queue_t _dispatchQueue; } + + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera" @@ -1366,11 +1358,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // Invoke the plugin on another dispatch queue to avoid blocking the UI. dispatch_async(_dispatchQueue, ^{ - [self handleMethodCallAsync:call result:result]; + FLTThreadSafeFlutterResult *threadSafeResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; + + [self handleMethodCallAsync:call result:threadSafeResult]; }); } -- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { +- (void)handleMethodCallAsync:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { if ([@"availableCameras" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession @@ -1399,9 +1395,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re @"sensorOrientation" : @90, }]; } - result(reply); + [result sendSuccessWithData:reply]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { NSString *cameraName = call.arguments[@"cameraName"]; @@ -1416,24 +1412,23 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re error:&error]; if (error) { - result(getFlutterError(error)); + [result sendError:error]; } else { if (_camera) { [_camera close]; } - int64_t textureId = [_registry registerTexture:cam]; + int64_t textureId = [self.registry registerTexture:cam]; _camera = cam; - - result(@{ + [result sendSuccessWithData:@{ @"cameraId" : @(textureId), - }); + }]; } } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; - result(nil); + [result sendSuccess]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; - result(nil); + [result sendSuccess]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -1465,21 +1460,21 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re }]; [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; [_camera start]; - result(nil); + [result sendSuccess]; } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { [_camera captureToFile:result]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } else if ([@"dispose" isEqualToString:call.method]) { [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; - result(nil); + [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { [_camera setUpCaptureSessionForAudio]; - result(nil); + [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { @@ -1509,11 +1504,11 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } [_camera setExposurePointWithResult:result x:x y:y]; } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.minExposureTargetBias)); + [result sendSuccessWithData:@(_camera.captureDevice.minExposureTargetBias)]; } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.maxExposureTargetBias)); + [result sendSuccessWithData:@(_camera.captureDevice.maxExposureTargetBias)]; } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { - result(@(0.0)); + [result sendSuccessWithData:@(0.0)]; } else if ([@"setExposureOffset" isEqualToString:call.method]) { [_camera setExposureOffsetWithResult:result offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; @@ -1537,7 +1532,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"resumePreview" isEqualToString:call.method]) { [_camera resumePreviewWithResult:result]; } else { - result(FlutterMethodNotImplemented); + [result sendNotImplemented]; } } } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index 6952ae0a4aaa..afbf6864a1f8 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -5,15 +5,30 @@ // This header is available in the Test module. Import via "@import camera.Test;" #import +#import /// Methods exposed for unit testing. @interface CameraPlugin () +/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger NS_DESIGNATED_INITIALIZER; + +/// Hide the default public constructor. - (instancetype)init NS_UNAVAILABLE; +/// Handles `FlutterMethodCall`s and ensures result is send on the main dispatch queue. +/// +/// @param call The method call command object. +/// @param result A wrapper around the `FlutterResult` callback which ensures the callback is called +/// on the main dispatch queue. +- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; + +/// Called by the @c NSNotificationManager each time the device's orientation is changed. +/// +/// @param notification @c NSNotification instance containing a reference to the `UIDevice` object +/// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..f290ca0fcd05 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -0,0 +1,51 @@ +// 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 + +/** + * Wrapper for FlutterResult that always delivers the result on the main thread. + */ +@interface FLTThreadSafeFlutterResult : NSObject + +/** + * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. + */ +@property(readonly, nonatomic, nonnull) FlutterResult flutterResult; + +/** + * Initializes with a FlutterResult object. + * @param result The FlutterResult object that the result will be given to. + */ +- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result; + +/** + * Sends a successful result without any data. + */ +- (void)sendSuccess; + +/** + * Sends a successful result with data. + * @param data Result data that is send to the Flutter Dart side. + */ +- (void)sendSuccessWithData:(nonnull id)data; + +/** + * Sends an NSError as result + * @param error Error that will be send as FlutterError. + */ +- (void)sendError:(nonnull NSError*)error; + +/** + * Sends a FlutterError as result. + */ +- (void)sendErrorWithCode:(nonnull NSString*)code + message:(nullable NSString*)message + details:(nullable id)details; + +/** + * Sends FlutterMethodNotImplemented as result. + */ +- (void)sendNotImplemented; +@end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..caa4788d8dc8 --- /dev/null +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeFlutterResult.h" +#import + +@implementation FLTThreadSafeFlutterResult { +} + +- (id)initWithResult:(FlutterResult)result { + self = [super init]; + if (!self) { + return nil; + } + _flutterResult = result; + return self; +} + +- (void)sendSuccess { + [self send:nil]; +} + +- (void)sendSuccessWithData:(id)data { + [self send:data]; +} + +- (void)sendError:(NSError*)error { + [self sendErrorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; +} + +- (void)sendErrorWithCode:(NSString*)code + message:(NSString* _Nullable)message + details:(id _Nullable)details { + FlutterError* flutterError = [FlutterError errorWithCode:code message:message details:details]; + [self send:flutterError]; +} + +- (void)sendNotImplemented { + [self send:FlutterMethodNotImplemented]; +} + +/** + * Sends result to flutterResult on the main thread. + */ +- (void)send:(id _Nullable)result { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_flutterResult(result); + }); + } else { + _flutterResult(result); + } +} + +@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera/ios/Classes/camera-umbrella.h index 5c39401e6261..b0fd493b24df 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera/ios/Classes/camera-umbrella.h @@ -4,6 +4,7 @@ #import #import +#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[]; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f68d026b206c..6b4db7cd91ec 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/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+2 +version: 0.9.4+3 environment: sdk: ">=2.14.0 <3.0.0"