diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fbeca2d..d8f4363 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,11 +5,23 @@ PODS: - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS + - flutter_sound (9.30.0): + - Flutter + - flutter_sound_core (= 9.30.0) + - flutter_sound_core (9.30.0) + - permission_handler_apple (9.3.0): + - Flutter DEPENDENCIES: - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - Flutter (from `Flutter`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + +SPEC REPOS: + trunk: + - flutter_sound_core EXTERNAL SOURCES: device_calendar: @@ -18,12 +30,19 @@ EXTERNAL SOURCES: :path: Flutter flutter_blue_plus_darwin: :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_sound: + :path: ".symlinks/plugins/flutter_sound/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_sound: d95194f6476c9ad211d22b3a414d852c12c7ca44 + flutter_sound_core: 7f2626d249d3a57bfa6da892ef7e22d234482c1a + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d -PODFILE CHECKSUM: 8c4d30c2258325612f2b7276ac7aac1f617fcf63 +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6413778..665f40d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 11042EE6E6B7D9E6B3203B5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */; }; + 13B07DAAD2277EC02485444C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */; }; 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 */; }; @@ -14,8 +16,6 @@ 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 */; }; - 99F5015DBD6F6F67074A1B9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */; }; - F8A02A1E8E6684C1D8450DF1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,13 +46,15 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; 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; }; + 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 61CA3FFAF8421699CD486D19 /* 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 = ""; }; + 4AF0703E326EC888C79A6934 /* 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 = ""; }; + 6F703DB2DF8D00352F539BEF /* 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 = ""; }; 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 = ""; }; + 75606A58A2724B2236273247 /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 807E81FEBDA1B3C207D2F02D /* 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 = ""; }; - 96D8DD3C040343A039F310FE /* 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 = ""; }; + 80B8CDD639846BDA7DEBA175 /* 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 = ""; }; 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; }; @@ -60,19 +62,17 @@ 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 = ""; }; - A66E4728E7B9DA7D3BF9212D /* 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 = ""; }; - A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E805B90C1540E73EE0890817 /* 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 = ""; }; - E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F06CDC837EF05FDD4CA99461 /* 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 = ""; }; + C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E3CB0A10B61A6DA72D29435D /* 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 = ""; }; + F85674E7919C9C6910F5C52A /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 7A60E1A3672AB68E0E2A42D1 /* Frameworks */ = { + 1C1F3D585EBC7A9D5F841A4B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F8A02A1E8E6684C1D8450DF1 /* Pods_RunnerTests.framework in Frameworks */, + 13B07DAAD2277EC02485444C /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 99F5015DBD6F6F67074A1B9D /* Pods_Runner.framework in Frameworks */, + 11042EE6E6B7D9E6B3203B5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,6 +95,28 @@ path = RunnerTests; sourceTree = ""; }; + 3A67259495ADFC50EA787FF3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */, + C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 820C0A71E31DDDCBE4781028 /* Pods */ = { + isa = PBXGroup; + children = ( + 80B8CDD639846BDA7DEBA175 /* Pods-Runner.debug.xcconfig */, + 4AF0703E326EC888C79A6934 /* Pods-Runner.release.xcconfig */, + 75606A58A2724B2236273247 /* Pods-Runner.profile.xcconfig */, + E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */, + F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */, + 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -113,8 +135,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - F0CFB2D7AEEBFE525929A1A9 /* Pods */, - CAAD90FF3B44AD28E7C94A7C /* Frameworks */, + 820C0A71E31DDDCBE4781028 /* Pods */, + 3A67259495ADFC50EA787FF3 /* Frameworks */, ); sourceTree = ""; }; @@ -142,29 +164,6 @@ path = Runner; sourceTree = ""; }; - CAAD90FF3B44AD28E7C94A7C /* Frameworks */ = { - isa = PBXGroup; - children = ( - A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */, - E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - F0CFB2D7AEEBFE525929A1A9 /* Pods */ = { - isa = PBXGroup; - children = ( - F06CDC837EF05FDD4CA99461 /* Pods-Runner.debug.xcconfig */, - A66E4728E7B9DA7D3BF9212D /* Pods-Runner.release.xcconfig */, - 61CA3FFAF8421699CD486D19 /* Pods-Runner.profile.xcconfig */, - 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */, - E805B90C1540E73EE0890817 /* Pods-RunnerTests.release.xcconfig */, - 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,10 +171,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - FEF6B413DA9FC937656B660D /* [CP] Check Pods Manifest.lock */, + 30194C4A061055FA00E6E695 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, - 7A60E1A3672AB68E0E2A42D1 /* Frameworks */, + 1C1F3D585EBC7A9D5F841A4B /* Frameworks */, ); buildRules = ( ); @@ -191,14 +190,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - BEC52B047FD349E6B05F8651 /* [CP] Check Pods Manifest.lock */, + 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 62220B443F82F6667CD7939F /* [CP] Embed Pods Frameworks */, + E7F491FBE75B21AAC1960BCF /* [CP] Embed Pods Frameworks */, + 5930E5E9A4A35EF8EAF80E24 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -270,96 +270,113 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + 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; }; - 62220B443F82F6667CD7939F /* [CP] Embed Pods Frameworks */ = { + 30194C4A061055FA00E6E695 /* [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-RunnerTests-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; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "Run Script"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - BEC52B047FD349E6B05F8651 /* [CP] Check Pods Manifest.lock */ = { + 5930E5E9A4A35EF8EAF80E24 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + 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; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( ); + inputPaths = ( + ); + name = "Run Script"; 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; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - FEF6B413DA9FC937656B660D /* [CP] Check Pods Manifest.lock */ = { + E7F491FBE75B21AAC1960BCF /* [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-RunnerTests-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; }; /* End PBXShellScriptBuildPhase section */ @@ -471,14 +488,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -488,7 +504,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -506,7 +522,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E805B90C1540E73EE0890817 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -522,7 +538,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -654,14 +670,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -677,14 +692,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties"; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..c30b367 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 010caf8..2885430 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,27 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + NSCalendarsFullAccessUsageDescription Access most functions for calendar viewing. UIApplicationSupportsIndirectInputEvents @@ -47,5 +68,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSBluetoothAlwaysUsageDescription + This app uses Bluetooth to discover and connect to Even Realities G1 glasses. + NSMicrophoneUsageDescription + This app uses the microphone for audio capture and transcription. diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index 86b3ded..db042dd 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -37,7 +37,9 @@ class _LandingScreenState extends State { late final PhoneAudioService _phoneAudio; bool _usePhoneMic = false; + bool _isMuted = false; final ValueNotifier _isRecording = ValueNotifier(false); + final ValueNotifier _isRecordingBusy = ValueNotifier(false); final List _displayedSentences = []; static const int _maxDisplayedSentences = 4; @@ -74,6 +76,7 @@ class _LandingScreenState extends State { void dispose() { _ws.aiResponse.removeListener(_onAiResponse); _isRecording.dispose(); + _isRecordingBusy.dispose(); _audioPipeline.dispose(); _phoneAudio.dispose(); _ws.dispose(); @@ -86,9 +89,13 @@ class _LandingScreenState extends State { debugPrint(aiResponse); - debugPrint("→ Adding to display: '$aiResponse'"); - if (_manager.isConnected && _manager.transcription.isActive.value) { - _addSentenceToDisplay(aiResponse); + if (!_isMuted) { + debugPrint("→ Adding to display: '$aiResponse'"); + if (_manager.isConnected && _manager.transcription.isActive.value) { + _addSentenceToDisplay(aiResponse); + } + } else { + debugPrint("→ Display is muted, skipping display update: '$aiResponse'"); } } @@ -96,9 +103,10 @@ class _LandingScreenState extends State { /// /// Each sentence is a separate BLE packet (lineNumber 1..N). /// When the list is full, the oldest sentence is evicted to make room. - /// Sentences never disappear on a timer — they scroll off only when - /// pushed out by new ones. + /// Sentences are also automatically removed after a fixed timeout + /// (currently 10 seconds), so older lines can clear even without new ones. void _addSentenceToDisplay(String sentence) { + if (_isMuted) return; if (_displayedSentences.length >= _maxDisplayedSentences) { _displayedSentences.removeAt(0); } @@ -109,6 +117,7 @@ class _LandingScreenState extends State { ); Future.delayed(const Duration(seconds: 10), () { + if (!mounted || _isMuted) return; _displayedSentences.remove(sentence); _manager.transcription.displayLines( List.unmodifiable(_displayedSentences), @@ -122,63 +131,76 @@ class _LandingScreenState extends State { /// Begin a transcription session Future _startTranscription() async { - if (_manager.isConnected) { - //glasses implementation - await _manager.transcription.stop(); // pakota clean stop ensin - await Future.delayed(const Duration(milliseconds: 300)); - _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too - _clearDisplayQueue(); - - await _ws.startAudioStream(); - await _manager.transcription.start(); - - if (_usePhoneMic) { - await _phoneAudio.start((pcm) { - if (_ws.connected.value) { - _ws.sendAudio(pcm); - } - }); + if (_isRecordingBusy.value) return; + if (!_usePhoneMic && !_manager.isConnected) return; + _isRecordingBusy.value = true; + try { + if (_manager.isConnected) { + //glasses implementation + await _manager.transcription.stop(); // pakota clean stop ensin + await Future.delayed(const Duration(milliseconds: 300)); + _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too + _clearDisplayQueue(); + + await _ws.startAudioStream(); + await _manager.transcription.start(); + + if (_usePhoneMic) { + await _phoneAudio.start((pcm) { + if (_ws.connected.value) { + _ws.sendAudio(pcm); + } + }); + } else { + await _manager.microphone.enable(); + _audioPipeline.addListenerToMicrophone(); + } + + await _manager.transcription.displayText('Recording started.'); + debugPrint("Transcription (re)started"); } else { - await _manager.microphone.enable(); - _audioPipeline.addListenerToMicrophone(); + //wo glasses + _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too + _clearDisplayQueue(); + await _ws.startAudioStream(); + await _phoneAudio.start( + (pcm) { + if (_ws.connected.value) _ws.sendAudio(pcm); + }, + ); } - - await _manager.transcription.displayText('Recording started.'); - debugPrint("Transcription (re)started"); - } else { - //wo glasses - _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too - _clearDisplayQueue(); - await _ws.startAudioStream(); - await _phoneAudio.start( - (pcm) { - if (_ws.connected.value) _ws.sendAudio(pcm); - }, - ); + _isRecording.value = true; + } finally { + _isRecordingBusy.value = false; } - _isRecording.value = true; } /// End a transcription session Future _stopTranscription() async { + if (_isRecordingBusy.value) return; + _isRecordingBusy.value = true; _isRecording.value = false; - if (_manager.isConnected) { - _clearDisplayQueue(); - await _manager.transcription.displayText('Recording stopped.'); - await Future.delayed(const Duration(seconds: 2)); - if (_usePhoneMic) { - await _phoneAudio.stop(); + try { + if (_manager.isConnected) { + _clearDisplayQueue(); + await _manager.transcription.displayText('Recording stopped.'); + await Future.delayed(const Duration(seconds: 2)); + if (_usePhoneMic) { + await _phoneAudio.stop(); + } else { + await _manager.microphone.disable(); + await _audioPipeline.stop(); + } + // lisätty jotta paketit kerkiävät lähteä ennen sulkemista + await Future.delayed(const Duration(milliseconds: 200)); + await _ws.stopAudioStream(); + await _manager.transcription.stop(); } else { - await _manager.microphone.disable(); - await _audioPipeline.stop(); + await _phoneAudio.stop(); + await _ws.stopAudioStream(); } - // lisätty jotta paketit kerkiävät lähteä ennen sulkemista - await Future.delayed(const Duration(milliseconds: 200)); - await _ws.stopAudioStream(); - await _manager.transcription.stop(); - } else { - await _phoneAudio.stop(); - await _ws.stopAudioStream(); + } finally { + _isRecordingBusy.value = false; } } @@ -304,11 +326,217 @@ class _LandingScreenState extends State { const SizedBox(width: 14), // Mic toggle + Expanded( + child: ListenableBuilder( + listenable: Listenable.merge( + [_isRecording, _isRecordingBusy]), + builder: (context, _) { + final isLocked = + _isRecording.value || _isRecordingBusy.value; + + final borderColor = isLocked + ? Colors.black26 + : (_usePhoneMic + ? Colors.lightGreen + : Colors.black12); + final backgroundColor = isLocked + ? Colors.black.withAlpha((0.04 * 255).round()) + : (_usePhoneMic + ? Colors.lightGreen + .withAlpha((0.15 * 255).round()) + : Colors.transparent); + + final textColor = isLocked + ? Colors.black38 + : (_usePhoneMic + ? Colors.lightGreen + : Colors.black); + + return Opacity( + opacity: isLocked ? 0.55 : 1, + child: InkWell( + onTap: isLocked + ? null + : () { + setState(() { + _usePhoneMic = !_usePhoneMic; + }); + }, + child: Container( + height: 72, + padding: const EdgeInsets.symmetric( + horizontal: 14), + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + _usePhoneMic + ? Icon( + Icons.phone_android, + size: 22, + color: isLocked + ? Colors.black38 + : Colors.lightGreen, + ) + : Image.asset( + 'assets/images/g1-smart-glasses.webp', + height: 22, + fit: BoxFit.contain, + color: isLocked + ? Colors.black38 + : null, + colorBlendMode: isLocked + ? BlendMode.srcIn + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _usePhoneMic + ? 'Switch to glasses mic' + : 'Switch to phone mic', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: _usePhoneMic + ? FontWeight.bold + : FontWeight.normal, + color: textColor, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + + const SizedBox(height: 14), + + Row( + children: [ + // Start / Stop recording + Expanded( + child: StreamBuilder( + stream: _manager.connectionState, + initialData: G1ConnectionEvent( + state: _manager.isConnected + ? G1ConnectionState.connected + : G1ConnectionState.disconnected, + ), + builder: (context, snapshot) { + final isGlassesConnected = snapshot.data?.state == + G1ConnectionState.connected; + + return ListenableBuilder( + listenable: Listenable.merge( + [_isRecording, _isRecordingBusy]), + builder: (context, _) { + final isRecording = _isRecording.value; + final isBusy = _isRecordingBusy.value; + + final canStart = _usePhoneMic || + isGlassesConnected == true; + + final isDisabled = + isBusy || (!isRecording && !canStart); + + final borderColor = isDisabled + ? Colors.black26 + : (isRecording + ? Colors.red + : Colors.black12); + final backgroundColor = isDisabled + ? Colors.black + .withAlpha((0.04 * 255).round()) + : (isRecording + ? Colors.red + .withAlpha((0.15 * 255).round()) + : Colors.transparent); + + final foregroundColor = isDisabled + ? Colors.black38 + : (isRecording + ? Colors.red + : Colors.grey[800]); + + return Opacity( + opacity: isDisabled ? 0.55 : 1, + child: InkWell( + onTap: isDisabled + ? null + : () async { + if (!isRecording) { + await _startTranscription(); + } else { + await _stopTranscription(); + } + }, + child: Container( + height: 72, + padding: const EdgeInsets.symmetric( + horizontal: 14), + decoration: BoxDecoration( + color: backgroundColor, + border: + Border.all(color: borderColor), + borderRadius: + BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + isRecording + ? Icons.stop_circle_outlined + : Icons.fiber_manual_record, + size: 22, + color: foregroundColor, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + isRecording + ? 'Stop\nRecording' + : 'Start\nRecording', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: foregroundColor, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + + const SizedBox(width: 14), + + // Mute button Expanded( child: InkWell( onTap: () { setState(() { - _usePhoneMic = !_usePhoneMic; + _isMuted = !_isMuted; }); }, child: Container( @@ -316,46 +544,43 @@ class _LandingScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( - color: _usePhoneMic - ? Colors.lightGreen + color: _isMuted + ? Colors.orange .withAlpha((0.15 * 255).round()) : Colors.transparent, border: Border.all( - color: _usePhoneMic - ? Colors.lightGreen - : Colors.black12, + color: + _isMuted ? Colors.orange : Colors.black12, ), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _usePhoneMic - ? const Icon( - Icons.mic, - size: 22, - color: Colors.lightGreen, - ) - : Image.asset( - 'assets/images/g1-smart-glasses.webp', - height: 22, - fit: BoxFit.contain, - ), + Icon( + _isMuted + ? Icons.comments_disabled_outlined + : Icons.comment_outlined, + size: 22, + color: _isMuted + ? Colors.orange + : Colors.grey[700], + ), const SizedBox(width: 10), Expanded( child: Text( - _usePhoneMic - ? 'Phone mic\n(Active)' - : 'Glasses mic\n(Active)', + _isMuted + ? 'Unmute display' + : 'Mute display', textAlign: TextAlign.center, style: TextStyle( - fontSize: 13, - fontWeight: _usePhoneMic + fontSize: 14, + fontWeight: _isMuted ? FontWeight.bold : FontWeight.normal, - color: _usePhoneMic - ? Colors.lightGreen - : Colors.black, + color: _isMuted + ? Colors.orange + : Colors.grey[800], ), ), ), @@ -367,88 +592,6 @@ class _LandingScreenState extends State { ], ), - const SizedBox(height: 14), - - Row( - children: [ - // Start / Stop recording - Expanded( - child: ValueListenableBuilder( - valueListenable: _isRecording, - builder: (context, isRecording, _) { - return InkWell( - onTap: () async { - if (!isRecording) { - await _startTranscription(); - } else { - await _stopTranscription(); - } - }, - child: Container( - height: 72, - padding: const EdgeInsets.symmetric( - horizontal: 14), - decoration: BoxDecoration( - color: isRecording - ? Colors.red - .withAlpha((0.15 * 255).round()) - : Colors.transparent, - border: Border.all( - color: isRecording - ? Colors.red - : Colors.black12, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - isRecording - ? Icons.stop_circle_outlined - : Icons.fiber_manual_record, - size: 22, - color: isRecording - ? Colors.red - : Colors.grey[800], - ), - const SizedBox(width: 10), - Expanded( - child: Text( - isRecording - ? 'Stop\nRecording' - : 'Start\nRecording', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: isRecording - ? Colors.red - : Colors.grey[800], - ), - ), - ), - ], - ), - ), - ); - }, - ), - ), - - const SizedBox(width: 14), - - // Recordings placeholder - Expanded( - child: LandingTile( - icon: Icons.play_circle_outline, - label: 'Recordings', - onTap: () {}, - ), - ), - ], - ), - const SizedBox(height: 22), ValueListenableBuilder( diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index aa61d00..f4555d2 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -155,11 +155,28 @@ class WebsocketService { } void dispose() { - disconnect(); - connected.dispose(); - committedText.dispose(); - interimText.dispose(); - asrActive.dispose(); - aiResponse.dispose(); + // `disconnect()` is async and may touch ValueNotifiers after awaiting. + // During dispose we must not schedule work that can run after notifiers + // are disposed. + final channel = _audioChannel; + _audioChannel = null; + connected.value = false; + try { + channel?.sink.add(jsonEncode({'type': 'control', 'cmd': 'stop'})); + channel?.sink.close(); + } catch (_) { + // ignore: connection may already be closed + } finally { + committedText.value = ''; + interimText.value = ''; + asrActive.value = false; + aiResponse.value = ''; + + connected.dispose(); + committedText.dispose(); + interimText.dispose(); + asrActive.dispose(); + aiResponse.dispose(); + } } } diff --git a/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart b/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart index 2c0a16b..dd501c3 100644 --- a/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart +++ b/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -51,10 +52,12 @@ class G1Glass { try { await device.connect(); await _discoverServices(); - await device.requestMtu(BluetoothConstants.defaultMtu); - await device.requestConnectionPriority( - connectionPriorityRequest: ConnectionPriority.high, - ); + if (!kIsWeb && Platform.isAndroid) { + await device.requestMtu(BluetoothConstants.defaultMtu); + await device.requestConnectionPriority( + connectionPriorityRequest: ConnectionPriority.high, + ); + } _startHeartbeat(); debugPrint('[$side Glass] Connected successfully'); } catch (e) { diff --git a/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart b/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart index bdfa491..b32fca1 100644 --- a/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart +++ b/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart @@ -66,6 +66,7 @@ class G1Manager { Timer? _scanTimer; StreamSubscription? _scanSubscription; bool _isScanning = false; + bool _isConnecting = false; int _retryCount = 0; bool _connectionCallbackFired = false; @@ -166,7 +167,7 @@ class G1Manager { throw Exception(msg); } - final adapterState = await FlutterBluePlus.adapterState.first; + final adapterState = FlutterBluePlus.adapterStateNow; if (adapterState != BluetoothAdapterState.on) { const msg = 'Bluetooth is turned off'; onUpdate?.call(msg); @@ -175,6 +176,7 @@ class G1Manager { // Reset state _isScanning = true; + _isConnecting = false; _retryCount = 0; _connectionCallbackFired = false; _leftGlass = null; @@ -204,7 +206,9 @@ class G1Manager { ) async { try { // Check system connected devices - final connectedDevices = await FlutterBluePlus.systemDevices([]); + final connectedDevices = await FlutterBluePlus.systemDevices([ + Guid(BluetoothConstants.uartServiceUuid), + ]); debugPrint('Found ${connectedDevices.length} system connected devices'); for (final device in connectedDevices) { @@ -381,11 +385,57 @@ class G1Manager { } if (glass != null) { - await glass.connect(); - _setupReconnect(glass); + if (_leftGlass != null && _rightGlass != null && !_isConnecting) { + await _connectDiscoveredGlasses( + onUpdate, + onGlassesFound, + onConnected, + ); + } + } + } + + Future _connectDiscoveredGlasses( + OnStatusUpdate? onUpdate, + OnGlassesFound? onGlassesFound, + OnConnected? onConnected, + ) async { + if (_leftGlass == null || _rightGlass == null || _isConnecting) return; + + _isConnecting = true; + _connectionStateController.add(const G1ConnectionEvent( + state: G1ConnectionState.connecting, + )); + onConnectionChanged?.call(G1ConnectionState.connecting, null); + onUpdate?.call('Connecting to glasses...'); + + try { + if (_isScanning) { + await stopScan(); + } + + if (!_leftGlass!.isConnected) { + await _leftGlass!.connect(); + _setupReconnect(_leftGlass!); + } + + if (!_rightGlass!.isConnected) { + await _rightGlass!.connect(); + _setupReconnect(_rightGlass!); + } - // Check if both glasses are now connected after this connection completes _checkBothConnected(onUpdate, onGlassesFound, onConnected); + } catch (e, st) { + debugPrint('Error connecting discovered glasses: $e'); + debugPrintStack(stackTrace: st); + onUpdate?.call('Failed to connect to glasses: $e'); + _connectionStateController.add(G1ConnectionEvent( + state: G1ConnectionState.error, + errorMessage: e.toString(), + )); + onConnectionChanged?.call(G1ConnectionState.error, null); + } finally { + _isConnecting = false; } } diff --git a/pubspec.lock b/pubspec.lock index aa8ce67..c164041 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -315,18 +315,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -560,10 +560,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" timezone: dependency: transitive description: diff --git a/test/ble_mock/g1_manager_mock.dart b/test/ble_mock/g1_manager_mock.dart index afa2579..7640d1b 100644 --- a/test/ble_mock/g1_manager_mock.dart +++ b/test/ble_mock/g1_manager_mock.dart @@ -145,6 +145,9 @@ class MockG1Microphone implements G1Microphone { } class MockG1Transcription implements G1Transcription { + final List displayTextCalls = []; + final List> displayLinesCalls = []; + @override final isActive = ValueNotifier(false); @@ -159,7 +162,20 @@ class MockG1Transcription implements G1Transcription { } @override - Future displayText(String text, {bool isInterim = false}) async {} + Future displayText(String text, {bool isInterim = false}) async { + displayTextCalls.add(text); + } + + @override + Future displayLines(List lines, + {bool isInterim = false}) async { + displayLinesCalls.add(List.from(lines)); + } + + void clearDisplayCalls() { + displayTextCalls.clear(); + displayLinesCalls.clear(); + } @override Future pause() async {} diff --git a/test/widget_test.dart b/test/widget_test.dart index 5003a35..d9ff236 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -3,6 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'ble_mock/g1_manager_mock.dart'; import 'package:front/screens/landing_screen.dart'; +import 'package:front/services/websocket_service.dart'; + +class TestWebsocketService extends WebsocketService { + TestWebsocketService() : super(baseUrl: 'test'); + + @override + Future connect() async { + connected.value = true; + } +} void main() { late MockG1Manager mockManager; @@ -31,15 +41,81 @@ void main() { await tester.pump(const Duration(milliseconds: 600)); } - testWidgets('App shows text input and send button', - (WidgetTester tester) async { + testWidgets('Landing screen shows key elements', (WidgetTester tester) async { await pumpLanding(tester); expect(find.text('Even realities G1 smart glasses'), findsOneWidget); - expect(find.text('Recordings'), findsOneWidget); - expect(find.text('Even realities G1 smart glasses'), findsOneWidget); + expect(find.text('Connect to glasses'), findsOneWidget); + expect(find.text('Switch to phone mic'), findsOneWidget); + expect(find.text('Start\nRecording'), findsOneWidget); + expect(find.text('Mute display'), findsOneWidget); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('Register'), findsOneWidget); + + await disposeLanding(tester); + }); + + testWidgets('No text is sent to glasses when display is muted', + (WidgetTester tester) async { + final ws = TestWebsocketService(); + mockManager.setConnected(true); + await mockManager.transcription.start(); + + await tester.pumpWidget( + MaterialApp( + home: LandingScreen( + manager: mockManager, + ws: ws, + ), + ), + ); + await tester.pump(); + + await tester.tap(find.text('Mute display')); + await tester.pump(); + expect(find.text('Unmute display'), findsOneWidget); + + (mockManager.transcription as MockG1Transcription).clearDisplayCalls(); + + ws.aiResponse.value = 'Hello from backend'; + await tester.pump(); + + final tx = mockManager.transcription as MockG1Transcription; + expect(tx.displayTextCalls, isEmpty); + expect(tx.displayLinesCalls, isEmpty); + + await disposeLanding(tester); + }); + + testWidgets('Text is sent to glasses when display is not muted', + (WidgetTester tester) async { + final ws = TestWebsocketService(); + mockManager.setConnected(true); + await mockManager.transcription.start(); + + await tester.pumpWidget( + MaterialApp( + home: LandingScreen( + manager: mockManager, + ws: ws, + ), + ), + ); + await tester.pump(); + + final tx = mockManager.transcription as MockG1Transcription; + tx.clearDisplayCalls(); + + ws.aiResponse.value = 'Hello from backend'; + await tester.pump(); + + expect(tx.displayLinesCalls, isNotEmpty); + expect(tx.displayLinesCalls.last, contains('Hello from backend')); await disposeLanding(tester); + // LandingScreen schedules a 10s cleanup timer per sentence. + // Advance time so the timer fires and doesn't remain pending. + await tester.pump(const Duration(seconds: 11)); }); testWidgets('Connecting to glasses text is shown when bluetooth is scanning',