diff --git a/ios/Podfile b/ios/Podfile index 4ca6f7005..1fbaaa637 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -29,7 +29,7 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d3216ce7f..b531c8ee5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -77,7 +77,7 @@ PODS: - firebase_core (3.13.0): - Firebase/CoreOnly (= 11.10.0) - Flutter - - FirebaseAppCheckInterop (11.12.0) + - FirebaseAppCheckInterop (11.13.0) - FirebaseAuth (11.10.0): - FirebaseAppCheckInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0) @@ -87,7 +87,7 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GTMSessionFetcher/Core (< 5.0, >= 3.4) - RecaptchaInterop (~> 101.0) - - FirebaseAuthInterop (11.12.0) + - FirebaseAuthInterop (11.13.0) - FirebaseCore (11.10.0): - FirebaseCoreInternal (~> 11.10.0) - GoogleUtilities/Environment (~> 8.0) @@ -380,8 +380,8 @@ SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf - azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd - ble_service: 7987f876505b5c2429303597d7604092d2545566 + azure_speech: 11927e53ab90a2def1370da1d44e4915a97c0290 + ble_service: 246764cd69adead824a6596fc2fa9e103dc1d2a4 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 @@ -395,9 +395,9 @@ SPEC CHECKSUMS: Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5 firebase_core: 2d4534e7b489907dcede540c835b48981d890943 - FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852 + FirebaseAppCheckInterop: 72066489c209823649a997132bcd9269bc33a4bb FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 - FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595 + FirebaseAuthInterop: 4fa327ec3c551a80a6929561f83af80b1dd44937 FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 @@ -442,6 +442,6 @@ SPEC CHECKSUMS: webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f -PODFILE CHECKSUM: c27b71cd46016bc2643e657c88b59244c316e68a +PODFILE CHECKSUM: d5ff9fcce4b2bc91659dc6d34600a8300e453909 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 03e8fadd9..e0dc639f7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,16 +7,18 @@ objects = { /* Begin PBXBuildFile section */ - 094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */; }; + 0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; }; + 0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; }; 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */; }; 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 */; }; + B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; }; + B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,16 +49,17 @@ 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; 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 = ""; }; + 1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 209BB2477C918E1C69F5F1E0 /* 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 = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4AC518688D83E399D06C65D9 /* 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 = ""; }; + 3CFCAD0427BE38C23B922B35 /* 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 = ""; }; + 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 8AEFEF9B7F1F2853E3BE4328 /* 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 = ""; }; - 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 85F42A75E97670A970955821 /* 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 = ""; }; 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; }; @@ -64,11 +67,10 @@ 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 = ""; }; - AE1151546CDAD222B8569F48 /* 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 = ""; }; - CD485A3D7C665ACD79DE1A05 /* 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 = ""; }; D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - D6F4721705FE7CF4B2C9A7C7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E75F8D421A56891E8C81AD82 /* 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 = ""; }; + F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,7 +78,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */, + B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */, + B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,12 +96,12 @@ 1ACB686EC7DC36C6E33D372F /* Pods */ = { isa = PBXGroup; children = ( - 4AC518688D83E399D06C65D9 /* Pods-Runner.debug.xcconfig */, - AE1151546CDAD222B8569F48 /* Pods-Runner.release.xcconfig */, - D6F4721705FE7CF4B2C9A7C7 /* Pods-Runner.profile.xcconfig */, - 8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */, - CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */, - D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */, + 3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */, + 85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */, + 1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */, + 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */, + E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */, + FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -108,8 +110,8 @@ isa = PBXGroup; children = ( 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, - 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */, - 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */, + 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */, + F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -178,7 +180,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */, + B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, B3515CD9628E3332AFC88CD4 /* Frameworks */, @@ -197,15 +199,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */, + B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */, - 56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */, + 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */, + C482126191919ADDA94A189A /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -278,26 +280,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */ = { + 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); 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"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -316,7 +313,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -338,24 +350,29 @@ 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; }; - 4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */ = { + B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + 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; }; - 56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */ = { + C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -372,21 +389,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - 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 */ @@ -516,7 +518,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -535,7 +537,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -552,7 +554,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcodeproj/project.pbxproj.backup b/ios/Runner.xcodeproj/project.pbxproj.backup new file mode 100644 index 000000000..020a2d883 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj.backup @@ -0,0 +1,804 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; }; + 0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; }; + 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 */; }; + B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; }; + B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; + 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 = ""; }; + 1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 209BB2477C918E1C69F5F1E0 /* 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3CFCAD0427BE38C23B922B35 /* 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 = ""; }; + 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 85F42A75E97670A970955821 /* 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 = ""; }; + 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; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + E75F8D421A56891E8C81AD82 /* 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 = ""; }; + F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */, + 0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */, + B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3515CD9628E3332AFC88CD4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1ACB686EC7DC36C6E33D372F /* Pods */ = { + isa = PBXGroup; + children = ( + 3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */, + 85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */, + 1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */, + 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */, + E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */, + FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 1E5DDD8701F6FB27AA502080 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, + 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */, + F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 1ACB686EC7DC36C6E33D372F /* Pods */, + 1E5DDD8701F6FB27AA502080 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + B3515CD9628E3332AFC88CD4 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */, + C482126191919ADDA94A189A /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(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; + }; + B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(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; + }; + C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 42C82SZUVX; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 42C82SZUVX; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 42C82SZUVX; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/alta/swift-opus"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.2; + }; + }; + 0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/alta/swift-opus"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0AD3C3762DDE122500FD4F41 /* Opus */ = { + isa = XCSwiftPackageProductDependency; + package = 0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; + productName = Opus; + }; + 0AD3C37B2DDE142500FD4F41 /* Opus */ = { + isa = XCSwiftPackageProductDependency; + package = 0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; + productName = Opus; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..9bd1cd320 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "bd9b3a14091d57af541f2718cbec3c63a0b618a9a7c0674743c7cf43998a352d", + "pins" : [ + { + "identity" : "swift-opus", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alta/swift-opus", + "state" : { + "revision" : "5f0164b8915fb9001265c54413aa0f0efbd3df06", + "version" : "0.0.2" + } + } + ], + "version" : 3 +} diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index e1083c5d2..0c63e4138 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -226,8 +226,7 @@ class HomeView extends GetView { // 设备信息 SizedBox(height: 12.h), Obx(() { - print('连接状态: ${controller.connectionStatus}'); // 添加调试打印 - print('错误信息: ${controller.connectionErrorMsg}'); // 添加调试打印 + // 如果连接失败且有错误信息,显示错误提示 if (controller.connectionStatus == ConnectionStatus.failed && controller.connectionErrorMsg.isNotEmpty) { diff --git a/local_plugins/agent_service/LICENSE b/local_plugins/agent_service/LICENSE new file mode 100644 index 000000000..e69de29bb diff --git a/local_plugins/agent_service/ios/Classes/AgentService.swift b/local_plugins/agent_service/ios/Classes/AgentService.swift deleted file mode 100644 index 7fdf89e62..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentService.swift +++ /dev/null @@ -1,994 +0,0 @@ -import Foundation -import MicrosoftCognitiveServicesSpeech -import open_ai_service -import chat_storage - -/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑 -/// -/// 负责集成Azure语音服务、OpenAI服务和本地存储服务, -/// 提供语音识别、语音合成、AI对话等功能 -class AgentService { - // 常量定义 - private let TAG = "AgentService" - - // 上下文和监听器 - private var listeners = NSHashTable.weakObjects() - - // 配置参数 - private var azureSpeechKey: String = "" - private var azureSpeechRegion: String = "" - private var openaiApiKey: String = "" - private var openaiBaseUrl: String = "" - private var openaiModel: String = "gpt-3.5-turbo" - private var mcpServer: String = "" - - // Azure服务 - private var azureAsrHelper: AzureAsrHelper? - private var azureTtsHelper: AzureTtsHelper? - - // OpenAI服务 - private var openAIService: OpenAIServiceBridge? - - // 聊天存储服务 - private var chatStorageHelper: ChatStorageHelperBridge? - - // 会话ID,用于区分不同聊天上下文 - private let sessionId = "default_agent" - - // 历史聊天消息缓存 - private var historyMessages = [[String: Any]]() - - // 系统提示词 - private var systemPrompt = "" - - // 状态 - private var isInitialized = false - private var isRecognitionActive = false - private var isTtsSpeaking = false - private var hasSpeechDetected = false - private var isAiStreaming = false - - // AI流生成相关 - private var currentAiTask: DispatchWorkItem? - - // 空闲检测相关 - private var idleCheckTimer: Timer? - private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 - - /// 初始化 - init() { - // 创建OpenAI服务桥接器 - openAIService = OpenAIServiceBridge() - - // 创建聊天存储桥接器 - chatStorageHelper = ChatStorageHelperBridge() - } - - /** - * 初始化系统提示词 - */ - private func initSystemPrompt() { - systemPrompt = """ - 你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 - 遵循以下指导原则: - 1. 保持简短精炼的回答,因为用户是通过语音与你交流 - 2. 优先使用中文回复,除非用户明确要求使用其他语言 - 3. 当用户问题不明确时,礼貌地请求更多信息 - 4. 避免过长的列表,尽量将信息分成小段 - 5. 不要使用需要视觉展示的元素(如表格、图表或代码块) - 6. 记住用户之前的对话内容,保持对话连贯 - 7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 - 你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 - 无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 - """ - } - - /** - * 初始化 - * - * - Parameter config: 配置参数,包含所需的所有API密钥和设置 - * - Returns: 是否初始化成功 - */ - func initialize(config: [String: Any]) -> Bool { - if isInitialized { return true } - - do { - // 从配置中获取参数 - if let azureKey = config["azureSpeechKey"] as? String { - azureSpeechKey = azureKey - } - - if let azureRegion = config["azureSpeechRegion"] as? String { - azureSpeechRegion = azureRegion - } - - if let openaiKey = config["openaiApiKey"] as? String { - openaiApiKey = openaiKey - } - - if let baseUrl = config["openaiBaseUrl"] as? String { - openaiBaseUrl = baseUrl - } - - if let model = config["openaiModel"] as? String { - openaiModel = model - } - - if let server = config["mcpServer"] as? String { - mcpServer = server - } - - // 自定义系统提示词 - if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { - systemPrompt = customSystemPrompt - } else { - // 使用默认系统提示词 - initSystemPrompt() - } - - // 检查必要参数 - if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty { - print("\(TAG): 初始化失败:关键配置参数缺失") - return false - } - - // 初始化OpenAI服务 - openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer) - - // 初始化Azure ASR - azureAsrHelper = AzureAsrHelper(self) - let asrInitResult = azureAsrHelper?.initialize( - subscriptionKey: azureSpeechKey, - region: azureSpeechRegion, - audioSourceType: .microphone - ) ?? false - - if !asrInitResult { - print("\(TAG): Azure ASR初始化失败") - return false - } - - // 初始化Azure TTS - azureTtsHelper = AzureTtsHelper(self) - let ttsInitResult = azureTtsHelper?.initialize( - subscriptionKey: azureSpeechKey, - region: azureSpeechRegion, - callback: self - ) ?? false - - if !ttsInitResult { - print("\(TAG): Azure TTS初始化失败") - return false - } - - // 加载最近的聊天记录 - loadChatHistory() - - isInitialized = true - print("\(TAG): 代理服务初始化成功") - return true - } catch { - print("\(TAG): 初始化失败:\(error.localizedDescription)") - return false - } - } - - /** - * 添加事件监听器 - * - * - Parameter listener: 要添加的监听器 - */ - func addListener(_ listener: AgentServiceListener) { - listeners.add(listener as AnyObject) - } - - /** - * 设置事件监听器(替换现有监听器) - * - * - Parameter listener: 要设置的监听器 - */ - func setListener(_ listener: AgentServiceListener) { - listeners.removeAllObjects() - listeners.add(listener as AnyObject) - } - - /** - * 移除事件监听器 - * - * - Parameter listener: 要移除的监听器 - */ - func removeListener(_ listener: AgentServiceListener) { - listeners.remove(listener as AnyObject) - } - - /** - * 移除所有事件监听器 - */ - func clearListeners() { - listeners.removeAllObjects() - } - - /** - * 启动空闲检测 - */ - private func startIdleCheck() { - stopIdleCheck() // 先停止现有的检查 - - if !isRecognitionActive { return } - - // 创建定时器 - idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in - guard let self = self else { return } - - // 如果状态仍然是空闲的,则停止识别 - if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming { - self.stopRecognition() - self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds]) - } - } - } - - /** - * 停止空闲检测 - */ - private func stopIdleCheck() { - idleCheckTimer?.invalidate() - idleCheckTimer = nil - } - - /** - * 重启空闲检测 - * 当状态发生变化时调用 - */ - private func restartIdleCheck() { - if isRecognitionActive { - startIdleCheck() - } - } - - /** - * 开始语音识别 - * - * - Returns: 是否成功开始识别 - */ - func startRecognition() -> Bool { - if !isInitialized { - print("\(TAG): 服务未初始化") - return false - } - - if isRecognitionActive { return true } - - isRecognitionActive = true - hasSpeechDetected = false - - do { - // 创建连续识别回调 - class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback { - private weak var service: AgentService? - - init(_ service: AgentService) { - self.service = service - } - - func onRecognizing(recognizing: String, detectedLanguage: String) { - guard let service = service else { return } - - if !recognizing.isEmpty { - // 检测到语音,更新状态 - let previousHasSpeech = service.hasSpeechDetected - service.hasSpeechDetected = true - - // 状态发生变化时重启空闲检测 - if !previousHasSpeech { - service.restartIdleCheck() - } - - service.sendEvent("recognizing", [ - "text": recognizing, - "language": detectedLanguage - ]) - - // 如果TTS正在播放或AI正在生成,则触发打断 - if service.isTtsSpeaking || service.isAiStreaming { - service.interruptCurrentResponse() - } - } - } - - func onResult(text: String, detectedLanguage: String) { - guard let service = service else { return } - - if !text.isEmpty { - service.sendEvent("recognition_result", [ - "text": text, - "language": detectedLanguage - ]) - - service.processWithOpenAI(text: text) - } - - // 重置状态,继续识别 - let previousHasSpeech = service.hasSpeechDetected - service.hasSpeechDetected = false - - // 状态发生变化时重启空闲检测 - if previousHasSpeech { - service.restartIdleCheck() - } - } - - func onSessionStarted() { - guard let service = service else { return } - - service.sendEvent("recognition_started", ["status": "started"]) - // 启动空闲检测 - service.startIdleCheck() - } - - func onSessionStopped() { - guard let service = service else { return } - - service.isRecognitionActive = false - service.stopIdleCheck() - service.sendEvent("recognition_stopped", ["status": "stopped"]) - } - - func onCanceled(reason: String, errorDetails: String) { - guard let service = service else { return } - - service.isRecognitionActive = false - service.stopIdleCheck() - service.sendEvent("recognition_canceled", [ - "reason": reason, - "details": errorDetails - ]) - } - - func onError(error: String) { - guard let service = service else { return } - - service.isRecognitionActive = false - service.stopIdleCheck() - print("\(service.TAG): 语音识别出错: \(error)") - service.sendEvent("error", [ - "code": "RECOGNITION_ERROR", - "message": error - ]) - } - } - - let callback = ContinuousRecognizeCallbackImpl(self) - let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false - - if !result { - isRecognitionActive = false - print("\(TAG): 启动语音识别失败") - sendEvent("error", [ - "code": "RECOGNITION_START_ERROR", - "message": "启动语音识别失败" - ]) - } - - return result - } catch { - isRecognitionActive = false - print("\(TAG): 启动语音识别失败: \(error.localizedDescription)") - sendEvent("error", [ - "code": "RECOGNITION_START_ERROR", - "message": error.localizedDescription - ]) - return false - } - } - - /** - * 停止语音识别 - */ - func stopRecognition() { - if !isRecognitionActive { return } - - print("\(TAG): 停止语音识别") - - // 停止识别 - let _ = azureAsrHelper?.stopContinuousRecognition() - isRecognitionActive = false - stopIdleCheck() - print("\(TAG): 语音识别已停止") - } - - /** - * 打断当前响应 - * 停止TTS播放和AI流输出 - */ - func interruptCurrentResponse() { - if isAiStreaming || isTtsSpeaking { - // 停止TTS播放 - stopTts() - - // 停止AI流输出 - stopAiStream() - - // 发送打断事件 - sendEvent("response_interrupted", ["status": "interrupted"]) - } - } - - /** - * 停止AI流输出 - */ - private func stopAiStream() { - if isAiStreaming { - // 取消当前AI生成任务 - currentAiTask?.cancel() - currentAiTask = nil - - // 通知OpenAI服务终止当前流式请求 - openAIService?.cancelCurrentStream() - - // 更新状态 - isAiStreaming = false - - // 记录日志 - print("\(TAG): AI流输出已停止") - } - } - - /** - * 处理文本输入 - * 作为语音输入的补充,直接处理文本并通过事件返回结果 - * - * - Parameters: - * - text: 用户输入文本 - * - speakResponse: 是否朗读回复,默认为false - * - Returns: 是否成功开始处理 - */ - func processTextInput(text: String, speakResponse: Bool = false) -> Bool { - if !isInitialized { - print("\(TAG): 服务未初始化") - sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) - return false - } - - if text.isEmpty { - print("\(TAG): 文本输入不能为空") - sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"]) - return false - } - - // 使用OpenAI处理文本 - processWithOpenAI(text: text, speakResponse: speakResponse) - return true - } - - /** - * 使用OpenAI处理语音识别结果 - * - * - Parameter text: 用户输入文本 - */ - private func processWithOpenAI(text: String) { - processWithOpenAI(text: text, speakResponse: true) - } - - /** - * 使用OpenAI处理文本消息 - * - * - Parameters: - * - text: 用户输入文本 - * - speakResponse: 是否使用TTS朗读回复 - */ - private func processWithOpenAI(text: String, speakResponse: Bool = true) { - print("\(TAG): 用户问题: \(text)") - - // 创建用户文本消息并处理 - guard let userMessage = openAIService?.createUserMessage(text: text) else { - print("\(TAG): 创建用户消息失败") - return - } - - processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) - } - - /** - * 使用OpenAI处理图片 - * - * - Parameters: - * - imagePath: 图片文件路径 - * - text: 可选的文本描述或问题 - * - speakResponse: 是否朗读回复 - * - Returns: 是否成功开始处理 - */ - func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool { - if !isInitialized { - print("\(TAG): 服务未初始化") - sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) - return false - } - - if imagePath.isEmpty { - print("\(TAG): 图片路径不能为空") - sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"]) - return false - } - - // 通知开始处理图片 - sendEvent("image_processing", [ - "status": "processing", - "imagePath": imagePath - ]) - - // 异步处理图片 - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - - // 将图片转换为Base64格式 - guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else { - DispatchQueue.main.async { - print("\(self.TAG): 图片转换失败: \(imagePath)") - self.sendEvent("error", [ - "code": "IMAGE_CONVERSION_FAILED", - "message": "图片转换失败" - ]) - } - return - } - - // 通知图片准备完成 - DispatchQueue.main.async { - self.sendEvent("image_ready", [ - "status": "ready", - "imagePath": imagePath - ]) - - // 处理包含图片的消息 - self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse) - } - } - - return true - } - - /** - * 使用OpenAI处理图片 - * - * - Parameters: - * - imageBase64: Base64编码的图片数据 - * - text: 可选的文本描述或问题 - * - speakResponse: 是否朗读回复 - */ - private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) { - print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")") - - // 创建带图片的用户消息并处理 - guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else { - print("\(TAG): 创建带图片的用户消息失败") - return - } - - // 图片描述用于存储 - let displayText = text.isEmpty ? "[图片]" : text - processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true) - } - - /** - * 内部方法:通用的OpenAI处理逻辑 - * - * - Parameters: - * - userMessage: 用户消息(可以是文本或图片格式) - * - displayText: 用于显示和存储的文本 - * - speakResponse: 是否朗读回复 - * - hasImage: 是否包含图片 - */ - private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { - // 如果有正在进行的AI流式输出,先停止它 - stopAiStream() - - // 创建AI任务 - let workItem = DispatchWorkItem { [weak self] in - guard let self = self else { return } - - // 设置状态为正在流式输出 - self.isAiStreaming = true - - // 使用历史记录作为上下文发送到OpenAI - var responseBuilder = "" - - // 添加系统提示词到历史消息的副本中 - var messagesWithSystemPrompt: [[String: Any]] = [] - - // 先添加系统提示词 - if !self.systemPrompt.isEmpty { - if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) { - messagesWithSystemPrompt.append(systemMessage) - } - } - - // 再添加历史消息 - messagesWithSystemPrompt.append(contentsOf: self.historyMessages) - messagesWithSystemPrompt.append(userMessage) - - // 将用户消息添加到历史记录 - self.addToHistoryMessages(userMessage) - - // 流式回调 - class StreamCallbackBridge: NSObject, OpenAIStreamCallback { - private weak var service: AgentService? - private var responseBuilder: String - private let speakResponse: Bool - private let displayText: String - private let hasImage: Bool - - init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) { - self.service = service - self.responseBuilder = responseBuilder - self.speakResponse = speakResponse - self.displayText = displayText - self.hasImage = hasImage - super.init() - } - - func onToken(token: String) { - guard let service = service else { return } - - responseBuilder.append(token) - if speakResponse { - _ = service.azureTtsHelper?.speakStream(token) - } - // 发送流式回复token - service.sendEvent("assistant_token", ["token": token]) - } - - func onComplete() { - guard let service = service else { return } - - // 视情况决定是否朗读回复 - if speakResponse { - _ = service.azureTtsHelper?.flushStream() - } - - if !responseBuilder.isEmpty { - // 发送完整回复,包含是否有图片的标记 - var responseData: [String: Any] = [ - "text": responseBuilder, - "userInput": displayText - ] - - if hasImage { - responseData["hasImage"] = true - } - - service.sendEvent("assistant_response", responseData) - - // 添加AI回复到历史记录 - if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) { - service.addToHistoryMessages(assistantMessage) - } - - // 保存聊天记录 - service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder) - } - - // 标记AI流式输出已完成 - service.isAiStreaming = false - service.currentAiTask = nil - } - - func onError(error: Error) { - guard let service = service else { return } - - print("\(service.TAG): AI处理出错: \(error.localizedDescription)") - service.sendEvent("error", [ - "code": "AI_ERROR", - "message": error.localizedDescription - ]) - - // 标记AI流式输出已完成 - service.isAiStreaming = false - service.currentAiTask = nil - } - - func onFunctionCall(call: [String: Any]) { - guard let service = service else { return } - - if let name = call["name"] as? String { - service.sendEvent("function_call", [ - "name": name, - "arguments": call - ]) - - if name == "exit_interaction" { - service.stopRecognition() - } - } - } - - func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) { - guard let service = service else { return } - - service.sendEvent("function_call_result", [ - "function_call": functionCall, - "result": functionCallResult, - ]) - } - } - - let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage) - - self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback) - } - - // 保存任务引用并在全局队列中执行 - currentAiTask = workItem - DispatchQueue.global(qos: .userInitiated).async(execute: workItem) - } - - /** - * 加载最近的聊天历史记录 - */ - private func loadChatHistory() { - // 清空当前历史记录 - historyMessages.removeAll() - - // 使用ChatStorageHelper直接获取最近消息 - guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else { - print("\(TAG): 没有找到历史记录") - return - } - - // 将消息添加到历史记录 - for message in recentMessages { - if let sender = message["sender"] as? String, - let content = message["message"] as? String { - - if sender == "user" { - if let userMessage = openAIService?.createUserMessage(text: content) { - addToHistoryMessages(userMessage) - } - } else if sender == "assistant" { - if let assistantMessage = openAIService?.createAssistantMessage(text: content) { - addToHistoryMessages(assistantMessage) - } - } - } - } - - print("\(TAG): 已加载\(recentMessages.count)条历史记录") - } - - /** - * 添加消息到历史记录,保持最近10条 - */ - private func addToHistoryMessages(_ message: [String: Any]) { - // 添加新消息 - historyMessages.append(message) - - // 如果超过10条,删除最早的消息 - while historyMessages.count > 10 { - historyMessages.removeFirst() - } - } - - /** - * TTS播放函数 - * - * - Parameter text: 要播放的文本 - */ - func speakText(text: String) { - if text.isEmpty { return } - - // 更新状态 - isTtsSpeaking = true - restartIdleCheck() // 状态变化,重启检测 - - // 播放文本 - _ = azureTtsHelper?.speakText(text) - } - - /** - * 停止TTS播放 - */ - func stopTts() { - if isTtsSpeaking { - _ = azureTtsHelper?.stopSpeaking() - isTtsSpeaking = false - restartIdleCheck() // 状态变化,重启检测 - sendEvent("tts_stopped", ["status": "stopped"]) - } - } - - /** - * 保存聊天消息 - */ - private func saveChatMessage(userMessage: String, assistantMessage: String) { - DispatchQueue.global(qos: .background).async { [weak self] in - guard let self = self else { return } - - // 保存用户消息 - let userMessageId = self.chatStorageHelper?.saveMessage( - sessionId: self.sessionId, - message: userMessage, - sender: "user" - ) ?? -1 - - if userMessageId != -1 { - // 保存AI回复 - let assistantMessageId = self.chatStorageHelper?.saveMessage( - sessionId: self.sessionId, - message: assistantMessage, - sender: "assistant" - ) ?? -1 - - if assistantMessageId == -1 { - print("\(self.TAG): 保存助手消息失败") - } - } else { - print("\(self.TAG): 保存用户消息失败") - } - } - } - - /** - * 清除聊天历史 - */ - func clearChatHistory(completion: @escaping (Bool) -> Void) { - DispatchQueue.global(qos: .background).async { [weak self] in - guard let self = self else { - DispatchQueue.main.async { - completion(false) - } - return - } - - // 清除指定会话的所有消息 - let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false - - if success { - // 清空内存中的历史记录 - self.historyMessages.removeAll() - print("\(self.TAG): 聊天历史已清除") - } else { - print("\(self.TAG): 清除聊天历史失败") - } - - DispatchQueue.main.async { - completion(success) - } - } - } - - /** - * 发送事件 - */ - private func sendEvent(_ eventName: String, _ data: [String: Any]) { - // 向所有监听器发送事件 - let allListeners = listeners.allObjects - for case let listener as AgentServiceListener in allListeners { - listener.onEvent(eventName: eventName, data: data) - } - } - - /** - * 释放资源 - */ - func dispose() { - stopRecognition() - stopTts() - stopAiStream() - stopIdleCheck() - - azureAsrHelper?.dispose() - azureTtsHelper?.dispose() - - // 清除所有监听器 - clearListeners() - - // 重置单例状态,以便下次使用时可以重新初始化 - isInitialized = false - - print("\(TAG): 代理服务资源已释放") - } -} - -// MARK: - AzureTtsHelper.TtsCallback -extension AgentService: AzureTtsHelper.TtsCallback { - func onSynthesisStarted() { - isTtsSpeaking = true - // 状态变化,重置空闲检测 - restartIdleCheck() - sendEvent("tts_started", ["status": "started"]) - } - - func onSynthesizing() { - // 可以在此添加TTS合成中的处理逻辑 - } - - func onSynthesisCompleted() { - isTtsSpeaking = false - // 状态变化,重启空闲检测 - restartIdleCheck() - sendEvent("tts_completed", ["status": "completed"]) - } - - func onSynthesisCanceled() { - isTtsSpeaking = false - // 状态变化,重启空闲检测 - restartIdleCheck() - sendEvent("tts_canceled", ["status": "canceled"]) - } -} - -// MARK: - 外部插件桥接器 - -/// 桥接OpenAIService插件 -class OpenAIServiceBridge { - private let service = OpenAIService() - - /// 初始化服务 - func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) { - service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer) - } - - /// 创建系统消息 - func createSystemMessage(text: String) -> [String: Any] { - return service.createSystemMessage(text) - } - - /// 创建用户消息 - func createUserMessage(text: String) -> [String: Any] { - return service.createUserMessage(text) - } - - /// 创建助手消息 - func createAssistantMessage(text: String) -> [String: Any] { - return service.createAssistantMessage(text) - } - - /// 创建带图片的用户消息 - func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? { - return service.createUserMessageWithImage(text: text, imageBase64: imageBase64) - } - - /// 文件转Base64 - func fileToBase64(filePath: String) -> String? { - return service.fileToBase64(filePath: filePath) - } - - /// 发送流式消息 - func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) { - service.sendMessageStream(messages: messages, callback: callback) - } - - /// 取消当前流 - func cancelCurrentStream() { - service.cancelCurrentStream() - } -} - -/// OpenAI流式回调协议 -@objc protocol OpenAIStreamCallback { - func onToken(token: String) - func onComplete() - func onError(error: Error) - func onFunctionCall(call: [String: Any]) - func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) -} - -/// 桥接ChatStorageHelper插件 -class ChatStorageHelperBridge { - private let helper = ChatStorageHelper() - - /// 保存消息 - func saveMessage(sessionId: String, message: String, sender: String) -> Int64 { - return helper.saveMessage(sessionId: sessionId, message: message, sender: sender) - } - - /// 获取最近消息 - func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? { - return helper.getRecentMessages(sessionId: sessionId, count: count) - } - - /// 删除消息 - func deleteMessages(sessionId: String) -> Bool { - return helper.deleteMessages(sessionId: sessionId) - } -} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift new file mode 100644 index 000000000..5e0104755 --- /dev/null +++ b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift @@ -0,0 +1,797 @@ +import Foundation +import AVFoundation +import ble_service +import speech + +/// 代理服务事件监听器接口 +protocol AgentServiceListener { + /// 当事件发生时调用 + /// - Parameters: + /// - eventName: 事件名称 + /// - data: 事件数据 + func onEvent(eventName: String, data: [String: Any]) +} + +/// 代理服务实现类,负责语音识别、OpenAI对话和语音合成 +class AgentServiceImpl: NSObject { + // MARK: - 常量 + private let TAG = "AgentServiceImpl" + + // MARK: - 单例实现 + static let shared = AgentServiceImpl() + + // MARK: - 属性 + + // 监听器集合 + private var listeners = Set() + private let listenersLock = NSLock() + + // Azure 语音相关 + private var azureSpeechKey: String = "" + private var azureSpeechRegion: String = "" + private var isRecognizing: Bool = false + private var isSpeaking: Bool = false + + // 语音识别和合成服务 + private var azureAsrHelper: AzureAsrHelper? + private var azureTtsHelper: AzureTtsHelper? + + // OpenAI相关 + private var apiKey: String = "" + private var baseUrl: String = "https://api.openai.com" + private var model: String = "gpt-3.5-turbo" + private var systemPrompt: String = "" + private var chatHistory: [[String: Any]] = [] + + // MCP服务相关 + private var mcpServer: String = "" + + // 状态标志 + private var isInitialized: Bool = false + private var hasSpeechDetected: Bool = false + private var isAiStreaming: Bool = false + + // 空闲检测相关 + private var idleCheckTimer: Timer? + private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 + + // 音频播放器 + private var audioPlayer: AudioPlayer? + + // MARK: - 初始化 + + /// 私有初始化方法 + private override init() { + super.init() + + // 设置音频会话 + setupAudioSession() + + // 初始化音频播放器 + audioPlayer = AudioPlayer() + + // 初始化语音助手服务 + azureAsrHelper = AzureAsrHelper() + azureTtsHelper = AzureTtsHelper() + + // 设置BleService回调 + BleService.shared.setDelegate(self) + } + + // MARK: - 监听器管理 + + /// 添加事件监听器 + /// - Parameter listener: 要添加的监听器 + func addListener(_ listener: AgentServiceListener) { + listenersLock.lock() + listeners.insert(listener) + listenersLock.unlock() + } + + /// 移除事件监听器 + /// - Parameter listener: 要移除的监听器 + func removeListener(_ listener: AgentServiceListener) { + listenersLock.lock() + listeners.remove(listener) + listenersLock.unlock() + } + + /// 清除所有监听器 + func clearListeners() { + listenersLock.lock() + listeners.removeAll() + listenersLock.unlock() + } + + /// 向所有监听器发送事件 + /// - Parameters: + /// - eventName: 事件名称 + /// - data: 事件数据 + private func sendEvent(name eventName: String, data: [String: Any]) { + listenersLock.lock() + let currentListeners = self.listeners + listenersLock.unlock() + + for listener in currentListeners { + listener.onEvent(eventName: eventName, data: data) + } + } + + /// 发送错误事件 + /// - Parameter message: 错误信息 + private func sendError(_ message: String, code: String = "ERROR") { + sendEvent(name: "error", data: ["code": code, "message": message]) + } + + // MARK: - 初始化配置 + + /// 初始化系统提示词 + private func initSystemPrompt() { + systemPrompt = """ + 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。 + [系统提示词内容省略...] + """ + } + + /// 初始化配置 + /// - Parameter config: 配置参数 + /// - Returns: 是否初始化成功 + func initialize(config: [String: Any]) -> Bool { + if isInitialized { return true } + + // 设置OpenAI配置 + if let openaiApiKey = config["openaiApiKey"] as? String { + self.apiKey = openaiApiKey + } else { + sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR") + return false + } + + if let openaiBaseUrl = config["openaiBaseUrl"] as? String { + self.baseUrl = openaiBaseUrl + } + + if let openaiModel = config["openaiModel"] as? String { + self.model = openaiModel + } + + // 设置系统提示词 + if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { + self.systemPrompt = customSystemPrompt + } else { + initSystemPrompt() + } + + // 设置Azure语音配置 + if let azureSpeechKey = config["azureSpeechKey"] as? String { + self.azureSpeechKey = azureSpeechKey + } else { + sendError("Azure语音密钥缺失", code: "CONFIG_ERROR") + return false + } + + if let azureSpeechRegion = config["azureSpeechRegion"] as? String { + self.azureSpeechRegion = azureSpeechRegion + } else { + sendError("Azure语音区域缺失", code: "CONFIG_ERROR") + return false + } + + // 设置MCP服务 + if let mcpServer = config["mcpServer"] as? String { + self.mcpServer = mcpServer + } + + // 初始化Azure语音服务 + let asrInitSuccess = initializeAzureSpeech() + + isInitialized = asrInitSuccess + return asrInitSuccess + } + + // MARK: - 音频会话设置 + + /// 设置音频会话 + private func setupAudioSession() { + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + sendError("设置音频会话失败: \(error.localizedDescription)") + } + } + + // MARK: - Azure 语音服务 + + /// 初始化Azure语音服务 + private func initializeAzureSpeech() -> Bool { + // 添加事件监听器 + azureAsrHelper?.addListener(self) + azureTtsHelper?.addListener(self) + + // 初始化ASR服务 + guard let asrSuccess = azureAsrHelper?.initialize( + subscriptionKey: azureSpeechKey, + region: azureSpeechRegion, + supportedLanguages: ["zh-CN"] + ), asrSuccess else { + sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") + return false + } + + // 初始化TTS服务 + guard let ttsSuccess = azureTtsHelper?.initialize( + ttsAppId: "", // Azure TTS不需要appId + ttsAppToken: azureSpeechKey, + ttsResource: azureSpeechRegion, + language: "zh-CN" + ), ttsSuccess else { + sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") + return false + } + + // 设置TTS语音 + _ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") + + return true + } + + // MARK: - 空闲检测 + + /// 启动空闲检测 + private func startIdleCheck() { + stopIdleCheck() // 先停止现有的检查 + + if !isRecognizing { return } + + // 创建一个新的定时器 + idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in + guard let self = self else { return } + + if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { + self.stopRecognition() + self.sendEvent(name: "auto_stop", data: [ + "reason": "idle_timeout", + "seconds": self.maxIdleSeconds + ]) + } + } + } + + /// 停止空闲检测 + private func stopIdleCheck() { + idleCheckTimer?.invalidate() + idleCheckTimer = nil + } + + /// 重启空闲检测 + private func restartIdleCheck() { + if isRecognizing { + startIdleCheck() + } + } + + // MARK: - 语音识别 + + /// 开始语音识别 + /// - Returns: 是否成功启动识别 + func startRecognition() -> Bool { + if !isInitialized { + sendError("服务未初始化", code: "NOT_INITIALIZED") + return false + } + + // 防止重复启动 + if isRecognizing { + stopRecognition() + } + + // 启动语音识别 + guard let success = azureAsrHelper?.startContinuousRecognition(), success else { + sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") + return false + } + + return true + } + + /// 停止语音识别 + /// - Returns: 是否成功停止识别 + func stopRecognition() -> Bool { + if !isRecognizing { + return true + } + + // 停止语音识别 + guard let success = azureAsrHelper?.stopContinuousRecognition(), success else { + return false + } + + return true + } + + /// 推送音频数据 + /// - Parameter audioData: 音频数据 + /// - Returns: 是否成功推送 + func pushAudioData(_ audioData: Data) -> Bool { + if !isRecognizing || !isInitialized { + return false + } + + // 将音频数据推送到语音识别引擎 + return azureAsrHelper?.pushAudioData(audioData: audioData) ?? false + } + + // MARK: - 语音合成 + + /// 语音合成播放文本 + /// - Parameter text: 要播放的文本 + /// - Returns: 是否成功开始播放 + func speakText(_ text: String) -> Bool { + if !isInitialized { + sendError("服务未初始化", code: "NOT_INITIALIZED") + return false + } + + if text.isEmpty { + return false + } + + // 使用TTS服务进行语音合成 + return azureTtsHelper?.speakOnce(text) ?? false + } + + /// 停止语音合成 + /// - Returns: 是否成功停止合成 + func stopTts() -> Bool { + if !isSpeaking { + return true + } + + // 停止TTS服务 + return azureTtsHelper?.stop() ?? false + } + + /// 打断当前响应 + /// - Returns: 是否成功打断 + func interruptCurrentResponse() -> Bool { + var interrupted = false + + // 停止TTS播放 + if isSpeaking { + interrupted = stopTts() || interrupted + } + + // 停止AI流输出 + if isAiStreaming { + isAiStreaming = false + interrupted = true + } + + // 发送打断事件 + if interrupted { + sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) + } + + return interrupted + } + + // MARK: - 文本处理 + + /// 处理文本输入 + /// - Parameters: + /// - text: 文本内容 + /// - speakResponse: 是否朗读响应 + /// - Returns: 是否成功处理 + func processTextInput(_ text: String, speakResponse: Bool) -> Bool { + if !isInitialized { + sendError("服务未初始化", code: "NOT_INITIALIZED") + return false + } + + if text.isEmpty { + sendError("文本输入不能为空", code: "EMPTY_TEXT") + return false + } + + // TODO: 实现与OpenAI的通信 + // 这里只是模拟处理 + + // 将用户消息添加到聊天历史 + let userMessage: [String: Any] = [ + "role": "user", + "content": text + ] + chatHistory.append(userMessage) + + // 更新状态 + isAiStreaming = true + + // 模拟助手响应 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + + // 模拟流式输出 + let response = "这是对"\(text)"的回复。我是AI助手,很高兴为您服务。" + + // 逐词输出模拟 + let words = response.split(separator: " ") + for (index, word) in words.enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in + guard let self = self, self.isAiStreaming else { return } + self.sendEvent(name: "assistant_token", data: ["token": String(word) + " "]) + + // 如果需要语音播报,则合成语音 + if speakResponse { + self.azureTtsHelper?.speakStream(String(word) + " ") + } + } + } + + // 完整响应 + DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in + guard let self = self else { return } + + // 结束AI流式输出 + self.isAiStreaming = false + + // 发送完整响应 + self.sendEvent(name: "assistant_response", data: ["text": response]) + + // 将助手消息添加到聊天历史 + let assistantMessage: [String: Any] = [ + "role": "assistant", + "content": response + ] + self.chatHistory.append(assistantMessage) + + // 如果需要语音播报,则完成语音流 + if speakResponse { + self.azureTtsHelper?.flushStream() + } + } + } + + return true + } + + // MARK: - 图片处理 + + /// 处理图片输入 + /// - Parameters: + /// - imagePath: 图片路径 + /// - text: 文本描述 + /// - speakResponse: 是否朗读响应 + /// - Returns: 是否成功处理 + func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { + if !isInitialized { + sendError("服务未初始化", code: "NOT_INITIALIZED") + return false + } + + if imagePath.isEmpty { + sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH") + return false + } + + // 发送图片处理中事件 + sendEvent(name: "image_processing", data: [:]) + + // TODO: 实现与OpenAI Vision API的通信 + // 这里只是模拟处理 + + // 模拟图片处理 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + guard let self = self else { return } + + // 图片就绪事件 + self.sendEvent(name: "image_ready", data: [:]) + + // 更新状态 + self.isAiStreaming = true + + // 模拟AI分析结果 + let imageDescription = "这是一张图片分析结果。" + let additionalText = text.isEmpty ? "" : "关于您提问的"\(text)"," + let response = "\(imageDescription)\(additionalText)我可以看到这是一张图片。" + + // 发送AI助手token流 + let words = response.split(separator: " ") + for (index, word) in words.enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in + guard let self = self, self.isAiStreaming else { return } + + let token = String(word) + " " + self.sendEvent(name: "assistant_token", data: ["token": token]) + + // 如果需要语音播报,则合成语音 + if speakResponse { + self.azureTtsHelper?.speakStream(token) + } + } + } + + // 发送完整响应 + DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in + guard let self = self else { return } + + // 结束AI流式输出 + self.isAiStreaming = false + + self.sendEvent(name: "assistant_response", data: [ + "text": response, + "userInput": text.isEmpty ? "[图片]" : text, + "hasImage": true + ]) + + // 如果需要语音播报,则完成语音 + if speakResponse { + self.azureTtsHelper?.flushStream() + } + } + } + + return true + } + + // MARK: - 聊天历史管理 + + /// 清除聊天历史 + /// - Returns: 是否成功清除 + func clearChatHistory() -> Bool { + chatHistory.removeAll() + return true + } + + // MARK: - 资源释放 + + /// 释放资源 + /// - Returns: 是否成功释放 + func dispose() -> Bool { + // 停止语音识别 + if isRecognizing { + stopRecognition() + } + + // 停止语音合成 + if isSpeaking { + stopTts() + } + + // 停止空闲检测 + stopIdleCheck() + + // 移除BleService代理 + BleService.shared.setDelegate(nil) + + // 释放Azure Speech资源 + azureAsrHelper?.removeListener(self) + azureTtsHelper?.removeListener(self) + azureAsrHelper?.dispose() + azureTtsHelper?.dispose() + + // 释放音频播放器 + audioPlayer = nil + + // 清除监听器 + clearListeners() + + isInitialized = false + + return true + } +} + +// MARK: - AsrEventListener 实现 +extension AgentServiceImpl: AsrEventListener { + func onEvent(_ event: AsrEvent) { + switch event.type { + case .sessionStarted: + sendEvent(name: "recognition_started", data: [:]) + isRecognizing = true + startIdleCheck() + audioPlayer?.playStartSound() + + case .sessionStopped: + sendEvent(name: "recognition_stopped", data: [:]) + isRecognizing = false + stopIdleCheck() + audioPlayer?.playStopSound() + + case .recognizing: + if let text = event.params["text"] as? String { + // 检测到语音,更新状态 + let previousHasSpeech = hasSpeechDetected + hasSpeechDetected = true + + if !previousHasSpeech { + restartIdleCheck() + } + + var data: [String: Any] = ["text": text] + if let language = event.params["language"] as? String, !language.isEmpty { + data["language"] = language + } + + sendEvent(name: "recognizing", data: data) + + // 如果TTS正在播放或AI正在生成,则触发打断 + if isSpeaking || isAiStreaming { + interruptCurrentResponse() + } + } + + case .recognized: + if let text = event.params["text"] as? String, !text.isEmpty { + var data: [String: Any] = ["text": text] + if let language = event.params["language"] as? String, !language.isEmpty { + data["language"] = language + } + + sendEvent(name: "recognition_result", data: data) + + // 处理识别结果,发送到OpenAI + processTextInput(text, speakResponse: true) + } + + // 重置状态,继续识别 + let previousHasSpeech = hasSpeechDetected + hasSpeechDetected = false + + if previousHasSpeech { + restartIdleCheck() + } + + case .canceled: + var data: [String: Any] = [:] + if let reason = event.params["reason"] as? String { + data["reason"] = reason + } + if let errorDetails = event.params["errorDetails"] as? String { + data["details"] = errorDetails + } + + sendEvent(name: "recognition_canceled", data: data) + isRecognizing = false + stopIdleCheck() + + case .error: + var data: [String: Any] = [:] + if let errorCode = event.params["errorCode"] as? String { + data["errorCode"] = errorCode + } + if let message = event.params["message"] as? String { + data["message"] = message + } else if let error = event.params["error"] as? String { + data["message"] = error + } else { + data["message"] = "未知错误" + } + + sendEvent(name: "recognition_error", data: data) + isRecognizing = false + stopIdleCheck() + + default: + break + } + } +} + +// MARK: - TtsEventListener 实现 +extension AgentServiceImpl: TtsEventListener { + func onEvent(_ event: TtsEvent) { + switch event.type { + case .synthesisStarted: + isSpeaking = true + restartIdleCheck() + sendEvent(name: "tts_started", data: [:]) + + case .synthesisCompleted: + isSpeaking = false + restartIdleCheck() + sendEvent(name: "tts_completed", data: [:]) + + case .synthesisCanceled: + isSpeaking = false + restartIdleCheck() + + var data: [String: Any] = [:] + if let reason = event.params["reason"] as? String { + data["reason"] = reason + } + if let errorDetails = event.params["errorDetails"] as? String { + data["details"] = errorDetails + } + + sendEvent(name: "tts_canceled", data: data) + + case .error: + isSpeaking = false + restartIdleCheck() + + var data: [String: Any] = [:] + if let errorCode = event.params["errorCode"] as? String { + data["errorCode"] = errorCode + } + if let errorMessage = event.params["errorMessage"] as? String { + data["message"] = errorMessage + } else { + data["message"] = "TTS错误" + } + + sendEvent(name: "error", data: data) + + default: + break + } + } +} + +// MARK: - BleService.Callback 实现 +extension AgentServiceImpl: BleService.Callback { + func onScanResult(devices: [[String: Any]]) { + // 不处理扫描结果 + } + + func onConnectionStateChanged(state: Int) { + // 可以在这里处理连接状态变化,例如通知UI更新 + } + + func onAudioDataReceived(data: Data) { + // 直接将音频数据推送到Azure Speech进行识别 + pushAudioData(data) + } + + func onWakeupSignalReceived() { + NSLog("\(TAG): 收到唤醒信号") + + // 停止当前TTS,避免冲突 + stopTts() + + // 如果已经在识别,则重新启动 + stopRecognition() + startRecognition() + } + + func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { + // 可以处理设备信息,例如电量、设备名称等 + } +} + +// MARK: - 音频播放器 + +/// 简单的音频播放器,用于播放提示音 +class AudioPlayer { + private var player: AVAudioPlayer? + + /// 播放开始识别提示音 + func playStartSound() { + playSound(named: "start") + } + + /// 播放停止识别提示音 + func playStopSound() { + playSound(named: "stop") + } + + /// 播放音频文件 + /// - Parameter named: 音频文件名 + private func playSound(named: String) { + guard let path = Bundle.main.path(forResource: named, ofType: "wav") else { + print("找不到音频文件: \(named).wav") + return + } + + let url = URL(fileURLWithPath: path) + + do { + player = try AVAudioPlayer(contentsOf: url) + player?.prepareToPlay() + player?.play() + } catch { + print("播放音频失败: \(error.localizedDescription)") + } + } +} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServiceListener.swift b/local_plugins/agent_service/ios/Classes/AgentServiceListener.swift deleted file mode 100644 index d7a63e653..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentServiceListener.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// 代理服务事件监听器协议 -/// 用于接收代理服务发送的各类事件 -protocol AgentServiceListener: AnyObject { - /// 事件回调 - /// - Parameters: - /// - eventName: 事件名称 - /// - data: 事件数据 - func onEvent(eventName: String, data: [String: Any]) -} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m index e79f257b9..9ba9ae5d6 100644 --- a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m +++ b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m @@ -7,6 +7,6 @@ @implementation AgentServicePlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftAgentServicePlugin registerWithRegistrar:registrar]; + [AgentServicePlugin registerWithRegistrar:registrar]; } @end \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift new file mode 100644 index 000000000..34620ed46 --- /dev/null +++ b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift @@ -0,0 +1,227 @@ +import Flutter +import UIKit +import AVFoundation + +/// 代理服务插件,负责连接Flutter端与原生服务 +public class AgentServicePlugin: NSObject, FlutterPlugin { + private let TAG = "AgentServicePlugin" + private var eventSink: FlutterEventSink? + + // MARK: - 插件注册 + + /// 插件注册方法 + public static func register(with registrar: FlutterPluginRegistrar) { + let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) + let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) + + let instance = AgentServicePlugin() + + registrar.addMethodCallDelegate(instance, channel: methodChannel) + eventChannel.setStreamHandler(instance) + + // 添加监听器 + AgentServiceImpl.shared.addListener(instance) + + NSLog("\(instance.TAG): 插件已加载") + } + + // MARK: - 方法处理 + + /// 处理来自Flutter的方法调用 + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + NSLog("\(TAG): 收到方法调用 - \(call.method)") + + switch call.method { + case "initialize": + handleInitialize(call, result: result) + case "startAgentService": + handleStartAgentService(call, result: result) + case "stopAgentService": + handleStopAgentService(result: result) + case "startConversation": + handleStartConversation(result: result) + case "stopConversation": + handleStopConversation(result: result) + case "processTextInput": + handleProcessTextInput(call, result: result) + case "speakText": + handleSpeakText(call, result: result) + case "stopTts": + handleStopTts(result: result) + case "clearChatHistory": + handleClearChatHistory(result: result) + case "interruptCurrentResponse": + handleInterruptCurrentResponse(result: result) + case "processImageInput": + handleProcessImageInput(call, result: result) + case "dispose": + handleDispose(result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + // MARK: - 方法实现 + + private func handleInitialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let config = args["config"] as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) + return + } + + // 初始化AgentServiceImpl + let success = AgentServiceImpl.shared.initialize(config: config) + result(success) + } + + private func handleStartAgentService(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let config = args["config"] as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) + return + } + + NSLog("\(TAG): 启动AgentService,配置参数: \(config)") + + // 初始化AgentServiceImpl + let success = AgentServiceImpl.shared.initialize(config: config) + + if success { + result(true) + } else { + result(FlutterError(code: "SERVICE_START_ERROR", message: "AgentService启动失败", details: nil)) + } + } + + private func handleStopAgentService(result: @escaping FlutterResult) { + NSLog("\(TAG): 停止AgentService") + let success = AgentServiceImpl.shared.dispose() + result(success) + } + + private func handleStartConversation(result: @escaping FlutterResult) { + NSLog("\(TAG): 开始对话") + let success = AgentServiceImpl.shared.startRecognition() + result(success) + } + + private func handleStopConversation(result: @escaping FlutterResult) { + NSLog("\(TAG): 停止对话") + let success = AgentServiceImpl.shared.stopRecognition() + result(success) + } + + private func handleProcessTextInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let text = args["text"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) + return + } + let speakResponse = args["speakResponse"] as? Bool ?? false + + NSLog("\(TAG): 处理文本输入 - \(text)") + let success = AgentServiceImpl.shared.processTextInput(text, speakResponse: speakResponse) + result(success) + } + + private func handleSpeakText(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let text = args["text"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) + return + } + + NSLog("\(TAG): 语音合成 - \(text)") + let success = AgentServiceImpl.shared.speakText(text) + result(success) + } + + private func handleStopTts(result: @escaping FlutterResult) { + NSLog("\(TAG): 停止语音合成") + let success = AgentServiceImpl.shared.stopTts() + result(success) + } + + private func handleClearChatHistory(result: @escaping FlutterResult) { + NSLog("\(TAG): 清除聊天历史") + let success = AgentServiceImpl.shared.clearChatHistory() + result(success) + } + + private func handleInterruptCurrentResponse(result: @escaping FlutterResult) { + NSLog("\(TAG): 中断当前响应") + let success = AgentServiceImpl.shared.interruptCurrentResponse() + result(success) + } + + private func handleProcessImageInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let imagePath = args["imagePath"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) + return + } + let text = args["text"] as? String ?? "" + let speakResponse = args["speakResponse"] as? Bool ?? false + + NSLog("\(TAG): 处理图片输入 - \(imagePath)") + let success = AgentServiceImpl.shared.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) + result(success) + } + + private func handleDispose(result: @escaping FlutterResult) { + NSLog("\(TAG): 释放资源") + + // 移除监听器并释放资源 + AgentServiceImpl.shared.removeListener(self) + let success = AgentServiceImpl.shared.dispose() + + result(success) + } + + // MARK: - 实例析构 + + deinit { + // 确保在实例销毁时移除监听器 + AgentServiceImpl.shared.removeListener(self) + NSLog("\(TAG): 插件已卸载") + } +} + +// MARK: - AgentServiceListener 实现 +extension AgentServicePlugin: AgentServiceListener { + public func onEvent(eventName: String, data: [String: Any]) { + guard let eventSink = eventSink else { return } + + // 将事件转换为JSON字符串 + let eventData: [String: Any] = [ + "event": eventName, + "data": data + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: []) + if let jsonString = String(data: jsonData, encoding: .utf8) { + // 确保在主线程发送事件到Flutter + DispatchQueue.main.async { + eventSink(jsonString) + } + } + } catch { + NSLog("\(TAG): 错误 - 事件数据序列化失败: \(error.localizedDescription)") + } + } +} + +// MARK: - FlutterStreamHandler 实现 +extension AgentServicePlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + self.eventSink = nil + return nil + } +} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift b/local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift deleted file mode 100644 index 5aa8195a9..000000000 --- a/local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift +++ /dev/null @@ -1,259 +0,0 @@ -import Flutter -import UIKit -import open_ai_service -import chat_storage - -/// Swift版Agent Service插件 -/// 处理Flutter与原生层的通信 -public class SwiftAgentServicePlugin: NSObject, FlutterPlugin { - // Flutter方法通道 - private var methodChannel: FlutterMethodChannel? - - // Flutter事件通道 - private var eventChannel: FlutterEventChannel? - - // 事件sink,用于向Flutter发送事件 - private var eventSink: FlutterEventSink? - - // 核心服务实例 - private var agentService: AgentService? - - /// 注册插件 - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = SwiftAgentServicePlugin() - - // 初始化方法通道 - let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) - registrar.addMethodCallDelegate(instance, channel: methodChannel) - instance.methodChannel = methodChannel - - // 初始化事件通道 - let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) - eventChannel.setStreamHandler(instance) - instance.eventChannel = eventChannel - } - - /// 处理方法调用 - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialize": - handleInitialize(call, result) - - case "startConversation": - handleStartConversation(result) - - case "stopConversation": - handleStopConversation(result) - - case "processTextInput": - handleProcessTextInput(call, result) - - case "speakText": - handleSpeakText(call, result) - - case "stopTts": - handleStopTts(result) - - case "clearChatHistory": - handleClearChatHistory(result) - - case "interruptCurrentResponse": - handleInterruptCurrentResponse(result) - - case "processImageInput": - handleProcessImageInput(call, result) - - case "dispose": - handleDispose(result) - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - 方法处理 - - /// 初始化 - private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any], - let config = args["config"] as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil)) - return - } - - // 初始化代理服务 - if agentService == nil { - agentService = AgentService() - - // 设置事件监听器 - agentService?.setListener(self) - } - - // 执行初始化 - let success = agentService?.initialize(config: config) ?? false - result(success) - } - - /// 开始对话 - private func handleStartConversation(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - let success = service.startRecognition() - result(success) - } - - /// 停止对话 - private func handleStopConversation(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - service.stopRecognition() - result(true) - } - - /// 处理文本输入 - private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - guard let args = call.arguments as? [String: Any], - let text = args["text"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) - return - } - - let speakResponse = (args["speakResponse"] as? Bool) ?? false - let success = service.processTextInput(text: text, speakResponse: speakResponse) - result(success) - } - - /// 朗读文本 - private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - guard let args = call.arguments as? [String: Any], - let text = args["text"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) - return - } - - service.speakText(text: text) - result(true) - } - - /// 停止语音合成 - private func handleStopTts(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - service.stopTts() - result(true) - } - - /// 清除聊天历史 - private func handleClearChatHistory(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - service.clearChatHistory { success in - result(success) - } - } - - /// 中断当前响应 - private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - service.interruptCurrentResponse() - result(true) - } - - /// 处理图片输入 - private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let service = agentService else { - result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) - return - } - - guard let args = call.arguments as? [String: Any], - let imagePath = args["imagePath"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil)) - return - } - - let text = (args["text"] as? String) ?? "" - let speakResponse = (args["speakResponse"] as? Bool) ?? false - - let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) - result(success) - } - - /// 释放资源 - private func handleDispose(_ result: @escaping FlutterResult) { - guard let service = agentService else { - result(true) // 已经是nil,直接返回成功 - return - } - - service.dispose() - agentService = nil - - result(true) - } -} - -// MARK: - FlutterStreamHandler -extension SwiftAgentServicePlugin: FlutterStreamHandler { - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - self.eventSink = nil - return nil - } -} - -// MARK: - AgentServiceListener -extension SwiftAgentServicePlugin: AgentServiceListener { - func onEvent(eventName: String, data: [String: Any]) { - guard let sink = eventSink else { return } - - do { - // 构建事件JSON - let eventMap: [String: Any] = [ - "event": eventName, - "data": data - ] - - // 将事件转换为JSON字符串 - let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: []) - if let jsonString = String(data: jsonData, encoding: .utf8) { - // 发送到Flutter - DispatchQueue.main.async { - sink(jsonString) - } - } - } catch { - print("事件序列化失败: \(error.localizedDescription)") - } - } -} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/agent_service.podspec b/local_plugins/agent_service/ios/agent_service.podspec new file mode 100644 index 000000000..9b4661ef8 --- /dev/null +++ b/local_plugins/agent_service/ios/agent_service.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint agent_service.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'agent_service' + s.version = '0.0.1' + s.summary = 'A Flutter plugin for agent service with voice interaction capabilities.' + s.description = <<-DESC +A Flutter plugin that provides voice interaction, AI conversation and text-to-speech functionalities. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'azure_speech' + s.dependency 'ble_service' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h index fe939442f..7f8606465 100644 --- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h +++ b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h @@ -1,4 +1,17 @@ +// +// AzureSpeechPlugin.h +// azure_speech +// +// Created for azure_speech plugin compatibility. +// + #import -@interface AzureSpeechPlugin : NSObject +/** + * Azure Speech Plugin 头文件 + * + * 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容 + */ +@interface AzureSpeechPlugin : NSObject ++ (void)registerWithRegistrar:(NSObject*)registrar; @end \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m deleted file mode 100644 index ca89297ef..000000000 --- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "AzureSpeechPlugin.h" -#if __has_include() -#import -#else -#import "azure_speech-Swift.h" -#endif - -@implementation AzureSpeechPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftAzureSpeechPlugin registerWithRegistrar:registrar]; -} -@end \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift index 87ae07e43..2238d5dd7 100644 --- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift @@ -11,7 +11,7 @@ import speech * 基于微软Azure语音服务的Flutter插件 * 提供语音识别(ASR)和语音合成(TTS)功能 */ -public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin { +@objc public class AzureSpeechPlugin: NSObject, FlutterPlugin { // 日志标签 private let tag = "AzureSpeechPlugin" @@ -32,7 +32,7 @@ public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin { // 插件注册 public static func register(with registrar: FlutterPluginRegistrar) { - let instance = SwiftAzureSpeechPlugin() + let instance = AzureSpeechPlugin() // 初始化ASR通道 let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger()) @@ -248,7 +248,7 @@ public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin { } // MARK: - 事件处理 -extension SwiftAzureSpeechPlugin: FlutterStreamHandler { +extension AzureSpeechPlugin: FlutterStreamHandler { public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { // 根据通道类型设置事件接收器 if let args = arguments as? [String: Any], @@ -297,7 +297,7 @@ extension SwiftAzureSpeechPlugin: FlutterStreamHandler { } // MARK: - ASR 事件监听实现 -extension SwiftAzureSpeechPlugin: AsrEventListener { +extension AzureSpeechPlugin: AsrEventListener { // 为ASR事件监听提供明确的选择器名称 @objc(onAsrEvent:) public func onEvent(_ event: AsrEvent) { @@ -334,7 +334,7 @@ extension SwiftAzureSpeechPlugin: AsrEventListener { } // MARK: - TTS 事件监听实现 -extension SwiftAzureSpeechPlugin: TtsEventListener { +extension AzureSpeechPlugin: TtsEventListener { // 使用@objc特性为方法提供一个不同的Objective-C选择器名称 @objc(onTtsEvent:) public func onEvent(_ event: TtsEvent) { @@ -374,7 +374,7 @@ extension SwiftAzureSpeechPlugin: TtsEventListener { } // MARK: - 音频数据监听实现 -extension SwiftAzureSpeechPlugin: AudioDataListener { +extension AzureSpeechPlugin: AudioDataListener { public func onAudioData(_ data: Data) { if ttsEventSink == nil { return } diff --git a/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h b/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h deleted file mode 100644 index 1d4245b2b..000000000 --- a/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef azure_speech_Bridging_Header_h -#define azure_speech_Bridging_Header_h - -#import - -#endif /* azure_speech_Bridging_Header_h */ \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/azure_speech.h b/local_plugins/azure_speech/ios/Classes/azure_speech.h deleted file mode 100644 index 7cb0fd7ab..000000000 --- a/local_plugins/azure_speech/ios/Classes/azure_speech.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -#import "AzureSpeechPlugin.h" - -// 添加其他公共头文件导出 \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/azure_speech.podspec b/local_plugins/azure_speech/ios/azure_speech.podspec index 7b6cf275e..230070cf4 100644 --- a/local_plugins/azure_speech/ios/azure_speech.podspec +++ b/local_plugins/azure_speech/ios/azure_speech.podspec @@ -13,13 +13,17 @@ A Flutter plugin for Azure Speech Services providing speech recognition and text s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{swift}' s.dependency 'Flutter' s.dependency 'speech' s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.30.0' s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'SWIFT_EMIT_LOC_STRINGS' => 'YES' + } s.swift_version = '5.0' end \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift index 990f5661a..539518668 100644 --- a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -1,72 +1,24 @@ import Foundation import CoreBluetooth import os.log -import ObjectiveC // 添加ObjC运行时支持 import AVFoundation - -// 条件编译只导入在iOS 18+上可用的框架 -#if canImport(AccessorySetupKit) && swift(>=5.9) import AccessorySetupKit import UIKit -#endif class BleAccessorySetup: NSObject { // 单例实现 static let shared = BleAccessorySetup() // 基本属性 - private var bleService: Any? + private var bleService: BleService? + private var session: ASAccessorySession? + private var isSessionActivated = false + private var isInitializing = false private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") - // iOS 18+ 特定属性和回调 - #if canImport(AccessorySetupKit) && swift(>=5.9) - @available(iOS 18.0, *) - class AccessorySessionWrapper { - var session: ASAccessorySession - var isActivated = false - - init() throws { - do { - os_log("尝试初始化ASAccessorySession...", log: BleAccessorySetup.shared.logger, type: .debug) - // 根据WWDC24示例,使用无参初始化 - self.session = try ASAccessorySession() - os_log("ASAccessorySession初始化成功", log: BleAccessorySetup.shared.logger, type: .info) - } catch { - os_log("ASAccessorySession初始化错误: %{public}@", log: BleAccessorySetup.shared.logger, type: .error, error.localizedDescription) - - // 检查Info.plist配置 - if let plistSupports = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") { - os_log("NSAccessorySetupKitSupports: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: plistSupports)) - } else { - os_log("NSAccessorySetupKitSupports未配置", log: BleAccessorySetup.shared.logger, type: .error) - } - - if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") { - os_log("NSAccessorySetupBluetoothServices: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: services)) - } else { - os_log("NSAccessorySetupBluetoothServices未配置", log: BleAccessorySetup.shared.logger, type: .error) - } - - if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") { - os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: companyIds)) - } else { - os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: BleAccessorySetup.shared.logger, type: .error) - } - - throw error - } - } - } - - var accessorySessionWrapper: Any? - // 添加初始化完成回调 private var onSessionActivated: (() -> Void)? - typealias DeviceDiscoveredCallback = (UUID) -> Void private var onDeviceDiscovered: DeviceDiscoveredCallback? - #endif - - private var isInitializing = false private override init() { super.init() @@ -74,238 +26,149 @@ class BleAccessorySetup: NSObject { // 设置BLE服务实例 func setBleService(_ service: Any) { - self.bleService = service - os_log("BLE服务实例已设置", log: logger, type: .info) + self.bleService = service as? BleService } // 设置设备发现回调 func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - self.onDeviceDiscovered = callback - os_log("设备发现回调已设置", log: logger, type: .debug) - } - #endif + self.onDeviceDiscovered = callback } // 初始化设备发现会话 func initializeDiscoverySession() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // 避免重复初始化 - if isInitializing { - os_log("初始化正在进行中,请等待...", log: logger, type: .debug) - return - } - - // 如果会话已经存在且已激活,无需重新初始化 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated { - os_log("会话已经初始化并激活", log: logger, type: .info) - return - } - - isInitializing = true - os_log("初始化iOS 18 AccessorySetupKit会话...", log: logger, type: .debug) - - // 检查Info.plist配置并详细打印 - if let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") { - os_log("NSAccessorySetupKitSupports配置: %{public}@", log: logger, type: .debug, String(describing: plistCheck)) - } else { - os_log("NSAccessorySetupKitSupports未配置", log: logger, type: .error) - } - - if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") { - os_log("NSAccessorySetupBluetoothServices配置: %{public}@", log: logger, type: .debug, String(describing: services)) - } else { - os_log("NSAccessorySetupBluetoothServices未配置", log: logger, type: .error) - } - - if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") { - os_log("NSAccessorySetupBluetoothCompanyIdentifiers配置: %{public}@", log: logger, type: .debug, String(describing: companyIds)) - } else { - os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: logger, type: .error) - } + os_log("开始初始化设备发现会话", log: logger, type: .info) + + if isInitializing || isSessionActivated { + os_log("会话已在初始化或已激活,跳过初始化", log: logger, type: .debug) + return + } + + isInitializing = true + + do { + os_log("创建ASAccessorySession", log: logger, type: .debug) + session = try ASAccessorySession() - // 创建会话 (使用try-catch) - do { - os_log("尝试创建AccessorySessionWrapper...", log: logger, type: .debug) - accessorySessionWrapper = try AccessorySessionWrapper() - os_log("AccessorySessionWrapper创建成功", log: logger, type: .info) + os_log("激活会话", log: logger, type: .debug) + session?.activate(on: .main) { [weak self] event in + guard let self = self else { return } - // 激活会话并设置事件处理 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { - os_log("尝试激活ASAccessorySession...", log: logger, type: .debug) - wrapper.session.activate(on: .main) { [weak self] event in - guard let self = self else { - os_log("self已被释放,无法处理事件", log: BleAccessorySetup.shared.logger, type: .error) - return - } + os_log("收到会话事件: %{public}@", log: self.logger, type: .debug, String(describing: event.eventType)) + + switch event.eventType { + case .activated: + os_log("会话已激活", log: self.logger, type: .info) + self.isSessionActivated = true + DispatchQueue.main.async(execute: DispatchWorkItem(block: { + self.onSessionActivated?() + self.onSessionActivated = nil + })) + + case .accessoryAdded: + if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { + os_log("发现设备: %{public}@", log: self.logger, type: .info, uuid.uuidString) - os_log("收到ASAccessorySession事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) + // 记录设备详细信息 + let name = "设备_\(uuid.uuidString.prefix(8))" + os_log("设备名称: %{public}@", log: self.logger, type: .debug, name) + // 直接获取公司ID无需使用可选链 + let descriptor = accessory.descriptor + let companyIdentifier = descriptor.bluetoothCompanyIdentifier.rawValue + os_log("设备公司ID: 0x%{public}x", log: self.logger, type: .debug, companyIdentifier) - switch event.eventType { - case .activated: - os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) - - // 标记会话已激活 - if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper { - wrapper.isActivated = true - // 触发激活回调 - DispatchQueue.main.async { - self.onSessionActivated?() - self.onSessionActivated = nil // 防止重复调用 - } - } - - case .accessoryAdded: - if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { - os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) - self.onDeviceDiscovered?(uuid) - } else { - os_log("设备添加事件但缺少设备信息或UUID", log: self.logger, type: .debug) - } - - case .accessoryRemoved: - if let accessory = event.accessory { - os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName) - } - - // 根据WWDC24视频,正确的事件类型为accessoryChanged - case .accessoryChanged: - if let accessory = event.accessory { - os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName) - } - - // 根据WWDC24视频,失败事件类型已变更 - case .pickerSetupFailed: - if let error = event.error { - os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) - } else { - os_log("会话出错但无错误详情", log: self.logger, type: .debug) - } - - default: - os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) + self.onDeviceDiscovered?(uuid) + } else { + os_log("发现设备但缺少UUID信息", log: self.logger, type: .error) + if let accessory = event.accessory { + os_log("设备信息: %{public}@", log: self.logger, type: .debug, String(describing: accessory)) } } - // 检查现有授权设备 - let accessoriesCount = wrapper.session.accessories.count - os_log("发现%{public}d个已授权设备", log: logger, type: .info, accessoriesCount) - - if accessoriesCount > 0 { - // 如果有现有设备,触发回调 - for accessory in wrapper.session.accessories { - if let uuid = accessory.bluetoothIdentifier { - os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) - self.onDeviceDiscovered?(uuid) - } else { - os_log("已授权设备缺少UUID: %{public}@", log: logger, type: .debug, accessory.displayName) - } - } + case .accessoryRemoved: + os_log("设备已移除", log: self.logger, type: .debug) + if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { + os_log("移除设备UUID: %{public}@", log: self.logger, type: .debug, uuid.uuidString) } - } else { - os_log("accessorySessionWrapper类型转换失败", log: logger, type: .error) + + case .accessoryChanged: + os_log("设备已变更", log: self.logger, type: .debug) + + case .pickerSetupFailed: + os_log("选择器设置失败: %{public}@", log: self.logger, type: .error, event.error?.localizedDescription ?? "未知错误") + + default: + os_log("未处理的事件类型: %{public}@", log: self.logger, type: .debug, String(describing: event.eventType)) + break } - } catch { - os_log("致命错误:AccessorySetupKit初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - - // 重置初始化状态 - isInitializing = false - - // 释放失败的会话 - accessorySessionWrapper = nil - - // 尝试通知可能正在等待的回调 - DispatchQueue.main.async { - self.onSessionActivated?() - self.onSessionActivated = nil + } + + // 处理现有授权设备 + if let accessories = session?.accessories { + os_log("检查当前已授权设备:共%d个", log: logger, type: .debug, accessories.count) + for accessory in accessories { + if let uuid = accessory.bluetoothIdentifier { + os_log("处理已授权设备:%@", log: logger, type: .info, uuid.uuidString) + let name = "设备_\(uuid.uuidString.prefix(8))" + os_log("已授权设备名称:%@", log: logger, type: .debug, name) + self.onDeviceDiscovered?(uuid) + } else { + os_log("发现已授权设备但无UUID", log: logger, type: .error) + } } + } else { + os_log("无已授权设备", log: logger, type: .debug) } - } else { - os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) + } catch { + // 出错时重置状态 + os_log("初始化会话失败:%@", log: logger, type: .error, error.localizedDescription) + isInitializing = false + session = nil - // 即使在不支持的系统上,也要调用回调 - DispatchQueue.main.async { + DispatchQueue.main.async(execute: DispatchWorkItem(block: { self.onSessionActivated?() self.onSessionActivated = nil - } - } - #else - os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) - - // 即使在不支持的环境中,也要调用回调 - DispatchQueue.main.async { - self.onSessionActivated?() - self.onSessionActivated = nil + })) } - #endif - // 完成初始化过程 isInitializing = false + os_log("设备发现会话初始化完成", log: logger, type: .debug) } // 初始化会话并在完成后执行回调 func initializeSessionAndRun(completion: @escaping () -> Void) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("安全初始化会话...", log: logger, type: .debug) - - // 如果会话已存在且已激活 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated { - os_log("会话已初始化并激活,直接执行回调", log: logger, type: .info) - completion() - return - } - - // 如果会话存在但未激活,设置回调 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, !wrapper.isActivated { - os_log("会话已创建但未激活,等待激活...", log: logger, type: .debug) - self.onSessionActivated = completion - return - } - - // 需要从头创建会话 - os_log("需要创建新会话...", log: logger, type: .debug) - self.onSessionActivated = completion - initializeDiscoverySession() - } else { - // 不支持iOS 18,直接执行回调 + // 如果会话已存在且已激活 + if session != nil && isSessionActivated { completion() + return + } + + // 如果会话存在但未激活,设置回调 + if session != nil && !isSessionActivated { + self.onSessionActivated = completion + return } - #else - // 不支持AccessorySetupKit,直接执行回调 - completion() - #endif + + // 需要从头创建会话 + self.onSessionActivated = completion + initializeDiscoverySession() } // 重置会话状态 func resetSession() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("重置AccessorySetupKit会话...", log: logger, type: .debug) - - // 取消可能挂起的回调 - onSessionActivated = nil - - // 清理现有会话 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { - os_log("清理现有会话", log: logger, type: .debug) - wrapper.session.invalidate() - } - - // 重置状态 - accessorySessionWrapper = nil - isInitializing = false - - os_log("会话已重置", log: logger, type: .info) - } - #endif + // 取消可能挂起的回调 + onSessionActivated = nil + + // 清理现有会话 + session?.invalidate() + + // 重置状态 + session = nil + isSessionActivated = false + isInitializing = false } // 安全显示设备选择器 func safeShowDevicePicker() { - // 重置会话状态,确保干净的重新初始化 resetSession() initializeSessionAndRun { [weak self] in @@ -316,247 +179,166 @@ class BleAccessorySetup: NSObject { // 显示设备选择器 func showDevicePicker() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("尝试显示设备选择器...", log: logger, type: .debug) - - // 检查会话是否已初始化 - if accessorySessionWrapper == nil { - os_log("会话未初始化,尝试自动初始化...", log: logger, type: .debug) - safeShowDevicePicker() - return + os_log("尝试显示设备选择器", log: logger, type: .info) + + // 检查会话是否已初始化 + if session == nil { + os_log("会话未初始化,重新安全初始化", log: logger, type: .debug) + safeShowDevicePicker() + return + } + + // 检查会话是否已激活 + if !isSessionActivated { + os_log("会话尚未激活,等待激活后再显示选择器", log: logger, type: .debug) + onSessionActivated = { [weak self] in + self?.showDevicePicker() } + return + } + + // 创建设备描述符和显示项 + os_log("创建设备描述符", log: logger, type: .debug) + let descriptor = ASDiscoveryDescriptor() + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) + os_log("设置公司标识符:0x%x", log: logger, type: .debug, 0xEFAB) + + // 检查和记录发现设置 + if let session = self.session { + os_log("当前会话设置已激活: %{public}@", log: logger, type: .debug, + String(describing: self.isSessionActivated)) - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { - // 检查会话是否已激活 - if !wrapper.isActivated { - os_log("会话尚未激活,等待激活后再显示...", log: logger, type: .debug) - onSessionActivated = { [weak self] in - self?.showDevicePicker() - } - return - } - - os_log("找到会话包装器,准备显示选择器", log: logger, type: .info) - - // 根据WWDC24示例代码创建设备描述符和显示项 - os_log("创建设备描述符...", log: logger, type: .debug) - let descriptor = ASDiscoveryDescriptor() - - // 设置蓝牙服务UUID - // let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" // BleConst.PRIMARY_SERVICE_UUID - // os_log("📝 设置服务UUID: %{public}@", log: logger, type: .debug, serviceUUID) - // descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID) - - // 设置制造商ID - os_log("设置公司ID: 0xABEF", log: logger, type: .debug) - // descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") - descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) - // 创建默认图像 - os_log("获取设备图标...", log: logger, type: .debug) - let deviceImage = UIImage(systemName: "headphones") ?? UIImage() - - // 创建显示项 - os_log("创建选择器显示项...", log: logger, type: .debug) - let displayItem = ASPickerDisplayItem( - name: "语音助理耳机", - productImage: deviceImage, - descriptor: descriptor - ) - - // 确保在主线程上调用 - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - os_log("调用showPicker方法...", log: self.logger, type: .debug) - - // 再次检查Info.plist配置 - let bundle = Bundle.main - os_log("Info.plist检查:", log: self.logger, type: .debug) - if let supports = bundle.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") as? [String] { - os_log("NSAccessorySetupKitSupports: %{public}@", log: self.logger, type: .debug, supports.description) - } - if let services = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") as? [String] { - os_log("NSAccessorySetupBluetoothServices: %{public}@", log: self.logger, type: .debug, services.description) - } - if let companyIds = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") as? [String] { - os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: self.logger, type: .debug, companyIds.description) - } - - os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug) - - // 使用标准方法显示选择器 - wrapper.session.showPicker(for: [displayItem]) { [weak self] error in - guard let self = self else { return } - - if let error = error { - os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) - - // 错误码解析 - if let nsError = error as? NSError { - os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code) - - // 解析常见错误 - switch nsError.code { - case 550: - os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error) - case 500...599: - os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code) - default: - os_log("未知错误码", log: self.logger, type: .error) - } - - if let info = nsError.userInfo as? [String: Any] { - os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description) - } - } - } else { - os_log("设备选择器显示已触发", log: self.logger, type: .info) - } - } + let currentAccessories = session.accessories + os_log("当前会话有%d个已知设备", log: logger, type: .debug, currentAccessories.count) + } + + // 创建默认图像 + let deviceImage = UIImage(systemName: "headphones") ?? UIImage() + + // 创建显示项 + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: deviceImage, + descriptor: descriptor + ) + os_log("创建显示项:语音助理耳机", log: logger, type: .debug) + + // 显示选择器 + os_log("即将显示设备选择器", log: logger, type: .debug) + DispatchQueue.main.async(execute: DispatchWorkItem(block: { + self.session?.showPicker(for: [displayItem]) { error in + if let error = error { + os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, + error.localizedDescription) + } else { + os_log("设备选择器已显示", log: self.logger, type: .debug) } - } else { - os_log("会话未初始化,无法显示设备选择器", log: logger, type: .error) } - } else { - os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) - } - #else - os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) - #endif + })) } // 处理发现的设备 func handleDiscoveredAccessory(uuid: UUID) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("处理UUID为%{public}@的发现设备", log: logger, type: .debug, uuid.uuidString) - - guard let service = bleService as? BleService else { - os_log("BLE服务未设置或类型转换失败", log: logger, type: .error) - return - } - - os_log("按照iOS 18规范,直接使用UUID调用特定连接方法", log: logger, type: .debug) - service.connectToAccessory(uuid: uuid) + guard let service = bleService else { + return } - #endif + + service.connectToAccessory(uuid: uuid) } // 从指定的视图控制器显示设备选择器 func showDevicePickerFromViewController(_ viewController: UIViewController) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("从视图控制器显示设备选择器...", log: logger, type: .debug) - - safeShowDevicePickerWithUI(presentingVC: viewController) - } else { - os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) - } - #else - os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) - #endif + safeShowDevicePickerWithUI(presentingVC: viewController) } // 带UI上下文的安全显示选择器 private func safeShowDevicePickerWithUI(presentingVC: UIViewController) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - resetSession() + os_log("准备从视图控制器显示设备选择器", log: logger, type: .info) + resetSession() + + initializeSessionAndRun { [weak self, weak presentingVC] in + guard let self = self, let presentingVC = presentingVC else { + os_log("视图控制器或self已被释放", log: self?.logger ?? OSLog.default, type: .error) + return + } - initializeSessionAndRun { [weak self, weak presentingVC] in - guard let self = self, let presentingVC = presentingVC else { return } - - if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper { - os_log("会话已激活,准备显示选择器", log: self.logger, type: .info) + if self.session != nil && self.isSessionActivated { + os_log("会话已激活,准备显示设备选择器UI", log: self.logger, type: .debug) + DispatchQueue.main.async { + let descriptor = ASDiscoveryDescriptor() + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) + os_log("设置公司标识符:0x%x", log: self.logger, type: .debug, 0xEFAB) - // 从主线程创建和显示 - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - os_log("创建设备描述符...", log: self.logger, type: .debug) - let descriptor = ASDiscoveryDescriptor() - - // 设置服务UUID和制造商ID - // let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" - // os_log("设置服务UUID: %{public}@", log: self.logger, type: .debug, serviceUUID) - // descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID) - - os_log("设置公司ID: 0xABEF", log: self.logger, type: .debug) - // descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") - descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) - - // 创建默认图像 - let deviceImage = UIImage(systemName: "headphones") ?? UIImage() - - // 创建显示项 - let displayItem = ASPickerDisplayItem( - name: "语音助理耳机", - productImage: deviceImage, - descriptor: descriptor - ) - - // 检查权限,但不创建CBCentralManager - os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug) + os_log("描述符配置: %{public}@", log: self.logger, type: .debug, + String(describing: descriptor)) + + let deviceImage = UIImage(systemName: "headphones") ?? UIImage() + + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: deviceImage, + descriptor: descriptor + ) + + os_log("显示设备选择器UI", log: self.logger, type: .debug) + self.session?.showPicker(for: [displayItem]) { error in + os_log("设备选择器UI回调", log: self.logger, type: .debug) - // 显示选择器 - wrapper.session.showPicker(for: [displayItem]) { [weak self] error in - guard let self = self else { return } + if let error = error { + os_log("设备选择器返回错误: %{public}@", log: self.logger, type: .error, + error.localizedDescription) - if let error = error { - os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) - - // 错误码解析 - if let nsError = error as? NSError { - os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code) - - // 解析常见错误 - switch nsError.code { - case 550: - os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error) - case 500...599: - os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code) - default: - os_log("未知错误码", log: self.logger, type: .error) - } - - if let info = nsError.userInfo as? [String: Any] { - os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description) - } - } - } else { - os_log("设备选择器显示已触发", log: self.logger, type: .info) - } + let nsError = error as NSError + os_log("错误域: %{public}@, 错误码: %d", log: self.logger, type: .error, + nsError.domain, nsError.code) } } } + } else { + os_log("会话未激活或为nil,无法显示设备选择器", log: self.logger, type: .error) } } - #endif } - // 检查蓝牙是否可用,不使用CBCentralManager + // 检查蓝牙是否可用 func isBluetoothAvailable() -> Bool { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // iOS 18+环境下不进行蓝牙状态检查 - os_log("iOS 18环境下,跳过蓝牙状态检查", log: logger, type: .debug) + // 日志输出 + os_log("检查蓝牙可用性", log: logger, type: .debug) + + // 默认返回true,适用于iOS 18环境 + if bleService == nil { + os_log("BleService未设置,默认返回true", log: logger, type: .info) return true } - #endif - // 使用AVAudioSession检测是否有蓝牙设备连接 - let audioSession = AVAudioSession.sharedInstance() - let outputs = audioSession.currentRoute.outputs + // 尝试类型转换 + guard let _ = bleService as? BleService else { + os_log("BleService类型转换失败", log: logger, type: .debug) + return true + } - // 检查是否有蓝牙输出 - for output in outputs { - if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || output.portType == .bluetoothLE { - return true + // 使用AVAudioSession检查蓝牙连接 + do { + let audioSession = AVAudioSession.sharedInstance() + let outputs = audioSession.currentRoute.outputs + + // 检查蓝牙音频输出设备 + for output in outputs { + if output.portType == .bluetoothA2DP || + output.portType == .bluetoothHFP || + output.portType == .bluetoothLE { + os_log("发现蓝牙音频输出设备", log: logger, type: .debug) + return true + } } + + // 未找到蓝牙设备 + os_log("未找到蓝牙音频设备", log: logger, type: .debug) + } catch { + os_log("检查音频会话出错: %{public}@", log: logger, type: .error, error.localizedDescription) } - // 没有检测到蓝牙设备,但不确定蓝牙是否已打开 - return false + // 默认返回true,避免阻止功能 + return true } } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index 8269289bb..abdc67571 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -1,6 +1,8 @@ import Foundation import CoreBluetooth import os.log +import SpeexKit // 使用杰理的SpeexKit库替代OpusSwift +import AVFoundation @available(iOS 13.0, *) class BleService: NSObject { @@ -18,6 +20,9 @@ class BleService: NSObject { // 数据相关回调 func onAudioDataReceived(data: Data) + // 唤醒信号回调 + func onWakeupSignalReceived() + // 设备信息相关回调 func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } @@ -30,37 +35,38 @@ class BleService: NSObject { private var audioCharacteristic: CBCharacteristic? private var isScanning = false - private var delegate: Callback? + private var callbacks = [Callback]() private var connectionState = BleConst.STATE_DISCONNECTED private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") + // MARK: - Opus音频处理器 + private var opusProcessor: OpusAudioProcessor? + // MARK: - 初始化 private override init() { super.init() - // iOS 18+ 下不在初始化阶段创建 CBCentralManager #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - os_log("iOS 18+: 延迟初始化CBCentralManager,等待AccessorySetupKit完成", log: logger, type: .info) + // iOS 18+下不立即初始化,等待AccessorySetupKit } else { initializeCentralManager() } #else initializeCentralManager() #endif + + // 初始化Opus音频处理器 + initializeOpusProcessor() } // 延迟初始化中央管理器 private func initializeCentralManager() { - // 防止重复初始化 if centralManager != nil { return } - os_log("正在初始化CBCentralManager", log: logger, type: .info) - - // 初始化中央管理器,提供恢复标识以支持后台恢复 let options: [String: Any] = [ CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", CBCentralManagerOptionShowPowerAlertKey: true @@ -69,13 +75,60 @@ class BleService: NSObject { centralManager = CBCentralManager(delegate: self, queue: nil, options: options) } - // MARK: - 公共方法 + // 初始化Opus处理器 + private func initializeOpusProcessor() { + opusProcessor = OpusAudioProcessor() + opusProcessor?.setCallback(self) + + // 立即启动Opus解码流,保持整个生命周期内活跃 + startOpusDecoding() + } + + // 启动Opus解码流 + private func startOpusDecoding() { + let opusStarted = opusProcessor?.startDecoding( + sampleRate: 16000, // 设备端Opus编码采样率 + channels: 1, // 单声道 + frameSize: 640 // 40ms at 16kHz = 640 samples + ) ?? false + + if opusStarted { + os_log("Opus解码流启动成功,将在整个BLE生命周期内保持活跃", log: logger, type: .info) + } else { + os_log("Opus解码流启动失败", log: logger, type: .error) + } + } + + // MARK: - 回调管理 + + /// 添加回调代理 + func addCallback(_ callback: Callback) -> Bool { + if !callbacks.contains(where: { $0 === callback }) { + callbacks.append(callback) + os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) + return true + } + return false + } + + /// 移除回调代理 + func removeCallback(_ callback: Callback) -> Bool { + if let index = callbacks.firstIndex(where: { $0 === callback }) { + callbacks.remove(at: index) + os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) + return true + } + return false + } - /// 设置回调代理 - func setDelegate(_ delegate: Callback) { - self.delegate = delegate + /// 清除所有回调 + func clearCallbacks() { + callbacks.removeAll() + os_log("已清除所有BLE回调", log: logger, type: .debug) } + // MARK: - 公共方法 + /// 获取中央管理器,如果未初始化则先初始化 func getCentralManager() -> CBCentralManager { if centralManager == nil { @@ -86,21 +139,18 @@ class BleService: NSObject { /// 开始扫描BLE设备 func startScan() { - // iOS 18+ 需要处理AccessorySetupKit兼容性 #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - os_log("iOS 18+环境: 不进行传统扫描,请使用AccessorySetupKit配对", log: logger, type: .info) + // iOS 18+不使用传统扫描 return } #endif - // iOS 17及以下版本继续使用传统扫描 if centralManager == nil { initializeCentralManager() } guard let manager = centralManager, manager.state == .poweredOn else { - os_log("蓝牙未打开", log: logger, type: .error) return } @@ -110,13 +160,12 @@ class BleService: NSObject { isScanning = true - // 扫描特定服务的设备 let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] manager.scanForPeripherals(withServices: serviceUUIDs, options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ]) - os_log("开始扫描BLE设备", log: logger, type: .info) + os_log("开始扫描BLE设备...", log: logger, type: .info) } /// 停止扫描 @@ -124,24 +173,21 @@ class BleService: NSObject { if isScanning && centralManager != nil { centralManager?.stopScan() isScanning = false - os_log("停止扫描BLE设备", log: logger, type: .info) + os_log("停止BLE扫描", log: logger, type: .info) } } /// 连接到指定设备 func connect(to peripheral: CBPeripheral) { if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING { - os_log("已连接或正在连接中", log: logger, type: .info) return } - // 确保中央管理器已初始化 if centralManager == nil { initializeCentralManager() } guard let manager = centralManager else { - os_log("蓝牙管理器未初始化", log: logger, type: .error) return } @@ -154,19 +200,21 @@ class BleService: NSObject { CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionNotifyOnNotificationKey: true ]) + + os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString) } /// 断开当前连接 func disconnect() { guard let peripheral = peripheralDevice, connectionState == BleConst.STATE_CONNECTED else { - os_log("未连接设备", log: logger, type: .info) return } if centralManager != nil { updateConnectionState(BleConst.STATE_DISCONNECTING) centralManager?.cancelPeripheralConnection(peripheral) + os_log("开始断开设备连接", log: logger, type: .info) } } @@ -175,92 +223,231 @@ class BleService: NSObject { return connectionState == BleConst.STATE_CONNECTED } - // MARK: - 获取设备信息 + // MARK: - 回调通知方法 + + /// 通知所有回调扫描结果 + private func notifyScanResult(devices: [[String: Any]]) { + for callback in callbacks { + callback.onScanResult(devices: devices) + } + } + + /// 通知所有回调连接状态变化 + private func notifyConnectionStateChanged(state: Int) { + for callback in callbacks { + callback.onConnectionStateChanged(state: state) + } + } + + /// 通知所有回调接收到音频数据 + private func notifyAudioDataReceived(data: Data) { + for callback in callbacks { + callback.onAudioDataReceived(data: data) + } + } + + /// 通知所有回调接收到唤醒信号 + private func notifyWakeupSignalReceived() { + for callback in callbacks { + callback.onWakeupSignalReceived() + } + } + + /// 通知所有回调接收到设备信息 + private func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { + for callback in callbacks { + callback.onDeviceInfoReceived(infoType: infoType, infoData: infoData) + } + } + + // MARK: - 设备信息和控制命令 /// 获取设备版本号 func getDeviceVersion() -> Bool { - return sendCommand(BleConst.CMD_GET_VERSION) + os_log("获取耳机版本号...", log: logger, type: .info) + let paramData = Data([0xFF]) + return sendCommand(BleConst.CMD_GET_VERSION, data: paramData) } /// 获取产品ID func getProductId() -> Bool { - return sendCommand(BleConst.CMD_GET_PRODUCT_ID) + os_log("获取耳机产品ID...", log: logger, type: .info) + let paramData = Data([0xFF]) + return sendCommand(BleConst.CMD_GET_PRODUCT_ID, data: paramData) + } + + /// 获取颜色ID + func getColorId() -> Bool { + os_log("获取耳机颜色ID...", log: logger, type: .info) + let paramData = Data([0xFF]) + return sendCommand(BleConst.CMD_GET_COLOR_ID, data: paramData) } /// 获取电池信息 func getBatteryInfo() -> Bool { - return sendCommand(BleConst.CMD_GET_BATTERY_INFO) + os_log("获取耳机电量信息...", log: logger, type: .info) + let paramData = Data([0xFF]) + return sendCommand(BleConst.CMD_GET_BATTERY_INFO, data: paramData) } // MARK: - 编解码控制 /// 打开编码器 func openEncoder() -> Bool { - return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_ENCODE_ON) + os_log("打开编码(设备侧)...", log: logger, type: .info) + let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) + return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 打开解码器 func openDecoder() -> Bool { - return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_DECODE_ON) + os_log("打开解码...", log: logger, type: .info) + + // 确保Opus解码器处于活跃状态(如果不是则启动) + if opusProcessor?.isDecoding() != true { + startOpusDecoding() + } + + // 发送命令通知设备开始解码 + let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) + return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 打开A2DP解码器 func openA2DPDecoder() -> Bool { - return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_A2DP_PLAY) + os_log("打开A2DP解码...", log: logger, type: .info) + let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT]) + return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 关闭编解码器 func closeCodec() -> Bool { - return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_CLOSE) + os_log("关闭编解码...", log: logger, type: .info) + + // 不停止Opus解码流,只发送命令通知设备关闭编解码 + let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_RIGHT]) + return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) } /// 播放A2DP func playA2DP() -> Bool { + os_log("切换至A2DP播放模式...", log: logger, type: .info) return sendCommand(BleConst.CMD_PLAY_A2DP) } + /// 音量增大 + func volumeUp() -> Bool { + os_log("设备音量增加...", log: logger, type: .info) + return sendCommand(BleConst.CMD_VOLUME_UP) + } + + /// 音量减小 + func volumeDown() -> Bool { + os_log("设备音量减小...", log: logger, type: .info) + return sendCommand(BleConst.CMD_VOLUME_DOWN) + } + + /// 设置声道模式 + func setChannelMode(_ channelMode: UInt8) -> Bool { + let modeDesc: String + switch channelMode { + case BleConst.AUDIO_CHANNEL_LEFT: + modeDesc = "左声道" + case BleConst.AUDIO_CHANNEL_RIGHT: + modeDesc = "右声道" + case BleConst.AUDIO_CHANNEL_STEREO: + modeDesc = "立体声" + default: + os_log("无效的声道模式: %d", log: logger, type: .error, channelMode) + return false + } + + os_log("设置%@模式...", log: logger, type: .info, modeDesc) + let paramData = Data([0xA1, channelMode]) + return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) + } + // MARK: - 私有方法 - /// 更新连接状态并通知代理 + /// 更新连接状态并通知回调 private func updateConnectionState(_ state: Int) { connectionState = state - delegate?.onConnectionStateChanged(state: state) + notifyConnectionStateChanged(state: state) } - /// 向设备发送命令(无参数) - private func sendCommand(_ cmd: UInt8) -> Bool { + /// 向设备发送命令 + private func sendCommand(_ cmd: UInt8, data: Data = Data()) -> Bool { guard let characteristic = writeCharacteristic, connectionState == BleConst.STATE_CONNECTED else { - os_log("设备未连接或写特征未找到", log: logger, type: .error) + os_log("发送命令失败: 设备未连接", log: logger, type: .error) return false } - // 构造帧: 帧头 + 命令ID + 长度(0) + 校验和 - let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x00, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x00]) - - peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) - return true - } - - /// 向设备发送带参数的命令 - private func sendCommandWithParam(_ cmd: UInt8, param: UInt8) -> Bool { - guard let characteristic = writeCharacteristic, - connectionState == BleConst.STATE_CONNECTED else { - os_log("设备未连接或写特征未找到", log: logger, type: .error) + do { + // 命令数据长度 + let cmdLength = UInt8(data.count) + + // 准备完整的命令包:帧头 + 命令ID + 长度 + 数据 + CRC + var cmdPacket = Data() + cmdPacket.append(BleConst.APP_REQUEST_HEADER) // 帧头 + cmdPacket.append(cmd) // 命令类型 + cmdPacket.append(cmdLength) // 命令长度 + + // 添加命令数据(如果有) + if !data.isEmpty { + cmdPacket.append(data) + } + + // 计算CRC校验 - 对除CRC外的所有字节 + let crc = calculateCRC8(data: cmdPacket) + cmdPacket.append(crc) + + // 记录命令详情 + var dataHexString = "" + if !data.isEmpty { + dataHexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") + } + os_log("发送命令: CMD=0x%02X, 长度=%d, 数据=[%@], CRC=0x%02X", + log: logger, type: .debug, cmd, cmdLength, dataHexString, crc) + + // 发送数据 + peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withResponse) + return true + } catch { + os_log("发送命令异常: %@", log: logger, type: .error, error.localizedDescription) return false } + } + + /// 计算CRC8/MAXIM校验值 + private func calculateCRC8(data: Data) -> UInt8 { + var crc: UInt8 = 0 + let polynomial: UInt8 = 0x31 - // 构造帧: 帧头 + 命令ID + 长度(1) + 参数 + 校验和 - let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x01, param, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x01 ^ param]) + for byte in data { + crc ^= byte + for _ in 0..<8 { + if (crc & 0x80) != 0 { + crc = ((crc << 1) ^ polynomial) + } else { + crc = (crc << 1) + } + } + } - peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) - return true + return crc + } + + /// 处理收到的音频数据 + private func processAudioData(_ data: Data) { + // 使用Opus处理器处理音频数据 + opusProcessor?.processAudioData(data) } /// 处理接收到的响应数据 private func handleResponseData(_ data: Data) { guard data.count >= 4 else { - os_log("接收数据格式错误", log: logger, type: .error) + os_log("响应数据格式错误:数据长度过短", log: logger, type: .error) return } @@ -278,54 +465,300 @@ class BleService: NSObject { return } + // 记录响应详情 + var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") + os_log("收到设备响应: %@", log: logger, type: .debug, hexString) + + // 检查数据长度 + if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC + os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) + return + } + + // 计算CRC校验 - 不包括最后一个字节(CRC) + let payload = data.prefix(data.count - 1) + let calculatedCrc = calculateCRC8(data: payload) + let receivedCrc = data[data.count - 1] + + if calculatedCrc != receivedCrc { + os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) + return + } + + // 提取响应数据 + let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() + // 根据命令类型处理设备响应 switch cmd { case BleConst.CMD_GET_VERSION: - if length > 0 && data.count >= 4 + Int(length) { - let versionData = data.subdata(in: 3..<(3 + Int(length))) - if let versionStr = String(data: versionData, encoding: .utf8) { - delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_VERSION), - infoData: ["version": versionStr]) + var resultMap: [String: Any] = [ + "success": true, + "command": "versionInfo", + "rawData": responseData + ] + + if !responseData.isEmpty { + if let versionStr = String(data: responseData, encoding: .utf8) { + resultMap["version"] = versionStr + os_log("接收到目标版本号: %@", log: logger, type: .info, versionStr) + } else { + // 如果无法解析为字符串,以点分隔的数字表示版本 + let versionNums = responseData + .map { $0 } + .filter { $0 != 0xFF } // 过滤掉无效字节 + .map { String($0) } + .joined(separator: ".") + resultMap["version"] = versionNums + os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) } } + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) + case BleConst.CMD_GET_PRODUCT_ID: - if length >= 2 && data.count >= 5 { - let productId = UInt16(data[3]) << 8 | UInt16(data[4]) - delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_PRODUCT_ID), - infoData: ["productId": productId]) + var resultMap: [String: Any] = [ + "success": true, + "command": "productId", + "rawData": responseData + ] + + if responseData.count >= 2 { + let productId = UInt16(responseData[0]) << 8 | UInt16(responseData[1]) + resultMap["productId"] = productId + os_log("接收到产品ID: %d", log: logger, type: .info, productId) + } else if !responseData.isEmpty { + // 单字节产品ID + let productId = UInt16(responseData[0]) + resultMap["productId"] = productId + os_log("接收到产品ID: %d", log: logger, type: .info, productId) } + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) + + case BleConst.CMD_GET_COLOR_ID: + var resultMap: [String: Any] = [ + "success": true, + "command": "colorInfo", + "rawData": responseData + ] + + if !responseData.isEmpty { + let colorId = Int(responseData[0]) + resultMap["colorId"] = colorId + os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId) + } + + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) + case BleConst.CMD_GET_BATTERY_INFO: - if length >= 1 && data.count >= 4 { - let batteryLevel = Int(data[3]) - delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_BATTERY_INFO), - infoData: ["batteryLevel": batteryLevel]) + var resultMap: [String: Any] = [ + "success": true, + "command": "batteryInfo", + "rawData": responseData + ] + + if responseData.count >= 3 { + // 处理电池信息 - 左耳、右耳、充电仓 + let leftEarBattery = Int(responseData[0] & 0x7F) + let leftEarCharging = (responseData[0] & 0x80) != 0 + + let rightEarBattery = Int(responseData[1] & 0x7F) + let rightEarCharging = (responseData[1] & 0x80) != 0 + + let caseBattery = Int(responseData[2] & 0x7F) + let caseCharging = (responseData[2] & 0x80) != 0 + + // 添加到结果Map + resultMap["leftEarBattery"] = leftEarBattery + resultMap["leftEarCharging"] = leftEarCharging + resultMap["rightEarBattery"] = rightEarBattery + resultMap["rightEarCharging"] = rightEarCharging + resultMap["caseBattery"] = caseBattery + resultMap["caseCharging"] = caseCharging + resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 + + os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", + log: logger, type: .info, + leftEarBattery, leftEarCharging ? "充电中" : "未充电", + rightEarBattery, rightEarCharging ? "充电中" : "未充电", + caseBattery, caseCharging ? "充电中" : "未充电") + } else if !responseData.isEmpty { + // 兼容单字节电量 + let batteryLevel = Int(responseData[0]) + resultMap["batteryLevel"] = batteryLevel + os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel) } + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) + + case BleConst.CMD_CONTROL_CODEC: + var resultMap: [String: Any] = [ + "success": true, + "command": Int(cmd), + "rawData": responseData + ] + + if responseData.count >= 2 { + let codecStatus = Int(responseData[0]) + let channelMode = Int(responseData[1]) + + // 解码状态描述 + let statusDesc: String + switch codecStatus { + case Int(BleConst.CODEC_CONTROL_CLOSE): + statusDesc = "已关闭编解码" + case Int(BleConst.CODEC_CONTROL_DECODE_ON): + statusDesc = "已打开解码" + case Int(BleConst.CODEC_CONTROL_A2DP_PLAY): + statusDesc = "A2DP播放模式" + case Int(BleConst.CODEC_CONTROL_ENCODE_ON): + statusDesc = "已打开编码" + default: + statusDesc = "未知状态(\(codecStatus))" + } + + // 声道模式描述 + let channelDesc: String + switch channelMode { + case Int(BleConst.AUDIO_CHANNEL_LEFT): + channelDesc = "左声道" + case Int(BleConst.AUDIO_CHANNEL_RIGHT): + channelDesc = "右声道" + case Int(BleConst.AUDIO_CHANNEL_STEREO): + channelDesc = "立体声" + default: + channelDesc = "未知声道(\(channelMode))" + } + + resultMap["codecStatus"] = codecStatus + resultMap["channelMode"] = channelMode + resultMap["statusDesc"] = statusDesc + resultMap["channelDesc"] = channelDesc + + os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc) + } + + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) + default: - os_log("未知的响应命令: %{public}d", log: logger, type: .debug, cmd) + // 对于未知命令,返回原始数据 + let resultMap: [String: Any] = [ + "success": true, + "command": Int(cmd), + "rawData": responseData, + "isUnknown": true + ] + os_log("收到未知命令响应: 0x%02X", log: logger, type: .debug, cmd) + notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) } } /// 处理设备主动上报的通知数据 private func handleNotificationData(_ data: Data) { - guard data.count >= 4 else { return } + guard data.count >= 4 else { + os_log("设备主动上报数据格式错误:数据长度过短", log: logger, type: .error) + return + } - let cmd = data[1] + let header = data[0] // 0xCC + let infoType = data[1] let length = data[2] - switch cmd { + var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") + os_log("收到设备主动上报: %@", log: logger, type: .debug, hexString) + + // 检查数据长度 + if data.count != Int(length) + 4 { // 帧头 + 类型 + 长度 + 数据 + CRC + os_log("设备主动上报数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) + return + } + + // 计算CRC校验 - 不包括最后一个字节(CRC) + let payload = data.prefix(data.count - 1) + let calculatedCrc = calculateCRC8(data: payload) + let receivedCrc = data[data.count - 1] + + if calculatedCrc != receivedCrc { + os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) + return + } + + // 提取上报数据 + let notifyData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() + + // 根据上报类型处理 + switch infoType { + case BleConst.CMD_GET_BATTERY_INFO: + var resultMap: [String: Any] = [ + "success": true, + "command": Int(infoType), + "rawData": notifyData, + "isNotification": true // 标记为主动上报 + ] + + if notifyData.count >= 3 { + // 处理电池信息 - 左耳、右耳、充电仓 + let leftEarBattery = Int(notifyData[0] & 0x7F) + let leftEarCharging = (notifyData[0] & 0x80) != 0 + + let rightEarBattery = Int(notifyData[1] & 0x7F) + let rightEarCharging = (notifyData[1] & 0x80) != 0 + + let caseBattery = Int(notifyData[2] & 0x7F) + let caseCharging = (notifyData[2] & 0x80) != 0 + + // 添加到结果Map + resultMap["leftEarBattery"] = leftEarBattery + resultMap["leftEarCharging"] = leftEarCharging + resultMap["rightEarBattery"] = rightEarBattery + resultMap["rightEarCharging"] = rightEarCharging + resultMap["caseBattery"] = caseBattery + resultMap["caseCharging"] = caseCharging + resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 + + os_log("收到主动上报电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", + log: logger, type: .info, + leftEarBattery, leftEarCharging ? "充电中" : "未充电", + rightEarBattery, rightEarCharging ? "充电中" : "未充电", + caseBattery, caseCharging ? "充电中" : "未充电") + } + + notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) + case BleConst.CMD_WAKE_UP: - // 处理唤醒指令 - os_log("收到设备唤醒指令", log: logger, type: .info) - // 可以在这里添加唤醒回调 + os_log("收到唤醒指令", log: logger, type: .info) + // 接收到唤醒信号,打开编码器 (设备侧) + openEncoder() + notifyWakeupSignalReceived() default: - os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) + // 对于未知上报类型,返回原始数据 + let resultMap: [String: Any] = [ + "success": true, + "command": Int(infoType), + "rawData": notifyData, + "isUnknown": true, + "isNotification": true // 标记为主动上报 + ] + os_log("收到未知主动上报类型: 0x%02X", log: logger, type: .debug, infoType) + notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) } } + // MARK: - 资源清理 + + /// 清理所有资源 + func cleanup() { + stopScan() + disconnect() + + // 停止Opus解码 + opusProcessor?.cleanup() + opusProcessor = nil + + os_log("BleService资源已清理", log: logger, type: .debug) + } + // MARK: - AccessorySetupKit /// 在接收到AccessorySetupKit的accessoryAdded事件后调用 @@ -337,11 +770,7 @@ class BleService: NSObject { initializeCentralManager() } - os_log("通过AccessorySetupKit连接设备: %{public}@", log: logger, type: .info, uuid.uuidString) - - // 检查centralManager状态 guard let manager = centralManager, manager.state == .poweredOn else { - os_log("CBCentralManager尚未准备好或状态不是poweredOn: %d", log: logger, type: .error, centralManager?.state.rawValue ?? -1) return } @@ -349,43 +778,40 @@ class BleService: NSObject { let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid]) if let peripheral = peripherals.first { - os_log("找到AccessorySetupKit配对的设备,尝试连接", log: logger, type: .info) connect(to: peripheral) - } else { - os_log("未找到对应UUID的设备: %{public}@", log: logger, type: .error, uuid.uuidString) } } #endif } } +// MARK: - OpusAudioProcessor.AudioDataCallback +@available(iOS 13.0, *) +extension BleService: OpusAudioProcessor.AudioDataCallback { + /// 接收到解码后的PCM音频数据 + func onAudioDataReceived(data: Data) { + notifyAudioDataReceived(data: data) + } +} + // MARK: - CBCentralManagerDelegate @available(iOS 13.0, *) extension BleService: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: - os_log("蓝牙已开启", log: logger, type: .info) // 蓝牙已开启,可以开始扫描 + break case .poweredOff: - os_log("蓝牙已关闭", log: logger, type: .info) updateConnectionState(BleConst.STATE_DISCONNECTED) - case .resetting: - os_log("蓝牙正在重置", log: logger, type: .info) - - case .unauthorized: - os_log("蓝牙未授权", log: logger, type: .error) - - case .unsupported: - os_log("设备不支持蓝牙", log: logger, type: .error) - - case .unknown: - os_log("蓝牙状态未知", log: logger, type: .info) + case .resetting, .unauthorized, .unsupported, .unknown: + // 其他状态不处理 + break @unknown default: - os_log("蓝牙状态未定义", log: logger, type: .info) + break } } @@ -407,33 +833,30 @@ extension BleService: CBCentralManagerDelegate { } // 报告给回调 - delegate?.onScanResult(devices: [deviceInfo]) + notifyScanResult(devices: [deviceInfo]) } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.name ?? "Unknown") - updateConnectionState(BleConst.STATE_CONNECTED) // 查找服务 peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) + + // 确保Opus解码器处于活跃状态 + if opusProcessor?.isDecoding() != true { + startOpusDecoding() + } } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - os_log("连接失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "Unknown error") - updateConnectionState(BleConst.STATE_ERROR) } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - os_log("设备断开连接: %{public}@", log: logger, type: .info, error?.localizedDescription ?? "Normal disconnection") - updateConnectionState(BleConst.STATE_DISCONNECTED) } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - os_log("恢复先前的连接状态", log: logger, type: .info) - // 从恢复状态中获取之前连接的外设 if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], let peripheral = peripherals.first { @@ -458,15 +881,12 @@ extension BleService: CBCentralManagerDelegate { extension BleService: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { - os_log("服务发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } guard let services = peripheral.services else { return } for service in services { - os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString) - if service.uuid == BleConst.PRIMARY_SERVICE_UUID { // 主服务,发现写入和通知特征 peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) @@ -479,15 +899,12 @@ extension BleService: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { - os_log("特征发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } guard let characteristics = service.characteristics else { return } for characteristic in characteristics { - os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - if characteristic.uuid == BleConst.WRITE_CHAR_UUID { writeCharacteristic = characteristic } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { @@ -504,7 +921,6 @@ extension BleService: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("接收数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } @@ -515,23 +931,15 @@ extension BleService: CBPeripheralDelegate { handleResponseData(data) } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { // 处理音频数据 - delegate?.onAudioDataReceived(data: data) + processAudioData(data) } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - os_log("写入数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) - } + // 写入完成回调,不需要特殊处理 } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - os_log("订阅通知错误: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - os_log("特征 %{public}@ 的通知状态更新为 %{public}d", log: logger, type: .debug, - characteristic.uuid.uuidString, characteristic.isNotifying ? 1 : 0) + // 通知状态更新回调,不需要特殊处理 } } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/OpusAudioProcessor.swift b/local_plugins/ble_service/ios/Classes/OpusAudioProcessor.swift new file mode 100644 index 000000000..36ee13f5d --- /dev/null +++ b/local_plugins/ble_service/ios/Classes/OpusAudioProcessor.swift @@ -0,0 +1,128 @@ +import Foundation +import SpeexKit +import os.log + +/// 基于杰理SpeexKit的Opus音频处理器 +@available(iOS 13.0, *) +class OpusAudioProcessor: NSObject { + + // MARK: - 回调代理协议 + protocol AudioDataCallback: AnyObject { + /// 接收到解码后的PCM音频数据 + func onAudioDataReceived(data: Data) + } + + // MARK: - 属性 + private var isDecodingActive = false + private weak var callback: AudioDataCallback? + private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "OpusAudioProcessor") + + // MARK: - 初始化 + override init() { + super.init() + + // 注册通知 - 这是SDK设计要求的 + NotificationCenter.default.addObserver( + self, + selector: #selector(onOpusDecodedData(_:)), + name: NSNotification.Name(rawValue: kOPUS_DECODE_DATA), + object: nil + ) + + // 启用日志 + OpusUnit.opusIsLog(true) + } + + deinit { + stopDecoding() + NotificationCenter.default.removeObserver(self) + } + + // MARK: - 通知回调 + @objc private func onOpusDecodedData(_ notification: Notification) { + guard let pcmData = notification.object as? Data else { + os_log("解码回调收到无效数据", log: logger, type: .error) + return + } + + // 传递给回调 + DispatchQueue.main.async { [weak self] in + self?.callback?.onAudioDataReceived(data: pcmData) + } + } + + // MARK: - 公共接口 + + /// 设置回调 + func setCallback(_ callback: AudioDataCallback?) { + self.callback = callback + } + + /// 开始Opus解码 + func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool { + // 如果已经在解码,直接返回成功 + if isDecodingActive { + os_log("Opus解码器已经在运行", log: logger, type: .debug) + return true + } + + // 设置音频参数 + OpusUnit.opusSetSampleRate(Int32(sampleRate), kbps: 16000, channels: Int32(channels)) + + // 必须在后台线程启动解码器(SDK要求) + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + // 启动解码器 + let result = OpusUnit.opusDecoderRun() + if result == 0 { + os_log("Opus解码器启动成功: 采样率=%d Hz", log: self.logger, type: .info, sampleRate) + self.isDecodingActive = true + } else { + os_log("Opus解码器启动失败: 错误=%d", log: self.logger, type: .error, result) + } + } + + return true + } + + /// 停止Opus解码 + func stopDecoding() -> Bool { + if !isDecodingActive { + return false + } + + // 必须在后台线程停止解码器 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + let result = OpusUnit.opusDecoderStop() + os_log("Opus解码器停止: %d", log: self.logger, type: .info, result) + self.isDecodingActive = false + } + + return true + } + + /// 处理Opus编码数据 + func processAudioData(_ data: Data) { + if !isDecodingActive { + os_log("Opus解码器未启动", log: logger, type: .error) + return + } + + // 将Opus数据送入解码器 + OpusUnit.opusWrite(data) + // 解码结果将通过通知回调给onOpusDecodedData方法 + } + + /// 判断解码器是否运行中 + func isDecoding() -> Bool { + return isDecodingActive + } + + /// 清理资源 + func cleanup() { + stopDecoding() + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift index d4a3444b1..54f6a3ca5 100644 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift @@ -22,7 +22,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { // iOS 18环境下,先只初始化AccessorySetup - os_log("iOS 18环境: 延迟BleService初始化,先初始化AccessorySetup", log: logger, type: .info) let bleAccessorySetup = BleAccessorySetup.shared // 在accessoryAdded回调中延迟设置BleService @@ -48,10 +47,8 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { // 确保所有服务都已初始化 private func ensureServicesInitialized() { if !servicesInitialized { - os_log("按需初始化BleService和相关服务", log: logger, type: .info) - // 设置BLE服务回调 - BleService.shared.setDelegate(self) + BleService.shared.addCallback(self) // 设置BleService到AccessorySetup BleAccessorySetup.shared.setBleService(BleService.shared) @@ -62,10 +59,8 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { // 初始化所有服务(iOS 17及以下版本的标准行为) private func initializeAllServices() { - os_log("标准初始化所有服务", log: logger, type: .info) - // 设置BLE服务回调 - BleService.shared.setDelegate(self) + BleService.shared.addCallback(self) // 初始化AccessorySetup BleAccessorySetup.shared.setBleService(BleService.shared) @@ -92,24 +87,14 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // 检查是否需要初始化服务 + checkAndInitializeServices() + switch call.method { case "isConnected": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.isConnected()) case "getBatteryLevel": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif - // 获取电池信息,通过回调返回结果 if BleService.shared.getBatteryInfo() { result(0) // 通过回调返回实际值 @@ -120,7 +105,6 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } case "isCompanionSupported": - // iOS 18+ 支持 AccessorySetupKit #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { result(true) @@ -135,115 +119,44 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { // iOS上禁用AccessorySetupKit,始终返回无配对设备 result(nil) - case "registerBackgroundScan": - // iOS上禁用,直接返回成功以避免Flutter端报错 - result(true) - - case "unregisterBackgroundScan": - // iOS上禁用,直接返回成功以避免Flutter端报错 - result(true) - - case "clearAssociations": + case "registerBackgroundScan", "unregisterBackgroundScan", "clearAssociations": // iOS上禁用,直接返回成功以避免Flutter端报错 result(true) case "getDeviceVersion": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.getDeviceVersion()) case "getProductId": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.getProductId()) case "getBatteryInfo": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.getBatteryInfo()) case "openEncoder": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.openEncoder()) case "openDecoder": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.openDecoder()) case "openA2DPDecoder": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.openA2DPDecoder()) case "closeCodec": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.closeCodec()) case "playA2DP": - // 确保服务已初始化 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif result(BleService.shared.playA2DP()) // iOS 18+ AccessorySetupKit相关方法 case "startDeviceAssociation": #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - // 从主线程调用 - DispatchQueue.main.async { - // 获取当前活跃的视图控制器 - if let rootVC = UIApplication.shared.windows.first?.rootViewController { - let topVC = self.getTopViewController(rootVC) - - os_log("尝试从顶层视图控制器显示设备选择器: %{public}@", log: self.logger, type: .debug, String(describing: topVC)) - // 调用带UI上下文的方法 - BleAccessorySetup.shared.showDevicePickerFromViewController(topVC) - } else { - os_log("无法获取顶层视图控制器,使用安全方法", log: self.logger, type: .error) - BleAccessorySetup.shared.safeShowDevicePicker() - } - } + showDevicePicker() result(true) } else { - os_log("iOS版本低于18.0,不支持AccessorySetupKit", log: self.logger, type: .info) result(true) } #else - os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", log: self.logger, type: .info) result(true) #endif @@ -253,9 +166,35 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } // MARK: - 私有方法 + // 检查并按需初始化服务 + private func checkAndInitializeServices() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif + } + + // 显示设备选择器 + private func showDevicePicker() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + // 从主线程调用 + DispatchQueue.main.async { + // 获取当前活跃的视图控制器 + if let rootVC = UIApplication.shared.windows.first?.rootViewController { + let topVC = self.getTopViewController(rootVC) + BleAccessorySetup.shared.showDevicePickerFromViewController(topVC) + } else { + BleAccessorySetup.shared.safeShowDevicePicker() + } + } + } + #endif + } + // AccessorySetupKit发现设备的处理 private func handleDiscoveredDevice(uuid: UUID) { - // 通过AccessorySetupKit处理发现的设备 BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid) } @@ -371,6 +310,15 @@ extension SwiftBleServicePlugin: BleService.Callback { sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常") } + func onWakeupSignalReceived() { + let wakeupMap: [String: Any] = [ + "type": "wakeupSignal", + "timestamp": Date().timeIntervalSince1970 * 1000 + ] + + sendEvent(statusEventSink, data: wakeupMap, errorPrefix: "发送唤醒信号异常") + } + func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) { var dataMap: [String: Any] = [ "type": "deviceInfo", diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/OpusUnit.h b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/OpusUnit.h new file mode 100644 index 000000000..62170ee2a --- /dev/null +++ b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/OpusUnit.h @@ -0,0 +1,92 @@ +// +// OpusUnit.h +// QCY_Demo +// +// Created by 杰理科技 on 2021/7/5. +// Copyright © 2021 杰理科技. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * 通知:流式编码回调Opus数据 + * 通知数据类型:NSData(Opus) + */ +extern NSString *kOPUS_ENCODE_DATA; +/** + * 通知:流式解码回调PCM数据 + * 通知数据类型:NSData(PCM) + */ +extern NSString *kOPUS_DECODE_DATA; + + +@interface OpusUnit : NSObject + ++ (void)opusIsLog:(BOOL)log; + +#pragma mark - Opus参数 +/** + * 设置参数 + * @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000) + * @param kbps 比特率 例如:16000、32000、64000(默认 16000) + * @param channel 声道 例如:1、2 (默认 1) + */ ++(void)opusSetSampleRate:(int)rate Kbps:(int)kbps Channels:(int)channel; + +#pragma mark - Opus解码 + +/** + * 直接【opus文件】转换成【pcm文件】 + * @param path_opus opus文件路径 + * @param path_pcm pcm文件路径 + */ ++ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm; + +/** + * 流式解码【开启】 + */ ++ (int)opusDecoderRun; + +/** + * 输入Opus的数据 + * 通知监听“kOPUS_DECODE_DATA”获得解码后数据 + * 通知数据类型:NSNotification.object => NSData(PCM格式) + * @param data opus数据流(长度:1024) + */ ++ (void)opusWriteData:(NSData*)data; + +/** + * 流式解码【关闭】 + */ ++ (int)opusDecoderStop; + +#pragma mark - Opus编码 + +/** + * 【pcm文件】转换成【opus文件】 + */ ++ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus; + +/** + * 流式编码【开启】 + */ ++ (int)opusEecoderRun; + +/** + * 输入pcm的数据 + * 通知监听“kOPUS_DECODE_DATA”获得解码后数据 + * 通知数据类型:NSNotification.object => NSData(opus格式) + * @param data opus数据流(长度:1024) + */ ++ (void)pcmWriteData:(NSData*)data; + +/** + * 流式编码【关闭】 + */ ++ (int)opusEncoderStop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexKit.h b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexKit.h new file mode 100644 index 000000000..ce3ff3bd3 --- /dev/null +++ b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexKit.h @@ -0,0 +1,20 @@ +// +// SpeexKit.h +// SpeexKit +// +// Created by zhihui liang on 2018/8/20. +// Copyright © 2018年 www.zh-jieli.com. All rights reserved. +// + +#import + +//! Project version number for SpeexKit. +FOUNDATION_EXPORT double SpeexKitVersionNumber; + +//! Project version string for SpeexKit. +FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import +#import diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexUnit.h b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexUnit.h new file mode 100644 index 000000000..da7e93dae --- /dev/null +++ b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Headers/SpeexUnit.h @@ -0,0 +1,40 @@ +// +// SpeexUnit.h +// SpeexKit +// +// Created by zhihui liang on 2018/8/20. +// Copyright © 2018年 www.zh-jieli.com. All rights reserved. +// + +#import + +/** + * 流式解码回调PCM数据 + */ +extern NSString *kSPEEX_DECODE_DATA; //speex decode + +@interface SpeexUnit : NSObject + ++(void)speexIsLog:(BOOL)log; + +/** + * 直接【speex文件】转换成【pcm文件】 + */ ++(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm; + +/** + * 流式解码【开启】 + */ ++(int)speexDecoderRun; + +/** + * 输入Speex的数据 + */ ++(void)speexWriteData:(NSData*)data; + +/** + * 流式解码【关闭】 + */ ++(int)speexDecoderStop; + +@end diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Info.plist b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Info.plist new file mode 100644 index 000000000..a8aafb7d5 Binary files /dev/null and b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Info.plist differ diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Modules/module.modulemap b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Modules/module.modulemap new file mode 100644 index 000000000..7959a70f6 --- /dev/null +++ b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module SpeexKit { + umbrella header "SpeexKit.h" + + export * + module * { export * } +} diff --git a/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/SpeexKit b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/SpeexKit new file mode 100644 index 000000000..0f6a8beb4 Binary files /dev/null and b/local_plugins/ble_service/ios/Frameworks/SpeexKit.framework/SpeexKit differ diff --git a/local_plugins/ble_service/ios/ble_service.podspec b/local_plugins/ble_service/ios/ble_service.podspec index 2fc4d9c42..e36ad2d98 100644 --- a/local_plugins/ble_service/ios/ble_service.podspec +++ b/local_plugins/ble_service/ios/ble_service.podspec @@ -1,5 +1,6 @@ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint ble_service.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'ble_service' @@ -15,7 +16,14 @@ BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ Acc s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '18.0' + + # -- ① 把 SpeexKit 声明为“随包带来的二进制框架” -- + s.vendored_frameworks = 'Frameworks/SpeexKit.framework' # <- 或 .xcframework + # -- ② SpeexKit 可能依赖到的系统库 -- + s.frameworks = 'AVFoundation', 'AudioToolbox' + s.libraries = 'iconv', 'c++' # libc++/iconv多数 Opus/Speex 都要 + # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' diff --git a/local_plugins/ble_service/pubspec.yaml b/local_plugins/ble_service/pubspec.yaml index 47cf208c7..f1d7e117e 100644 --- a/local_plugins/ble_service/pubspec.yaml +++ b/local_plugins/ble_service/pubspec.yaml @@ -1,26 +1,72 @@ name: ble_service -description: BLE服务插件,提供蓝牙扫描、连接和通信功能 +description: 蓝牙低功耗服务插件,提供蓝牙设备连接、通信功能 version: 0.0.1 homepage: environment: - sdk: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" dependencies: flutter: sdk: flutter + # 添加其他依赖项 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.0 +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. plugin: platforms: android: package: com.yunqiinnovation.ble_service - pluginClass: BleServicePlugin + pluginClass: BleServicePlugin ios: - pluginClass: BleServicePlugin \ No newline at end of file + pluginClass: BleServicePlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages \ No newline at end of file