diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..fcad18058 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,798 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 185F55DBA6478EEF5FFF7E6A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */; }; + 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 */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 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 */; }; + CEB768CA2E7CAF03821CF421 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D6C5F44530DFFD81E47C128 /* Pods_Runner.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 */ + 096510A210F973A646E99AD8 /* 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 = ""; }; + 0D6C5F44530DFFD81E47C128 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 1F1F03E72E8BE0810058536B /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; + 2D13EECE4B6C62BD47C22D9D /* 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 = ""; }; + 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 = ""; }; + 52494E4695CE324CC6FA34BC /* 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 = ""; }; + 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 88C1945CFDFAE27F2AC121CE /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; 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 = ""; }; + 9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9FF5CAECB37ADCA2B74922A7 /* 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 = ""; }; + A20A85E4945A321D43982A29 /* 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 = ""; }; + F5F0D3DE8CC05490B05F5DA9 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + CEB768CA2E7CAF03821CF421 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA86459C72DB089ADF958E94 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 185F55DBA6478EEF5FFF7E6A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 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 */, + E39A6535EC9F01D38C488F0E /* Pods */, + 99D7B3F1955EBC6FC7EA7156 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 1F1F03E72E8BE0810058536B /* RunnerProfile.entitlements */, + 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 */, + 88C1945CFDFAE27F2AC121CE /* Runner.entitlements */, + ); + path = Runner; + sourceTree = ""; + }; + 99D7B3F1955EBC6FC7EA7156 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0D6C5F44530DFFD81E47C128 /* Pods_Runner.framework */, + 9C84B0B4E86F91BE0E2BE308 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E39A6535EC9F01D38C488F0E /* Pods */ = { + isa = PBXGroup; + children = ( + 2D13EECE4B6C62BD47C22D9D /* Pods-Runner.debug.xcconfig */, + A20A85E4945A321D43982A29 /* Pods-Runner.release.xcconfig */, + F5F0D3DE8CC05490B05F5DA9 /* Pods-Runner.profile.xcconfig */, + 52494E4695CE324CC6FA34BC /* Pods-RunnerTests.debug.xcconfig */, + 9FF5CAECB37ADCA2B74922A7 /* Pods-RunnerTests.release.xcconfig */, + 096510A210F973A646E99AD8 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 58E539742EB8665AA62B584E /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + FA86459C72DB089ADF958E94 /* 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 = ( + E8ABF8289F58977405D6531F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + DF981F3460DF1EA74F93EF68 /* [CP] Embed Pods Frameworks */, + B8F8941054DC9B48433EDB23 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + 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; + LastSwiftUpdateCheck = 1640; + 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; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + 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 */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 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"; + }; + 58E539742EB8665AA62B584E /* [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; + }; + 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"; + }; + B8F8941054DC9B48433EDB23 /* [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; + }; + DF981F3460DF1EA74F93EF68 /* [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; + }; + E8ABF8289F58977405D6531F /* [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; + }; +/* 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_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 = 13.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/RunnerProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 537SFY28XS; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 52494E4695CE324CC6FA34BC /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = K39FB49XKY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.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 = 9FF5CAECB37ADCA2B74922A7 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = K39FB49XKY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.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 = 096510A210F973A646E99AD8 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = K39FB49XKY; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans.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_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 = 13.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_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 = 13.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; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K39FB49XKY; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = K39FB49XKY; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index edce3635e..749992807 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -54,7 +54,6 @@ voitrans - CFBundleURLName deepvoiceclient.payment @@ -74,10 +73,12 @@ FacebookClientToken 193d0a2b1757c41cfa557ea8e8f21c33 + ITSAppUsesNonExemptEncryption + LSApplicationQueriesSchemes - App-Prefs - prefs + App-Prefs + prefs qqmusic weixin weixinULAPI @@ -98,7 +99,7 @@ fbauth2 fbshareextension google.navigation - googlemaps + googlemaps comgooglemaps baidumap amapuri @@ -114,7 +115,6 @@ NSAllowsArbitraryLoadsInWebContent - NSExceptionDomains music.126.net @@ -160,10 +160,10 @@ Bluetooth permission is required to connect and manage audio devices NSBluetoothPeripheralUsageDescription Bluetooth permission is required to connect and manage audio devices - NSCalendarsUsageDescription - Calendar permission is required to schedule and manage your audio meetings and recording tasks NSCalendarsFullAccessUsageDescription Calendar permission is required to schedule and manage your audio meetings and recording tasks + NSCalendarsUsageDescription + Calendar permission is required to schedule and manage your audio meetings and recording tasks NSCameraUsageDescription Camera permission is required for QR code scanning and video calling functions NSContactsUsageDescription @@ -178,12 +178,12 @@ This app uses motion and fitness data to detect when you're walking or moving, which helps optimize voice recognition accuracy and automatically adjust microphone sensitivity for better audio recording quality during different activities. NSPhotoLibraryUsageDescription Photo library permission is required to save recording files and import audio files + NSRemindersUsageDescription + This app needs access to reminders to set calendar reminders NSUserTrackingUsageDescription This identifier will be used to deliver personalized ads to you. SKAdNetworkItems - ITSAppUsesNonExemptEncryption - UIApplicationSupportsIndirectInputEvents UIBackgroundModes @@ -196,8 +196,6 @@ 1 - NSRemindersUsageDescription - This app needs access to reminders to set calendar reminders UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index a03772888..b13da804d 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.sync.withLock import com.yunqiinnovation.azure_speech.tools.RecordFile import com.example.astclient.DoubaoE2ETranslateHelper import com.example.astclient.Config +import java.util.concurrent.atomic.AtomicInteger /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, @@ -101,6 +102,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, private var astWsConfig: Config? = null private var currentAstProvider: String = "azure" + // AST 初始化世代号:每次 dispose / initialize 时自增,被 dispatch 出去的 Main 协程 + // 在执行前先比对,发现已经过时就直接 return,避免 1.2s delay 后落到旧 provider 上。 + private val astInitGeneration = AtomicInteger(0) private var iflytekAStarted: Boolean = false private var iflytekBStarted: Boolean = false @@ -132,6 +136,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, private var captureQuality: Int = 80 private val REQUEST_SCREEN_CAPTURE = 9001 + private fun nextAstInitGeneration(): Int = astInitGeneration.incrementAndGet() + private fun isAstInitGenerationCurrent(generation: Int): Boolean = + astInitGeneration.get() == generation + /** * 推送音频到当前选择的 AST 服务 A * @@ -1136,6 +1144,8 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, "dispose" -> { try { + // 让所有等待 delay(1200L) 的旧 init 协程在恢复时直接 return,避免落到已 dispose 的 helper 上 + nextAstInitGeneration() FileLogger.d(tag, "释放双向 AST 资源(异步)") callBPcmPlayer.stop() GlobalScope.launch(Dispatchers.IO) { @@ -1207,6 +1217,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, } "initialize" -> { + val initGeneration = nextAstInitGeneration() val provider = call.argument("provider") ?: "" val supportedLanguages = call.argument>("supportedLanguages") ?: listOf("zh-CN") @@ -1301,6 +1312,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, ) GlobalScope.launch(Dispatchers.Main) { + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } val initA = iflytekAstHelperA?.initialize( azureConfig = azureConfig, translationConfig = translationConfig, @@ -1309,6 +1323,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, ) ?: false delay(1200L) + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } val initB = iflytekAstHelperB?.initialize( azureConfig = azureConfig, @@ -1364,6 +1381,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, ) GlobalScope.launch(Dispatchers.Main) { + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } azureAstHelperA?.initialize( azureConfig = azureConfig, translationConfig = translationConfig, @@ -1371,6 +1391,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, callback = callbackA ) delay(1200L) + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } azureAstHelperB?.initialize( azureConfig = azureConfig, translationConfig = translationConfig, @@ -1419,8 +1442,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, ) GlobalScope.launch(Dispatchers.Main) { + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } doubaoAstHelperA?.initialize(cfgA, callbackA) delay(1200L) + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } doubaoAstHelperB?.initialize(cfgB, callbackB) result.success(true) } @@ -1462,8 +1491,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware, ) GlobalScope.launch(Dispatchers.Main) { + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } val initA = bailianAstHelperA?.initialize(bailianConfigA, callbackA) ?: false delay(1200L) + if (!isAstInitGenerationCurrent(initGeneration)) { + result.success(false); return@launch + } val initB = bailianAstHelperB?.initialize(bailianConfigB, callbackB) ?: false result.success(initA && initB) } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt index 6caa262d8..7f5c9a2bd 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/DoubaoE2ETranslateHelper.kt @@ -11,6 +11,7 @@ import java.io.InputStream import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.* import data.speech.ast.TranslateRequest import data.speech.ast.TranslateResponse @@ -61,6 +62,17 @@ class DoubaoE2ETranslateHelper( private var webSocket: WebSocket? = null private var sessionId: String = "" private val sessionLock = Any() + // 同一 helper 实例内多次重连/语言切换的世代号;onOpen/onMessage/onFailure/onClosed + // 拿到 generation 与当前 generation 比较,丢弃旧 listener 的回调,避免它们清掉新 ws 状态。 + private val connectionGeneration = AtomicInteger(0) + // 切换瞬间(旧 ws 已关、新 ws 还没 onOpen)暂存上行音频,避免首段语音被丢, + // 等服务端真正下发首条消息(确认 session 已就绪)后再 flush 进 pump 缓冲。 + private val pendingAudioData = ArrayDeque() + private val maxPendingAudioChunks = 120 + private val hasServerReadyEvent = AtomicBoolean(false) + private val hasFlushedPendingAfterOpen = AtomicBoolean(false) + private val warmupSilentChunk = ByteArray(640) + private val isDisposed = AtomicBoolean(false) private val recvAudio = ByteArrayOutputStream() private val recvText = mutableListOf() @@ -90,12 +102,13 @@ class DoubaoE2ETranslateHelper( fun initialize(config: Config, cb: Callback): Boolean { instanceTag = "${config.sourceLanguage}->${config.targetLanguage}" Log.d(TAG, "[$instanceTag] initialize: wsUrl=${config.wsUrl}, resourceId=${config.resourceId}") - // 重置状态,确保重新初始化时能正常启动 - isStarted.set(false) - try { webSocket?.close(1000, "reinitialize") } catch (_: Exception) {} - webSocket = null + // 重新初始化时保留切换窗口内已经入队的音频,避免延迟 init 导致首段语音丢失 + closeSocketAndResetState("reinitialize", 1000, clearPending = false) + client?.dispatcher?.cancelAll() + client?.connectionPool?.evictAll() conf = config callback = cb + isDisposed.set(false) client = OkHttpClient.Builder() .pingInterval(30, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS) @@ -109,15 +122,18 @@ class DoubaoE2ETranslateHelper( * 重启会话(用于超时自动重连) */ private fun restartSession() { + if (isDisposed.get()) { + Log.i(TAG, "restartSession: already disposed, skip") + return + } Log.i(TAG, "restartSession: performing auto-restart...") - stopAudioPump() - try { webSocket?.close(1000, "restarting") } catch (_: Exception) {} - webSocket = null - isStarted.set(false) - + // 自动重连:保留待发音频,等新 session ready 后再 flush + closeSocketAndResetState("restarting", 1000, clearPending = false) + try { java.util.Timer().schedule(object : java.util.TimerTask() { override fun run() { + if (isDisposed.get()) return Log.i(TAG, "restartSession: timer task running") if (!isStarted.get()) { Log.i(TAG, "restartSession: calling startContinuousTranslation") @@ -157,6 +173,7 @@ class DoubaoE2ETranslateHelper( .header("X-Api-Connect-Id", connId) .build() + val listenerGeneration = connectionGeneration.incrementAndGet() val listener = object : WebSocketListener() { /** * WebSocket 连接建立时回调。 @@ -167,12 +184,20 @@ class DoubaoE2ETranslateHelper( * @param response 握手响应,包含状态码与头信息 */ override fun onOpen(ws: WebSocket, response: Response) { + if (listenerGeneration != connectionGeneration.get()) { + Log.i(TAG, "onOpen: stale listener (gen=$listenerGeneration cur=${connectionGeneration.get()}), closing") + try { ws.close(1000, "stale-open") } catch (_: Exception) {} + return + } Log.d(TAG, "[$instanceTag] onOpen: code=${response.code} logid=${response.header("X-Tt-Logid")}") webSocket = ws + isStarted.set(true) + hasServerReadyEvent.set(false) + hasFlushedPendingAfterOpen.set(false) val startReq = makeStartRequest(sessionId) ws.send(ByteString.of(*startReq.toByteArray())) Log.d(TAG, "onOpen: StartSession sent") - isStarted.set(true) + sendWarmupPacket(ws) startAudioPump() callback?.onSessionStarted(sessionId) } @@ -189,6 +214,15 @@ class DoubaoE2ETranslateHelper( * @param bytes 服务端下发的二进制消息 */ override fun onMessage(ws: WebSocket, bytes: ByteString) { + if (ws !== webSocket) { + Log.i(TAG, "onMessage: ignore stale websocket callback") + return + } + if (hasServerReadyEvent.compareAndSet(false, true) && + hasFlushedPendingAfterOpen.compareAndSet(false, true) + ) { + flushPendingAudioToBuffer() + } Log.d(TAG, "[$instanceTag] onMessage: ${bytes.size} bytes") val resp = TranslateResponse.parseFrom(bytes.toByteArray()) val event = resp.event @@ -287,8 +321,13 @@ class DoubaoE2ETranslateHelper( * @param response 可选的响应对象(可能为 null) */ override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { + if (ws !== webSocket) { + Log.i(TAG, "onFailure: ignore stale websocket callback") + return + } val msg = t.message ?: "unknown" Log.e(TAG, "onFailure: ${msg} code=${response?.code}") + markDisconnected(ws) callback?.onSessionError(sessionId, 1011, msg) } @@ -301,10 +340,13 @@ class DoubaoE2ETranslateHelper( * @param reason 关闭原因描述 */ override fun onClosed(ws: WebSocket, code: Int, reason: String) { + if (ws !== webSocket) { + Log.i(TAG, "onClosed: ignore stale websocket callback") + return + } Log.d(TAG, "onClosed: code=${code} reason=${reason}") - stopAudioPump() try { recvAudio.close() } catch (_: Exception) {} - isStarted.set(false) + markDisconnected(ws) } } @@ -356,13 +398,21 @@ class DoubaoE2ETranslateHelper( fun pushAudioData(data: ByteArray): Boolean { pushCount++ if (pushCount % 100 == 1L) { - Log.d(TAG, "pushAudioData: size=${data.size} isStarted=${isStarted.get()} wsIsNull=${webSocket==null} count=$pushCount") + Log.d(TAG, "pushAudioData: size=${data.size} isStarted=${isStarted.get()} wsIsNull=${webSocket==null} count=$pushCount pending=${pendingAudioData.size}") } - if (!isStarted.get()) return false - if (data.isNotEmpty()) { - synchronized(audioSendBufferLock) { - audioSendBuffer += data + if (data.isEmpty()) return true + // 切换/重连窗口:ws 还没就绪 → 暂存到 pendingAudioData,等 onMessage 里第一条服务端消息到达再 flush + if (!isStarted.get() || webSocket == null) { + synchronized(sessionLock) { + while (pendingAudioData.size >= maxPendingAudioChunks) { + pendingAudioData.removeFirst() + } + pendingAudioData.addLast(data) } + return true + } + synchronized(audioSendBufferLock) { + audioSendBuffer += data } return true } @@ -436,16 +486,78 @@ class DoubaoE2ETranslateHelper( /** * 释放资源,关闭会话。 + * 注意:plugin 持有 helper 单例并在切换语言时反复 initialize/dispose,因此 dispose **不能** + * 调 scope.cancel() —— SupervisorJob 一旦 cancel 永远不可重启,下次 initialize 后再 launch + * 出来的 pumpJob 立刻是 cancelled 状态,body 永不执行,导致服务端 [Timeout waiting next packet]。 + * 这里只关 ws、清状态、停 pump。如果将来真要彻底销毁实例,请显式调用 finalDispose()。 */ fun dispose() { Log.d(TAG, "dispose: begin") + isDisposed.set(true) + closeSocketAndResetState("dispose", 1000, clearPending = true) + try { recvAudio.close() } catch (_: Exception) {} + client?.dispatcher?.cancelAll() + client?.connectionPool?.evictAll() + Log.d(TAG, "dispose: done (scope kept alive for re-initialize)") + } + + /** + * 关闭当前 ws 并重置状态。clearPending=false 时保留 pendingAudioData, + * 用于 reinitialize / restartSession 这类期望延续上行音频的场景。 + */ + private fun closeSocketAndResetState(reason: String, code: Int, clearPending: Boolean) { stopAudioPump() - try { webSocket?.close(1000, "dispose") } catch (_: Exception) {} + try { webSocket?.close(code, reason) } catch (_: Exception) {} webSocket = null isStarted.set(false) - try { recvAudio.close() } catch (_: Exception) {} - scope.cancel() - Log.d(TAG, "dispose: done") + hasServerReadyEvent.set(false) + hasFlushedPendingAfterOpen.set(false) + if (clearPending) { + synchronized(sessionLock) { pendingAudioData.clear() } + } + } + + /** + * onClosed/onFailure 调用:仅当当前 ws 引用就是回调里那个 ws 时才清空。 + * 防止旧 ws 的 onClosed 把新 ws 状态清掉。 + */ + private fun markDisconnected(ws: WebSocket) { + if (webSocket === ws) { + webSocket = null + isStarted.set(false) + } + } + + /** + * 把 pendingAudioData 一次性灌入 pump 的 audioSendBuffer,让后续 20ms 节拍发送。 + */ + private fun flushPendingAudioToBuffer() { + val pending: List + synchronized(sessionLock) { + if (pendingAudioData.isEmpty()) return + pending = pendingAudioData.toList() + pendingAudioData.clear() + } + Log.d(TAG, "flushPendingAudioToBuffer: count=${pending.size}") + synchronized(audioSendBufferLock) { + for (chunk in pending) { + audioSendBuffer += chunk + } + } + } + + /** + * onOpen 后立即先发一帧静音,作为服务端"已收到客户端首包"的探测, + * 防止首条上行被服务端 stream timeout([Timeout waiting next packet])。 + */ + private fun sendWarmupPacket(ws: WebSocket) { + try { + val req = makeChunkRequest(sessionId, warmupSilentChunk) + val ok = ws.send(ByteString.of(*req.toByteArray())) + Log.d(TAG, "sendWarmupPacket: sent=$ok size=${warmupSilentChunk.size}") + } catch (e: Exception) { + Log.w(TAG, "sendWarmupPacket: failed", e) + } } /** diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 935fc40e7..ec7560fd8 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -103,6 +103,19 @@ import ble_service private var bailianAstHelperA = AliyunBailianE2EHelper() private var bailianAstHelperB = AliyunBailianE2EHelper() private var currentAstProvider: String = "azure" + // AST 初始化世代号:每次 dispose / initialize 自增;异步任务在恢复时比对,过期就 return, + // 避免新 provider 已切换后旧 init 的延迟代码再落到当前 helper 上。 + private var astInitGeneration: Int = 0 + private let astInitGenerationLock = NSLock() + private func nextAstInitGeneration() -> Int { + astInitGenerationLock.lock(); defer { astInitGenerationLock.unlock() } + astInitGeneration += 1 + return astInitGeneration + } + private func isAstInitGenerationCurrent(_ generation: Int) -> Bool { + astInitGenerationLock.lock(); defer { astInitGenerationLock.unlock() } + return astInitGeneration == generation + } private var astWsConfig: DoubaoE2ETranslateHelper.Config? internal var doubaoFinalSourceTextCache: [String: String] = [:] private var doubaoCallbackA: DoubaoCallbackProxy? @@ -863,6 +876,8 @@ private func sendAudioDataEvent(_ event: [String: Any]) { result(true) case "dispose": + // 让 init 的 Task 在 sleep 之后比对 generation 时直接 return,避免落到已 dispose 的 helper 上 + _ = nextAstInitGeneration() os_log("[AST-DISPOSE] Releasing all AST resources, provider=%{public}@", log: ctLog, type: .info, currentAstProvider) azureAstHelperA.dispose() azureAstHelperB.dispose() @@ -893,6 +908,7 @@ private func sendAudioDataEvent(_ event: [String: Any]) { result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing arguments", details: nil)) return } + let initGeneration = nextAstInitGeneration() let provider = (args["provider"] as? String ?? "azure").lowercased() let supported = (args["supportedLanguages"] as? [String]) ?? ["zh-CN"] let lang0 = supported.count > 0 ? supported[0] : "zh-CN" @@ -963,10 +979,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) { let translationConfigI = TranslationConfiguration(region: azureTranslationRegion, subscriptionKey: azureTranslationKey, location: azureTranslationRegion) Task { + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } os_log("[AST-INIT] Azure: initializing A service...", log: self.ctLog, type: .info) await azureAstHelperA.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcA, callback: callbackA) os_log("[AST-INIT] Azure: A initialized, waiting 1.2s before B...", log: self.ctLog, type: .info) try? await Task.sleep(nanoseconds: 1_200_000_000) + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } os_log("[AST-INIT] Azure: initializing B service...", log: self.ctLog, type: .info) await azureAstHelperB.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcB, callback: callbackB) os_log("[AST-INIT] Azure: A+B both initialized", log: self.ctLog, type: .info) @@ -990,10 +1014,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) { cfgB.targetLanguage = translationLang0 Task { + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } os_log("[AST-INIT] Volcano: initializing A service...", log: self.ctLog, type: .info) let rA = await doubaoAstHelperA.initialize(config: cfgA, cb: cbA) os_log("[AST-INIT] Volcano: A service result=%d, sleeping 500ms before B...", log: self.ctLog, type: .info, rA ? 1 : 0) try? await Task.sleep(nanoseconds: 500_000_000) + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } os_log("[AST-INIT] Volcano: initializing B service...", log: self.ctLog, type: .info) let rB = await doubaoAstHelperB.initialize(config: cfgB, cb: cbB) os_log("[AST-INIT] Volcano: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0) @@ -1027,9 +1059,17 @@ private func sendAudioDataEvent(_ event: [String: Any]) { bailianCallbackB = cbB Task { + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } os_log("[AST-INIT] Alibaba: initializing A service...", log: self.ctLog, type: .info) let rA = await bailianAstHelperA.initialize(config: cfgA, cb: cbA) os_log("[AST-INIT] Alibaba: A service result=%d, initializing B service...", log: self.ctLog, type: .info, rA ? 1 : 0) + if !self.isAstInitGenerationCurrent(initGeneration) { + DispatchQueue.main.async { result(false) } + return + } let rB = await bailianAstHelperB.initialize(config: cfgB, cb: cbB) os_log("[AST-INIT] Alibaba: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0) DispatchQueue.main.async { result(true) } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift index 615628189..6bbf935f7 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/DoubaoE2ETranslateHelper.swift @@ -53,9 +53,15 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { private var audioChunkBuffer = Data() private var isStarted = false + private var isDisposed = false private let lock = NSLock() private var isWebSocketConnected = false + // 切换/重连窗口内(urlSession 替换、新 ws 还没 didOpen)暂存上行音频, + // 等 onMessage 收到第一条服务端消息再 flush 到 pump 缓冲。 + private var pendingAudioData: [Data] = [] + private let maxPendingAudioChunks = 120 + private var hasFlushedPendingAfterOpen = false // 上行音频 pump: // 会话建立后启动一个 20ms 固定节拍的定时器,持续从 audioSendBuffer 取 640B 一帧上行; @@ -74,25 +80,34 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { * 返回是否成功触发了连接逻辑 */ func initialize(config: Config, cb: Callback) async -> Bool { - // 清理旧的 Session + // 重新初始化:保留 pendingAudioData,等新 ws 起来后 flush;只清状态/旧 session + stopAudioPump() + webSocket?.cancel(with: .normalClosure, reason: "reinitialize".data(using: .utf8)) + webSocket = nil + isStarted = false + lock.lock() + isWebSocketConnected = false + hasFlushedPendingAfterOpen = false + lock.unlock() if let session = urlSession { session.invalidateAndCancel() } - + os_log("Initialize with wsUrl: %{public}@", log: log, type: .info, config.wsUrl) conf = config // 去除可能的空白字符 conf.appKey = conf.appKey.trimmingCharacters(in: .whitespacesAndNewlines) conf.accessKey = conf.accessKey.trimmingCharacters(in: .whitespacesAndNewlines) conf.resourceId = conf.resourceId.trimmingCharacters(in: .whitespacesAndNewlines) - + callback = cb + isDisposed = false let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = 0 // 恢复为0(无超时),因为 WebSocket 是长连接 configuration.timeoutIntervalForResource = 0 // 使用主队列,方便调试且避免并发问题 urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) - + // 等待连接建立(简单的延迟模拟,或者通过回调通知) // 这里仅启动连接,实际连接结果是异步的 return startContinuousTranslation() @@ -102,6 +117,10 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { * 重启会话(用于超时自动重连) */ private func restartSession() { + if isDisposed { + os_log("restartSession: already disposed, skip", log: log, type: .info) + return + } os_log("Restarting session due to timeout...", log: log, type: .info) stopAudioPump() @@ -110,10 +129,12 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { isStarted = false lock.lock() isWebSocketConnected = false + hasFlushedPendingAfterOpen = false + // 保留 pendingAudioData:自动重连仍想把切换瞬间的麦克风音频送出 lock.unlock() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in - guard let self = self else { return } + guard let self = self, !self.isDisposed else { return } if !self.isStarted { _ = self.startContinuousTranslation() } @@ -177,20 +198,80 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { /** * 只负责把外部音频塞进上行缓冲区,真正的发送由 pump 按 20ms 节拍统一调度。 + * 切换/重连窗口(isStarted=false 或 ws 还没建好):先入 pendingAudioData, + * 等首条服务端消息到达再 flush 进 audioSendBuffer。 */ func pushAudioData(_ data: Data) -> Bool { - guard isStarted else { - os_log("Push audio failed: not started", log: log, type: .error) - return false - } - if !data.isEmpty { - audioSendBufferLock.lock() - audioSendBuffer.append(data) - audioSendBufferLock.unlock() + if isDisposed { return false } + if data.isEmpty { return true } + if !isStarted || !isWebSocketConnected { + lock.lock() + while pendingAudioData.count >= maxPendingAudioChunks { + pendingAudioData.removeFirst() + } + pendingAudioData.append(data) + lock.unlock() + return true } + audioSendBufferLock.lock() + audioSendBuffer.append(data) + audioSendBufferLock.unlock() return true } + /** + * 把 pendingAudioData 一次性灌入 audioSendBuffer,让 pump 按 20ms 节拍消费。 + */ + private func flushPendingAudioToBuffer() { + var pending: [Data] = [] + lock.lock() + if hasFlushedPendingAfterOpen || pendingAudioData.isEmpty { + lock.unlock() + return + } + hasFlushedPendingAfterOpen = true + pending = pendingAudioData + pendingAudioData.removeAll() + lock.unlock() + os_log("flushPendingAudioToBuffer: count=%{public}d", log: log, type: .info, pending.count) + audioSendBufferLock.lock() + for chunk in pending { + audioSendBuffer.append(chunk) + } + audioSendBufferLock.unlock() + } + + /** + * onOpen 后立即先发一帧静音,作为服务端"已收到客户端首包"的探测, + * 防止服务端 stream timeout([Timeout waiting next packet])。 + */ + private func sendWarmupPacket(_ ws: URLSessionWebSocketTask) { + let req = makeChunkRequest(sessionId: sessionId, chunk: silentFrame) + guard let msg = try? req.serializedData() else { return } + ws.send(.data(msg)) { [weak self] error in + if let e = error { + os_log("sendWarmupPacket failed: %{public}@", log: self?.log ?? .default, type: .info, e.localizedDescription) + } + } + } + + /** + * 仅当当前 ws 引用就是回调里那个 ws 时才清空,避免旧 ws 的 close 把新 ws 状态清掉。 + */ + private func markDisconnected(currentWs: URLSessionWebSocketTask) { + lock.lock() + let isCurrent = (webSocket === currentWs) + if isCurrent { + webSocket = nil + isWebSocketConnected = false + } + lock.unlock() + if isCurrent { + isStarted = false + stopAudioPump() + } + } + /** * 从上行缓冲区取一帧 20ms 数据;不够则返回静音帧。 */ @@ -271,16 +352,24 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { } /** - * 释放资源 + * 释放资源。 + * 注意:对齐 Android 行为——plugin 持有 helper 单例并在切换语言时反复 initialize/dispose, + * 因此这里**只清状态**,不能让 helper 进入"永久不可用"状态: + * - pumpTimer/urlSession 都是按需重建的(`startAudioPump`/`initialize` 会重新创建),不需要 deinit pumpQueue + * - isDisposed=true 只在"dispose 之后到下次 initialize 之前"这段窗口生效, + * 用于让在飞的 restartSession timer 跳过;initialize 会把 isDisposed 重置为 false */ func dispose() { - os_log("Disposing resources", log: log, type: .info) + os_log("Disposing resources (instance reusable after re-initialize)", log: log, type: .info) + isDisposed = true stopAudioPump() - do { try webSocket?.cancel(with: .normalClosure, reason: "dispose".data(using: .utf8)) } catch { } + webSocket?.cancel(with: .normalClosure, reason: "dispose".data(using: .utf8)) webSocket = nil isStarted = false lock.lock() isWebSocketConnected = false + pendingAudioData.removeAll() + hasFlushedPendingAfterOpen = false lock.unlock() urlSession?.invalidateAndCancel() urlSession = nil @@ -290,6 +379,10 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { * WebSocket打开回调(发送 Protobuf StartSession) */ func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + guard session === urlSession, webSocketTask === webSocket else { + os_log("Ignore didOpen for stale URLSession/WebSocket", log: log, type: .info) + return + } os_log("WebSocket didOpen", log: log, type: .info) let startReq = makeStartRequest(sessionId: sessionId) if let data = try? startReq.serializedData() { @@ -303,9 +396,11 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { } } } + sendWarmupPacket(webSocketTask) lock.lock() isWebSocketConnected = true + hasFlushedPendingAfterOpen = false lock.unlock() startAudioPump() @@ -318,28 +413,35 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { * WebSocket关闭回调 */ func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + guard session === urlSession else { + os_log("Ignore didClose for stale URLSession", log: log, type: .info) + return + } let reasonStr = String(data: reason ?? Data(), encoding: .utf8) ?? "" os_log("WebSocket didClose, code: %d, reason: %{public}@", log: log, type: .info, closeCode.rawValue, reasonStr) - stopAudioPump() - lock.lock() - isWebSocketConnected = false - lock.unlock() - isStarted = false + markDisconnected(currentWs: webSocketTask) } /** * 任务完成回调(处理错误) */ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + // initialize 会 invalidate 旧 session 并新建;旧 task 的完成回调必须忽略,否则会误伤当前连接 + guard session === urlSession else { + return + } + if let wsTask = task as? URLSessionWebSocketTask, wsTask !== webSocket { + return + } if let e = error { os_log("WebSocket task failed: %{public}@", log: log, type: .error, e.localizedDescription) callback?.onSessionError(sessionId: sessionId, code: 1012, message: e.localizedDescription) - stopAudioPump() - - lock.lock() - isWebSocketConnected = false - lock.unlock() - isStarted = false + if let wsTask = task as? URLSessionWebSocketTask { + markDisconnected(currentWs: wsTask) + } else { + stopAudioPump() + isStarted = false + } } } @@ -347,12 +449,20 @@ class DoubaoE2ETranslateHelper: NSObject, URLSessionWebSocketDelegate { * 接收消息循环 */ private func receiveLoop() { - webSocket?.receive { [weak self] result in + guard let receivingWs = webSocket else { return } + receivingWs.receive { [weak self] result in guard let self = self else { return } + // 语言切换 / 重连时旧 ws 的 receive 可能晚于新 ws 建立;忽略 stale 回调,否则会误关新连接 + guard self.webSocket === receivingWs else { + os_log("Ignore receive for stale WebSocket", log: self.log, type: .info) + return + } switch result { case .failure(let error): self.callback?.onSessionError(sessionId: self.sessionId, code: 1011, message: error.localizedDescription) case .success(let message): + // 首条服务端消息抵达即认为 session 真正就绪,把切换瞬间缓冲的 pending 灌给 pump + self.flushPendingAudioToBuffer() self.handleMessage(message) self.receiveLoop() }