diff --git a/.mcp copy.json b/.mcp copy.json deleted file mode 100644 index eaa12d328..000000000 --- a/.mcp copy.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mcpServers": { - "web-search": { - "url": "http://mcp.ideapsound.com:8000/sse" - }, - "amap-amap-sse": { - "url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646" - } - } -} \ No newline at end of file diff --git a/.metadata b/.metadata index b4fb38531..5c982c8d4 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 - base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - platform: ios - create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 - base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc # User provided section diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist index c90ada3a3..590c85a5f 100644 --- a/GoogleService-Info.plist +++ b/GoogleService-Info.plist @@ -3,9 +3,9 @@ CLIENT_ID - 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + 907754492821-tl2blr39g82re40sttkve36ovt6qcu6c.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + com.googleusercontent.apps.907754492821-tl2blr39g82re40sttkve36ovt6qcu6c ANDROID_CLIENT_ID 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com API_KEY @@ -15,7 +15,7 @@ PLIST_VERSION 1 BUNDLE_ID - com.yunqiinnovation.deepsound + com.chuangxiangyunqi.deapsound PROJECT_ID deepsound-e0e3d STORAGE_BUCKET @@ -31,6 +31,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:907754492821:ios:246794fc44b23174145c9c + 1:907754492821:ios:e4b4f3b37e58a7d1145c9c \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 89bcfdef7..44c9dd0dc 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -120,28 +120,8 @@ android { } dependencies { - - implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") - - - // 添加本地插件模块依赖 - implementation(project(":spotify_sdk")) - implementation(project(":deepvoice_utils")) - implementation(project(":ble_service")) - // 添加OkHttp依赖 - implementation("com.squareup.okhttp3:okhttp:4.9.3") - // 添加核心库反糖化 coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") - - - // 添加androidx.media依赖 - implementation("androidx.media:media:1.6.0") - - // 添加legacy-support-v4依赖 - implementation("androidx.legacy:legacy-support-v4:1.0.0") - - } flutter { diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt index bf9d92455..e7b34ac65 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt @@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity import android.content.Context import android.os.Handler import android.os.Looper -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log class MainActivity: FlutterActivity() { private val TAG = "MainActivity" diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 533e00619..412f86b15 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -35,7 +35,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") // 其他依赖... } } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index d350a0017..3dd7256fc 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -37,9 +37,9 @@ include(":chat_storage") include(":jieli") include(":spotify_sdk") include(":ble_service") -include(":deepvoice_utils") include(":classic_bluetooth") include(":agent_service") +include(":speech") // 设置azure_speech项目的路径 project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") @@ -47,7 +47,6 @@ project(":chat_storage").projectDir = file("../local_plugins/chat_storage/androi project(":jieli").projectDir = file("../local_plugins/jieli/android") project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android") project(":ble_service").projectDir = file("../local_plugins/ble_service/android") -project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android") project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android") - +project(":speech").projectDir = file("../local_plugins/speech/android") \ No newline at end of file diff --git a/assets/config/GoogleService-Info.plist b/assets/config/GoogleService-Info.plist new file mode 100644 index 000000000..c90ada3a3 --- /dev/null +++ b/assets/config/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + ANDROID_CLIENT_ID + 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com + API_KEY + AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808 + GCM_SENDER_ID + 907754492821 + PLIST_VERSION + 1 + BUNDLE_ID + com.yunqiinnovation.deepsound + PROJECT_ID + deepsound-e0e3d + STORAGE_BUCKET + deepsound-e0e3d.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:907754492821:ios:246794fc44b23174145c9c + + \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index c10c27d8b..a2084703d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '13.0' +# Define a global platform for your project +platform :ios, '18.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -31,10 +31,6 @@ target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - # 添加 Microsoft Cognitive Services Speech SDK - pod 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0' - target 'RunnerTests' do inherit! :search_paths end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cc790be8d..336d0ad28 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,8 +11,45 @@ PODS: - Flutter - audio_waveforms (0.0.1): - Flutter + - ble_service (0.0.1): + - Flutter + - chat_storage (0.0.1): + - Flutter + - classic_bluetooth (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif - FBAEMKit (17.1.0): - FBSDKCoreKit_Basics (= 17.1.0) - FBSDKCoreKit (17.1.0): @@ -21,36 +58,39 @@ PODS: - FBSDKCoreKit_Basics (17.1.0) - FBSDKLoginKit (17.1.0): - FBSDKCoreKit (= 17.1.0) - - Firebase/Auth (11.8.0): + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Firebase/Auth (11.10.0): - Firebase/CoreOnly - - FirebaseAuth (~> 11.8.0) - - Firebase/CoreOnly (11.8.0): - - FirebaseCore (~> 11.8.0) - - firebase_auth (5.5.1): - - Firebase/Auth (= 11.8.0) + - FirebaseAuth (~> 11.10.0) + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - firebase_auth (5.5.2): + - Firebase/Auth (= 11.10.0) - firebase_core - Flutter - - firebase_core (3.12.1): - - Firebase/CoreOnly (= 11.8.0) + - firebase_core (3.13.0): + - Firebase/CoreOnly (= 11.10.0) - Flutter - - FirebaseAppCheckInterop (11.10.0) - - FirebaseAuth (11.8.1): + - FirebaseAppCheckInterop (11.12.0) + - FirebaseAuth (11.10.0): - FirebaseAppCheckInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0) - - FirebaseCore (~> 11.8.0) - - FirebaseCoreExtension (~> 11.8.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/Environment (~> 8.0) - GTMSessionFetcher/Core (< 5.0, >= 3.4) - - RecaptchaInterop (~> 100.0) - - FirebaseAuthInterop (11.10.0) - - FirebaseCore (11.8.1): - - FirebaseCoreInternal (~> 11.8.0) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.12.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreExtension (11.8.0): - - FirebaseCore (~> 11.8.0) - - FirebaseCoreInternal (11.8.0): + - FirebaseCoreExtension (11.10.0): + - FirebaseCore (~> 11.10.0) + - FirebaseCoreInternal (11.10.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - Flutter (1.0.0) - flutter_blue_plus_darwin (0.0.2): @@ -86,25 +126,25 @@ PODS: - AppAuth (< 2.0, >= 1.7.3) - GTMAppAuth (< 5.0, >= 4.1.1) - GTMSessionFetcher/Core (~> 3.3) - - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.0.2)": + - "GoogleUtilities/NSData+zlib (8.1.0)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.0.2) - - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - GTMAppAuth (4.1.1): @@ -115,8 +155,12 @@ PODS: - GTMSessionFetcher/Core (3.5.0) - GTMSessionFetcher/Full (3.5.0): - GTMSessionFetcher/Core + - haptic_feedback (0.5.1): + - Flutter - image_picker_ios (0.0.1): - Flutter + - jl_ota (0.0.1): + - Flutter - just_audio (0.0.1): - Flutter - FlutterMacOS @@ -135,13 +179,27 @@ PODS: - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) - - MicrosoftCognitiveServicesSpeech-iOS (1.34.0) + - music_service (0.0.1): + - Flutter + - open_ai_service (0.0.1): + - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - RecaptchaInterop (100.0.0) + - QCloudCore (6.4.7): + - QCloudCore/Default (= 6.4.7) + - QCloudCore/Default (6.4.7): + - QCloudTrack/Beacon (= 6.4.7) + - QCloudCOSXML (6.4.7): + - QCloudCOSXML/Default (= 6.4.7) + - QCloudCOSXML/Default (6.4.7): + - QCloudCore (= 6.4.7) + - QCloudTrack/Beacon (6.4.7) + - RecaptchaInterop (101.0.0) - record_darwin (1.0.0): - Flutter - SDWebImage (5.21.0): @@ -156,17 +214,30 @@ PODS: - Flutter - FlutterMacOS - Try + - spotify_sdk (0.0.1): + - Flutter - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - SwiftyGif (5.4.5) + - tencentcloud_cos_sdk_plugin (1.2.3): + - Flutter + - QCloudCOSXML (= 6.4.7) - Try (2.1.1) + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS - WechatOpenSDK-XCFramework (2.0.4) DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) + - ble_service (from `.symlinks/plugins/ble_service/ios`) + - chat_storage (from `.symlinks/plugins/chat_storage/ios`) + - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) @@ -178,19 +249,28 @@ DEPENDENCIES: - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) - fluwx (from `.symlinks/plugins/fluwx/ios`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - jl_ota (from `.symlinks/plugins/jl_ota/ios`) - just_audio (from `.symlinks/plugins/just_audio/darwin`) - - MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0) + - music_service (from `.symlinks/plugins/music_service/ios`) + - open_ai_service (from `.symlinks/plugins/open_ai_service/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`) + - spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: - AppAuth + - DKImagePickerController + - DKPhotoGallery - FBAEMKit - FBSDKCoreKit - FBSDKCoreKit_Basics @@ -208,10 +288,13 @@ SPEC REPOS: - GTMSessionFetcher - libwebp - Mantle - - MicrosoftCognitiveServicesSpeech-iOS + - QCloudCore + - QCloudCOSXML + - QCloudTrack - RecaptchaInterop - SDWebImage - SDWebImageWebPCoder + - SwiftyGif - Try - WechatOpenSDK-XCFramework @@ -222,8 +305,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_session/ios" audio_waveforms: :path: ".symlinks/plugins/audio_waveforms/ios" + ble_service: + :path: ".symlinks/plugins/ble_service/ios" + chat_storage: + :path: ".symlinks/plugins/chat_storage/ios" + classic_bluetooth: + :path: ".symlinks/plugins/classic_bluetooth/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" firebase_auth: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: @@ -246,10 +337,20 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluwx/ios" google_sign_in_ios: :path: ".symlinks/plugins/google_sign_in_ios/darwin" + haptic_feedback: + :path: ".symlinks/plugins/haptic_feedback/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + jl_ota: + :path: ".symlinks/plugins/jl_ota/ios" just_audio: :path: ".symlinks/plugins/just_audio/darwin" + music_service: + :path: ".symlinks/plugins/music_service/ios" + open_ai_service: + :path: ".symlinks/plugins/open_ai_service/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: @@ -260,28 +361,40 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" speech_to_text: :path: ".symlinks/plugins/speech_to_text/darwin" + spotify_sdk: + :path: ".symlinks/plugins/spotify_sdk/ios" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + tencentcloud_cos_sdk_plugin: + :path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf + ble_service: 068ebe51800424f980282b16cbf757574eec4e08 + chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 + classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881 FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2 FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43 FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a - Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf - firebase_auth: 9ebbd83276bf977dea1c74dfc199acf9d2a2f42f - firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510 - FirebaseAppCheckInterop: 9664c858489710f682766ef54e2b6741d3b62070 - FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737 - FirebaseAuthInterop: 01a804fb074424fd58b92dd50dd0272277199356 - FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d - FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e - FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5 + firebase_core: 2d4534e7b489907dcede540c835b48981d890943 + FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852 + FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 + FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595 + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6 flutter_facebook_auth: 8a9ad5fbfc25b77e7a53ea8986866950b27edcae @@ -292,26 +405,37 @@ SPEC CHECKSUMS: fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db - GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + jl_ota: 5fa90bf47674de0f5bce9af2219e25fa0c1425f5 just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb + music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d + open_ai_service: 82ea947b1a081a2a17bc0c8e5467dcbd7cf9b232 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 + QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223 + QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298 + QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3 + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac + spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 + webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f -PODFILE CHECKSUM: 4f26889ae572e1722a139b651939749d40c71469 +PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b46ef17e1..70369f46d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,25 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; }; - 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; }; - 0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */; }; - 0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */; }; - 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; }; - 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; }; - 0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; }; - 0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */; }; - 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */; }; + 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 591293922D9524D1004A65BB /* GoogleService-Info.plist */; }; + 6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9190D0838960F143BDC158 /* Pods_Runner.framework */; }; + A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,32 +43,19 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 02CF74744224D1C933681603 /* 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 = ""; }; - 0906EF7A73CBA5EAFC64E05D /* 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 = ""; }; - 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = ""; }; - 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = ""; }; - 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = ""; }; - 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolcanoAIService.swift; sourceTree = ""; }; - 0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = ""; }; - 0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; - 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = ""; }; - 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = ""; }; - 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = ""; }; + 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 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 = ""; }; - 4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; }; - 591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6189535D5FBB81D8377BE874 /* 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 = ""; }; + 742A00629970110BA5008064 /* 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 = ""; }; 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 = ""; }; - 77D16D761891F29239C8DF02 /* 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 = ""; }; + 762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8C1130E20389847CBD7B1AAC /* 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 = ""; }; - 930FE6D58BDB20688CE0A7CF /* 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 = ""; }; 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; }; @@ -85,7 +63,12 @@ 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 = ""; }; - 9B84B894288BF76332D3F407 /* 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 = ""; }; + B3EC1304D6F5453D58E0BDB0 /* 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 = ""; }; + C15B3030DF1AED99D3829135 /* 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 = ""; }; + D1CF75E5BD49BB58812D5F36 /* 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 = ""; }; + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -93,40 +76,50 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */, + 6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - B4459155A2535A0ED1D6C7D8 /* Frameworks */ = { + B3515CD9628E3332AFC88CD4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */, + A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { + 1ACB686EC7DC36C6E33D372F /* Pods */ = { isa = PBXGroup; children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, + 762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */, + B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */, + 6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */, + C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */, + 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */, + D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */, ); - path = RunnerTests; + path = Pods; sourceTree = ""; }; - 776E5AB1CE0A62CBDBAB3996 /* Pods */ = { + 1E5DDD8701F6FB27AA502080 /* Frameworks */ = { isa = PBXGroup; children = ( - 8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */, - 77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */, - 0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */, - 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */, - 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */, - 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */, + 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, + E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */, + F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */, ); - path = Pods; + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -147,8 +140,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 776E5AB1CE0A62CBDBAB3996 /* Pods */, - A5A9FDE94E78257E9A48E2E8 /* Frameworks */, + 1ACB686EC7DC36C6E33D372F /* Pods */, + 1E5DDD8701F6FB27AA502080 /* Frameworks */, ); sourceTree = ""; }; @@ -164,18 +157,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */, - 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */, - 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */, - 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */, - 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */, - 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */, - 0ADE29672D8C25660032BFCB /* sample.mp3 */, - 0ADE29682D8C25660032BFCB /* silence.mp3 */, - 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */, - 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */, + 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */, 97C146FA1CF9000F007C117D /* Main.storyboard */, - 591293922D9524D1004A65BB /* GoogleService-Info.plist */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, @@ -183,19 +166,11 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */, ); path = Runner; sourceTree = ""; }; - A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 4A9190D0838960F143BDC158 /* Pods_Runner.framework */, - 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -203,10 +178,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */, + D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, - B4459155A2535A0ED1D6C7D8 /* Frameworks */, + B3515CD9628E3332AFC88CD4 /* Frameworks */, ); buildRules = ( ); @@ -222,15 +197,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */, + 90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */, - D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */, + 510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */, + 298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -270,9 +245,6 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, - ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -297,37 +269,30 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */, - 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - 591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */ = { + 298589D956D542AE0EE16DAE /* [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 = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${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-resources.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -346,7 +311,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */ = { + 510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -363,7 +328,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */ = { + 90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -400,21 +365,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */ = { + D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */ = { 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"; + 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-resources-${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-resources.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; }; /* End PBXShellScriptBuildPhase section */ @@ -432,14 +402,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */, - 0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */, - 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */, - 0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */, - 0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */, - 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -498,6 +461,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -532,16 +496,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 23; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -552,64 +516,53 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 42C82SZUVX; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 42C82SZUVX; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 42C82SZUVX; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; @@ -638,6 +591,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -695,6 +649,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -731,21 +686,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -759,17 +711,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; - CURRENT_PROJECT_VERSION = 23; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 42C82SZUVX; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -812,17 +763,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 11.10.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 7ee26c6b2..000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,123 +0,0 @@ -{ - "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4", - "version" : "11.10.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6", - "version" : "11.10.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", - "version" : "8.0.2" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", - "version" : "1.69.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", - "version" : "4.4.0" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - }, - { - "identity" : "swift-protobuf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-protobuf.git", - "state" : { - "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", - "version" : "1.29.0" - } - } - ], - "version" : 3 -} diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 7ee26c6b2..000000000 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,123 +0,0 @@ -{ - "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4", - "version" : "11.10.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6", - "version" : "11.10.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", - "version" : "8.0.2" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", - "version" : "1.69.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35", - "version" : "4.4.0" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - }, - { - "identity" : "swift-protobuf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-protobuf.git", - "state" : { - "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", - "version" : "1.29.0" - } - } - ], - "version" : 3 -} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 3bfd2c086..626664468 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,743 +1,13 @@ import Flutter import UIKit -import AVFoundation @main @objc class AppDelegate: FlutterAppDelegate { - private var classicBluetoothHelper: ClassicBluetoothHelper? - private var mediaButtonHelper: Any? - // 添加Azure语音服务辅助类 - private var azureAsrHelper: Any? - private var azureTtsHelper: Any? - // 添加火山AI服务 - private var volcanoAIService: VolcanoAIService? - // 语音交互服务 - private var voiceInteractionService: VoiceInteractionService? - - private var audioSessionManager: AudioSessionManager? - override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - - // 获取flutter控制器 - guard let controller = window?.rootViewController as? FlutterViewController else { - NSLog("AppDelegate: Flutter控制器不可用") - return false - } - - // 初始化音频会话管理器 - audioSessionManager = AudioSessionManager.shared - - - // 统一注册所有方法通道 - registerMethodChannel() - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - - private func registerMethodChannel() { - let controller = window?.rootViewController as? FlutterViewController - guard let controller = controller else { - NSLog("AppDelegate: FlutterViewController 不可用") - return - } - - // 注册经典蓝牙方法通道 - registerClassicBluetoothChannels(controller: controller) - - // 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道 - if #available(iOS 13.0, *) { - // 注册媒体按钮方法通道 - registerMediaButtonChannels(controller: controller) - - // 注册Azure ASR方法通道 - registerAzureAsrChannels(controller: controller) - - // 注册Azure TTS方法通道 - registerAzureTtsChannels(controller: controller) - - // 注册火山AI服务方法通道 - registerVolcanoAIServiceChannels(controller: controller) - - // 注册语音交互服务方法通道 - registerVoiceInteractionChannels(controller: controller) - } - } - - // MARK: - 经典蓝牙通道 - - private func registerClassicBluetoothChannels(controller: FlutterViewController) { - // 初始化蓝牙辅助类 - if classicBluetoothHelper == nil { - classicBluetoothHelper = ClassicBluetoothHelper() - } - - guard let classicBluetoothHelper = self.classicBluetoothHelper else { - NSLog("AppDelegate: 经典蓝牙辅助实例不可用") - return - } - - // 注册经典蓝牙方法通道 - let classicBluetoothChannel = FlutterMethodChannel( - name: "com.deep_voice.classic_bluetooth", - binaryMessenger: controller.binaryMessenger - ) - - classicBluetoothChannel.setMethodCallHandler { [weak self, weak classicBluetoothHelper] (call, result) in - guard let _ = self, let classicBluetoothHelper = classicBluetoothHelper else { - result(FlutterError(code: "UNAVAILABLE", message: "经典蓝牙辅助实例不可用", details: nil)) - return - } - - switch call.method { - case "isBluetoothEnabled": - result(classicBluetoothHelper.isBluetoothEnabled()) - - case "getConnectedHeadsetDevices": - // 获取已连接的耳机设备 - classicBluetoothHelper.getConnectedHeadsetDevices { devices, error in - if let error = error { - result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil)) - } else { - result(devices) - } - } - case "initialize": - classicBluetoothHelper.initialize() - result(true) - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册蓝牙流通道 - let classicBluetoothStreamChannel = FlutterEventChannel( - name: "com.deep_voice.classic_bluetooth_events", - binaryMessenger: controller.binaryMessenger - ) - - // 创建蓝牙流处理器(作为局部变量) - let bluetoothStreamHandler = BluetoothStreamHandler(bluetoothHelper: classicBluetoothHelper) - classicBluetoothStreamChannel.setStreamHandler(bluetoothStreamHandler) - } - - // MARK: - 消息通道设置 - - // MARK: - 蓝牙媒体按键通道 - - @available(iOS 13.0, *) - private func registerMediaButtonChannels(controller: FlutterViewController) { - // 初始化媒体按钮辅助类 - if mediaButtonHelper == nil { - mediaButtonHelper = BluetoothMediaButtonHelper() - } - // guard let mediaButtonHelper = self.mediaButtonHelper as? BluetoothMediaButtonHelper else { - // NSLog("AppDelegate: 媒体按钮辅助实例不可用") - // return - // } - // mediaButtonHelper.startButtonListening() - // 注册媒体按钮方法通道 - let mediaButtonChannel = FlutterMethodChannel( - name: "com.deep_voice.bluetooth_media_button", - binaryMessenger: controller.binaryMessenger - ) - - mediaButtonChannel.setMethodCallHandler { [weak self] (call, result) in - guard let _ = self, let mediaButtonHelper = self?.mediaButtonHelper as? BluetoothMediaButtonHelper else { - result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil)) - return - } - - switch call.method { - case "startButtonListening": - let success = mediaButtonHelper.startButtonListening() - result(success) - - case "stopButtonListening": - let success = mediaButtonHelper.stopButtonListening() - result(success) - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册媒体按钮流通道 - let mediaButtonStreamChannel = FlutterEventChannel( - name: "com.deep_voice.bluetooth_media_button_events", - binaryMessenger: controller.binaryMessenger - ) - - // 创建媒体按钮流处理器 - let mediaButtonStreamHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper as! BluetoothMediaButtonHelper) - mediaButtonStreamChannel.setStreamHandler(mediaButtonStreamHandler) - } - - - // 注册Azure ASR通道 - @available(iOS 13.0, *) - private func registerAzureAsrChannels(controller: FlutterViewController) { - // 注册Azure ASR方法通道 - let azureAsrChannel = FlutterMethodChannel( - name: "com.deep_voice.azure_asr", - binaryMessenger: controller.binaryMessenger - ) - - // 初始化Azure ASR辅助类 - azureAsrHelper = AzureAsrHelper() - - // 设置ASR方法处理器 - azureAsrChannel.setMethodCallHandler { [weak self] (call, result) in - guard let self = self, - let asrHelper = self.azureAsrHelper as? AzureAsrHelper else { - result(FlutterError(code: "UNAVAILABLE", message: "Azure ASR辅助实例不可用", details: nil)) - return - } - - switch call.method { - case "initialize": - guard let args = call.arguments as? [String: Any], - let key = args["subscriptionKey"] as? String, - let region = args["region"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil)) - return - } - - let languages = args["supportedLanguages"] as? [String] - let success = asrHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, supportedLanguages: languages) - result(success) - - case "recognizeOnce": - let success = asrHelper.recognizeOnce() - result(success) - - case "startContinuousRecognition": - let success = asrHelper.startContinuousRecognition() - result(success) - - case "stopContinuousRecognition": - let success = asrHelper.stopContinuousRecognition() - result(success) - - case "isContinuousRecognitionActive": - let isActive = asrHelper.isContinuousRecognitionActive() - result(isActive) - - case "dispose": - asrHelper.dispose() - result(true) - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册Azure ASR流通道 - let azureAsrStreamChannel = FlutterEventChannel( - name: "com.deep_voice.azure_asr_events", - binaryMessenger: controller.binaryMessenger - ) - - // 创建ASR流处理器 - let asrStreamHandler = AsrStreamHandler(azureAsrHelper: azureAsrHelper as! AzureAsrHelper) - azureAsrStreamChannel.setStreamHandler(asrStreamHandler) - } - - // 注册Azure TTS通道 - @available(iOS 13.0, *) - private func registerAzureTtsChannels(controller: FlutterViewController) { - // 注册Azure TTS方法通道 - let azureTtsChannel = FlutterMethodChannel( - name: "com.deep_voice.azure_tts", - binaryMessenger: controller.binaryMessenger - ) - - // 初始化Azure TTS辅助类 - azureTtsHelper = AzureTtsHelper() - - // 设置TTS方法处理器 - azureTtsChannel.setMethodCallHandler { [weak self] (call, result) in - guard let self = self, - let ttsHelper = self.azureTtsHelper as? AzureTtsHelper else { - result(FlutterError(code: "UNAVAILABLE", message: "Azure TTS辅助实例不可用", details: nil)) - return - } - - switch call.method { - case "initialize": - guard let args = call.arguments as? [String: Any], - let key = args["subscriptionKey"] as? String, - let region = args["region"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil)) - return - } - - let language = args["language"] as? String ?? "zh-CN" - let success = ttsHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, language: language) - result(success) - - case "setVoice": - guard let args = call.arguments as? [String: Any], - let voiceName = args["voiceName"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "语音名称参数无效", details: nil)) - return - } - - let success = ttsHelper.setVoice(voiceName: voiceName) - result(success) - - case "setSpeechParams": - guard let args = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "语音参数无效", details: nil)) - return - } - - let rate = args["rate"] as? Int ?? 0 - let pitch = args["pitch"] as? Int ?? 0 - let volume = args["volume"] as? Int ?? 100 - - let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume) - result(success) - - case "speakText": - guard let args = call.arguments as? [String: Any], - let text = args["text"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "文本参数无效", details: nil)) - return - } - - let success = ttsHelper.speakText(text: text) - result(success) - - case "stopSpeaking": - let success = ttsHelper.stopSpeaking() - result(success) - - case "isSpeaking": - let isSpeaking = ttsHelper.isSpeaking() - result(isSpeaking) - - case "dispose": - ttsHelper.dispose() - result(true) - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册Azure TTS流通道 - let azureTtsStreamChannel = FlutterEventChannel( - name: "com.deep_voice.azure_tts_events", - binaryMessenger: controller.binaryMessenger - ) - - // 创建TTS流处理器 - let ttsStreamHandler = TtsStreamHandler(azureTtsHelper: azureTtsHelper as! AzureTtsHelper) - azureTtsStreamChannel.setStreamHandler(ttsStreamHandler) - } - - // 注册火山AI服务方法通道 - @available(iOS 13.0, *) - private func registerVolcanoAIServiceChannels(controller: FlutterViewController) { - guard let volcanoAIService = volcanoAIService else { - return - } - - // 注册火山AI方法通道 - let volcanoAIChannel = FlutterMethodChannel( - name: "com.deep_voice.volcano_ai", - binaryMessenger: controller.binaryMessenger - ) - - volcanoAIChannel.setMethodCallHandler { [weak self, weak volcanoAIService] (call, result) in - guard let _ = self, let volcanoAIService = volcanoAIService else { - result(FlutterError(code: "UNAVAILABLE", message: "火山AI服务实例不可用", details: nil)) - return - } - - switch call.method { - case "initialize": - let apiKey = call.arguments as? String ?? "" - let success = volcanoAIService.initialize(apiKey: apiKey) - result(success) - - case "sendMessage": - guard let arguments = call.arguments as? [String: Any], - let message = arguments["message"] as? String, - let systemPrompt = arguments["systemPrompt"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - if message.isEmpty { - result(FlutterError(code: "EMPTY_MESSAGE", message: "消息不能为空", details: nil)) - return - } - - // 异步发送消息 - Task { - do { - // 创建用户消息 - let userMessage = volcanoAIService.createUserMessage(content: message) - - // 发送消息并等待响应 - let response = try await volcanoAIService.sendMessage( - messages: [userMessage], - systemPrompt: systemPrompt - ) - - // 返回响应 - DispatchQueue.main.async { - result(response) - } - } catch { - // 处理错误 - DispatchQueue.main.async { - result(FlutterError( - code: "AI_ERROR", - message: "AI请求失败: \(error.localizedDescription)", - details: nil - )) - } - } - } - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册火山AI流通道 - let volcanoAIStreamChannel = FlutterEventChannel( - name: "com.deep_voice.volcano_ai_stream", - binaryMessenger: controller.binaryMessenger - ) - - // 创建火山AI流处理器(作为局部变量) - let volcanoAIStreamHandler = VolcanoAIStreamHandler(volcanoAIService: volcanoAIService) - volcanoAIStreamChannel.setStreamHandler(volcanoAIStreamHandler) - } - - // 注册语音交互服务方法通道 - @available(iOS 13.0, *) - private func registerVoiceInteractionChannels(controller: FlutterViewController) { - // 初始化语音交互服务 - voiceInteractionService = VoiceInteractionService.shared - - // 注册语音交互服务方法通道 - let voiceInteractionChannel = FlutterMethodChannel( - name: "com.deep_voice.voice_interaction", - binaryMessenger: controller.binaryMessenger - ) - - voiceInteractionChannel.setMethodCallHandler { [weak self, weak voiceInteractionService] (call, result) in - guard let _ = self, let voiceInteractionService = voiceInteractionService else { - result(FlutterError(code: "UNAVAILABLE", message: "语音交互服务实例不可用", details: nil)) - return - } - - switch call.method { - case "startService": - guard let args = call.arguments as? [String: Any], - let azureKey = args["azure_speech_key"] as? String, - let azureRegion = args["azure_speech_region"] as? String, - let volcanoKey = args["volcano_ai_api_key"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "启动服务参数无效", details: nil)) - return - } - - let success = voiceInteractionService.start(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey) - result(success) - - case "stopService": - voiceInteractionService.stop() - result(true) - - case "isServiceRunning": - let isRunning = voiceInteractionService.isServiceRunning() - result(isRunning) - - case "pauseVoiceInteraction": - voiceInteractionService.pauseVoiceInteraction() - result(true) - - default: - result(FlutterMethodNotImplemented) - } - } - - // 注册语音交互流通道 - let voiceInteractionStreamChannel = FlutterEventChannel( - name: "com.deep_voice.voice_interaction_events", - binaryMessenger: controller.binaryMessenger - ) - - // 创建语音交互流处理器(作为局部变量) - let voiceInteractionStreamHandler = VoiceInteractionStreamHandler(voiceInteractionService: voiceInteractionService!) - voiceInteractionStreamChannel.setStreamHandler(voiceInteractionStreamHandler) - } -} - -// 蓝牙事件流处理器 -class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHandler { - private let bluetoothHelper: ClassicBluetoothHelper - private var eventSink: FlutterEventSink? - - init(bluetoothHelper: ClassicBluetoothHelper) { - self.bluetoothHelper = bluetoothHelper - super.init() - - // 设置事件处理器 - bluetoothHelper.setEventHandler(self) - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - // NSLog("BluetoothStreamHandler: 开始监听蓝牙事件") - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - // NSLog("BluetoothStreamHandler: 取消监听蓝牙事件") - eventSink = nil - return nil - } - - // 实现BluetoothEventHandler协议 - func sendEvent(_ event: [String: Any]) { - NSLog("BluetoothStreamHandler: 接收到蓝牙事件: \(event)") - DispatchQueue.main.async { [weak self] in - self?.eventSink?(event) - } - } -} - -// 媒体按钮事件流处理器 -@available(iOS 13.0, *) -class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEventHandler { - private let mediaButtonHelper: BluetoothMediaButtonHelper - private var eventSink: FlutterEventSink? - - init(mediaButtonHelper: BluetoothMediaButtonHelper) { - self.mediaButtonHelper = mediaButtonHelper - super.init() - - // 设置事件处理器 - mediaButtonHelper.setEventHandler(self) - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - NSLog("MediaButtonStreamHandler: 开始监听媒体按钮事件") - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - NSLog("MediaButtonStreamHandler: 取消监听媒体按钮事件") - eventSink = nil - return nil - } - - // 实现MediaButtonEventHandler协议 - func sendEvent(_ event: [String: Any]) { - NSLog("MediaButtonStreamHandler: 收到按钮事件 \(event)") - DispatchQueue.main.async { [weak self] in - self?.eventSink?(event) - } - } -} - -// ASR事件处理器协议 -@available(iOS 13.0, *) -protocol AsrEventHandler: AnyObject { - func sendEvent(_ event: [String: Any]) -} - -// ASR事件流处理器 -@available(iOS 13.0, *) -class AsrStreamHandler: NSObject, FlutterStreamHandler, AsrEventHandler { - private let azureAsrHelper: AzureAsrHelper - private var eventSink: FlutterEventSink? - - init(azureAsrHelper: AzureAsrHelper) { - self.azureAsrHelper = azureAsrHelper - super.init() - - // 设置事件处理器 - azureAsrHelper.setEventHandler { [weak self] event in - self?.sendEvent(event) - } - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - NSLog("AsrStreamHandler: 开始监听ASR事件") - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - NSLog("AsrStreamHandler: 取消监听ASR事件") - eventSink = nil - return nil - } - - // 实现AsrEventHandler协议 - func sendEvent(_ event: [String: Any]) { - // NSLog("AsrStreamHandler: 收到事件 \(event)") - DispatchQueue.main.async { [weak self] in - self?.eventSink?(event) - } - } -} - -// TTS事件处理器协议 -@available(iOS 13.0, *) -protocol TtsEventHandler: AnyObject { - func sendEvent(_ event: [String: Any]) -} - -// TTS事件流处理器 -@available(iOS 13.0, *) -class TtsStreamHandler: NSObject, FlutterStreamHandler, TtsEventHandler { - private let azureTtsHelper: AzureTtsHelper - private var eventSink: FlutterEventSink? - - init(azureTtsHelper: AzureTtsHelper) { - self.azureTtsHelper = azureTtsHelper - super.init() - - // 设置事件处理器 - azureTtsHelper.setEventHandler { [weak self] event in - self?.sendEvent(event) - } - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - NSLog("TtsStreamHandler: 开始监听TTS事件") - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - NSLog("TtsStreamHandler: 取消监听TTS事件") - eventSink = nil - return nil - } - - // 实现TtsEventHandler协议 - func sendEvent(_ event: [String: Any]) { - NSLog("TtsStreamHandler: 收到事件 \(event)") - DispatchQueue.main.async { [weak self] in - self?.eventSink?(event) - } - } -} - -// 火山AI事件流处理器 -@available(iOS 13.0, *) -class VolcanoAIStreamHandler: NSObject, FlutterStreamHandler { - private let volcanoAIService: VolcanoAIService - private var eventSink: FlutterEventSink? - - init(volcanoAIService: VolcanoAIService) { - self.volcanoAIService = volcanoAIService - super.init() - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - eventSink = nil - return nil - } - - // 处理来自Flutter的流式请求 - func handleStreamRequest(messages: [[String: Any]], systemPrompt: String) { - volcanoAIService.sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: VolcanoAIService.StreamCallback( - onToken: { [weak self] token in - guard let self = self, let eventSink = self.eventSink else { return } - - let event: [String: Any] = [ - "type": "token", - "token": token - ] - - DispatchQueue.main.async { - eventSink(event) - } - }, - onComplete: { [weak self] in - guard let self = self, let eventSink = self.eventSink else { return } - - let event: [String: Any] = [ - "type": "complete" - ] - - DispatchQueue.main.async { - eventSink(event) - } - }, - onError: { [weak self] error in - guard let self = self, let eventSink = self.eventSink else { return } - - let event: [String: Any] = [ - "type": "error", - "error": error.localizedDescription - ] - - DispatchQueue.main.async { - eventSink(event) - } - } - )) - } -} - -// 语音交互事件处理器协议 -@available(iOS 13.0, *) -protocol VoiceInteractionEventHandler: AnyObject { - func sendEvent(_ event: [String: Any]) -} - -// 语音交互流处理器 -@available(iOS 13.0, *) -class VoiceInteractionStreamHandler: NSObject, FlutterStreamHandler, VoiceInteractionEventHandler { - private let voiceInteractionService: VoiceInteractionService - private var eventSink: FlutterEventSink? - - init(voiceInteractionService: VoiceInteractionService) { - self.voiceInteractionService = voiceInteractionService - super.init() - - // 设置事件处理器 - voiceInteractionService.setEventHandler(self) - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - NSLog("VoiceInteractionStreamHandler: 开始监听语音交互事件") - self.eventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - NSLog("VoiceInteractionStreamHandler: 取消监听语音交互事件") - eventSink = nil - return nil - } - - // 实现VoiceInteractionEventHandler协议 - func sendEvent(_ event: [String: Any]) { - NSLog("VoiceInteractionStreamHandler: 收到事件 \(event)") - DispatchQueue.main.async { [weak self] in - self?.eventSink?(event) - } - } } diff --git a/ios/Runner/AudioSessionManager.swift b/ios/Runner/AudioSessionManager.swift deleted file mode 100644 index d41d2fbf5..000000000 --- a/ios/Runner/AudioSessionManager.swift +++ /dev/null @@ -1,589 +0,0 @@ -import Foundation -import AVFoundation -import UIKit - -/// 统一音频会话管理类 -/// 用于统一管理所有组件的音频会话配置 -@objc class AudioSessionManager: NSObject { - // MARK: - 单例实现 - - @objc static let shared = AudioSessionManager() - - // 音频会话 - private let audioSession = AVAudioSession.sharedInstance() - - // 音频会话状态 - private var isConfigured = false - private var isActive = false - - // 当前配置类型 - private var currentCategory: AVAudioSession.Category? - private var currentMode: AVAudioSession.Mode? - private var currentOptions: AVAudioSession.CategoryOptions? - - // 中断监听器 - private var interruptionObserver: NSObjectProtocol? - - // 路由变化监听器 - private var routeChangeObserver: NSObjectProtocol? - - // 私有初始化方法,确保只能通过shared访问 - private override init() { - super.init() - NSLog("[AudioSessionManager] 音频会话管理器已初始化") - setupNotifications() - - } - - deinit { - removeNotifications() - } - - - // MARK: - 通知设置 - - private func setupNotifications() { - // 监听中断事件 - interruptionObserver = NotificationCenter.default.addObserver( - forName: AVAudioSession.interruptionNotification, - object: nil, - queue: .main) { [weak self] notification in - self?.handleInterruption(notification) - } - - // 监听路由变化事件 - routeChangeObserver = NotificationCenter.default.addObserver( - forName: AVAudioSession.routeChangeNotification, - object: nil, - queue: .main) { [weak self] notification in - self?.handleRouteChange(notification) - } - - // 监听应用进入后台/前台 - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppDidEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil) - - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppWillEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil) - } - - private func removeNotifications() { - if let interruptionObserver = interruptionObserver { - NotificationCenter.default.removeObserver(interruptionObserver) - } - - if let routeChangeObserver = routeChangeObserver { - NotificationCenter.default.removeObserver(routeChangeObserver) - } - - NotificationCenter.default.removeObserver(self) - } - - // MARK: - 通知处理 - - private func handleInterruption(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { - return - } - - switch type { - case .began: - // 中断开始 - NSLog("[AudioSessionManager] 音频会话被中断") - isActive = false - - case .ended: - // 中断结束,尝试恢复 - if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { - let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) - if options.contains(.shouldResume) { - NSLog("[AudioSessionManager] 尝试恢复被中断的音频会话") - try? activateSession() - } - } - - @unknown default: - break - } - } - - private func handleRouteChange(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, - let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { - return - } - - switch reason { - case .newDeviceAvailable: - // 新设备连接 - let currentRoute = audioSession.currentRoute - for output in currentRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP { - NSLog("[AudioSessionManager] 检测到蓝牙音频设备连接: \(output.portName)") - // 蓝牙设备连接,可能需要更新配置 - if isActive && currentOptions?.contains(.allowBluetooth) == true { - try? forceReconfigure() - } - break - } - - case .oldDeviceUnavailable: - // 设备断开连接 - if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { - for output in previousRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP { - NSLog("[AudioSessionManager] 蓝牙音频设备断开连接: \(output.portName)") - // 蓝牙设备断开,重新配置到内置扬声器 - if isActive { - try? forceReconfigure() - } - break - } - } - - default: - break - } - } - - @objc private func handleAppDidEnterBackground() { - NSLog("[AudioSessionManager] 应用进入后台") - // 应用进入后台时可能需要调整音频会话 - - } - - @objc private func handleAppWillEnterForeground() { - NSLog("[AudioSessionManager] 应用即将进入前台") - // 应用返回前台,检查并恢复音频会话 - if isConfigured && !isActive { - try? activateSession() - } - } - - // MARK: - 音频会话配置 - - /// 统一配置方法 - 所有音频配置都通过此方法进行 - @objc func configureSession(category: AVAudioSession.Category, - mode: AVAudioSession.Mode, - options: AVAudioSession.CategoryOptions, - force: Bool = false) throws { - - NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)") - - // 如果已配置且不强制重新配置,则直接返回 - if isConfigured && !force && - currentCategory == category && - currentMode == mode && - currentOptions == options { - NSLog("[AudioSessionManager] 音频会话已配置,跳过配置") - return - } - - do { - // NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)") - - // 暂时停用当前会话(如果正在活动) - if isActive { - try audioSession.setActive(false, options: .notifyOthersOnDeactivation) - } - - // 设置新的分类、模式和选项 - try audioSession.setCategory(category, mode: mode, options: options) - - // 设置采样率和缓冲区大小(对于语音识别优化) - // if mode == .spokenAudio { - // try audioSession.setPreferredSampleRate(16000) - // try audioSession.setPreferredIOBufferDuration(0.01) // 10ms - // } - - // 重新激活会话 - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - isActive = true - // 切换到扬声器 - // 记录当前配置 - currentCategory = category - currentMode = mode - currentOptions = options - isConfigured = true - - // 检查并设置首选的输入设备(如果需要) - if category == .playAndRecord || category == .record { - try configurePreferredInput() - } - return - } catch { - NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") - throw error - } - } - - /// 配置首选输入设备 - private func configurePreferredInput() throws { - // 获取可用输入设备 - guard let availableInputs = audioSession.availableInputs else { - NSLog("[AudioSessionManager] 无法获取可用输入设备") - return - } - - // 首选顺序:有线耳机 > 蓝牙耳机 > 内置麦克风 - var preferredInput: AVAudioSessionPortDescription? = nil - - // 先尝试找到有线耳机 - if let wiredHeadset = availableInputs.first(where: { $0.portType == .headsetMic }) { - preferredInput = wiredHeadset - NSLog("[AudioSessionManager] 设置首选输入设备: 有线耳机") - } - // 如果没有有线耳机,尝试使用蓝牙 - else if let bluetooth = availableInputs.first(where: { $0.portType == .bluetoothHFP }) { - preferredInput = bluetooth - NSLog("[AudioSessionManager] 设置首选输入设备: 蓝牙耳机") - } - // 如果没有外部设备,使用内置麦克风 - else if let builtIn = availableInputs.first(where: { $0.portType == .builtInMic }) { - preferredInput = builtIn - NSLog("[AudioSessionManager] 设置首选输入设备: 内置麦克风") - } - - // 设置首选输入设备 - if let input = preferredInput { - try audioSession.setPreferredInput(input) - } - } - - - - /// 激活音频会话(如果已配置) - @objc func activateSession() throws { - if !isConfigured { - NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话,应用默认配置") - // 未配置,应用默认配置 - // try configureSession( - // category: .playAndRecord, - // mode: .spokenAudio, - // options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker], - // force: true - // ) - - try configureSession( - category: .playAndRecord, // 允许同时录音和播放 - mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 - options: [ - .allowBluetooth, // 允许蓝牙设备 - .defaultToSpeaker, // 默认使用扬声器 - .mixWithOthers // 允许与其他应用混音 - ], - force: false // 是否强制重新配置 - ) - - - } else if !isActive { - NSLog("[AudioSessionManager] 激活已配置的音频会话") - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - isActive = true - } - } - - /// 停用音频会话(保留配置) - @objc func deactivateSession() throws { - if isActive { - NSLog("[AudioSessionManager] 停用音频会话") - try audioSession.setActive(false, options: .notifyOthersOnDeactivation) - isActive = false - } - } - - /// 重新配置当前会话 - @objc func forceReconfigure() throws { - NSLog("[AudioSessionManager] 强制重新配置音频会话") - // try configureSession( - // category: currentCategory ?? .playAndRecord, - // mode: currentMode ?? .default, - // options: currentOptions ?? [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker], - // force: true - // ) - - try configureSession( - category: .playAndRecord, // 允许同时录音和播放 - mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 - options: [ - .allowBluetooth, // 允许蓝牙设备 - .defaultToSpeaker, // 默认使用扬声器 - .mixWithOthers // 允许与其他应用混音 - ], - force: false // 是否强制重新配置 - ) - try audioSession.overrideOutputAudioPort(.speaker) - - - } - - // MARK: - 状态检查 - - /// 检查麦克风权限 - @objc func checkMicrophonePermission(completion: @escaping (Bool) -> Void) { - switch AVAudioSession.sharedInstance().recordPermission { - case .granted: - completion(true) - case .denied: - NSLog("[AudioSessionManager] 麦克风权限被拒绝") - completion(false) - case .undetermined: - NSLog("[AudioSessionManager] 请求麦克风权限") - AVAudioSession.sharedInstance().requestRecordPermission { granted in - DispatchQueue.main.async { - completion(granted) - } - } - @unknown default: - completion(false) - } - } - - /// 获取当前音频会话的配置信息 - @objc func getCurrentConfiguration() -> [String: Any] { - return [ - "isConfigured": isConfigured, - "isActive": isActive, - "category": String(describing: currentCategory), - "mode": String(describing: currentMode), - "bluetoothEnabled": currentOptions?.contains(.allowBluetooth) == true, - "currentInputs": audioSession.currentRoute.inputs.map { [$0.portType.rawValue: $0.portName] }, - "currentOutputs": audioSession.currentRoute.outputs.map { [$0.portType.rawValue: $0.portName] } - ] - } - - /// 检查当前是否有蓝牙耳机连接 - @objc func hasBluetoothHeadsetConnected() -> Bool { - let outputs = audioSession.currentRoute.outputs - return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE } - } - - /// 检查蓝牙是否启用 - @objc func isBluetoothEnabled() -> Bool { - // 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断 - - // 检查音频会话是否配置了允许蓝牙 - if let options = currentOptions { - return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP) - } - - // 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备 - let outputs = audioSession.currentRoute.outputs - let hasBluetooth = outputs.contains { - $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE - } - - return hasBluetooth - } - - /// 获取当前音频路由 - @objc func getCurrentAudioRoute() -> String { - let outputs = audioSession.currentRoute.outputs - if let output = outputs.first { - return "\(output.portType.rawValue) (\(output.portName))" - } - return "unknown" - } - - /// 获取当前输入设备 - @objc func getCurrentInputDevice() -> String { - let inputs = audioSession.currentRoute.inputs - if let input = inputs.first { - return "\(input.portType.rawValue) (\(input.portName))" - } - return "unknown" - } - - // MARK: - 路由管理和监听 - - /// 获取当前路由信息 - @objc func getCurrentRoute() -> AVAudioSessionRouteDescription { - return audioSession.currentRoute - } - - /// 添加路由变更监听器 - /// - Parameters: - /// - observer: 监听对象 - /// - selector: 监听方法选择器 - @objc func addRouteChangeListener(_ observer: Any, selector: Selector) { - NotificationCenter.default.addObserver( - observer, - selector: selector, - name: AVAudioSession.routeChangeNotification, - object: nil - ) - } - - /// 移除路由变更监听器 - /// - Parameter observer: 要移除的监听对象 - @objc func removeRouteChangeListener(_ observer: Any) { - NotificationCenter.default.removeObserver( - observer, - name: AVAudioSession.routeChangeNotification, - object: nil - ) - } - /// 获取设备类型描述 - private func getDeviceType(portType: AVAudioSession.Port) -> String { - switch portType { - case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE: - return "bluetooth" - case .headphones, .headsetMic: - return "wired" - case .airPlay: - return "airplay" - default: - return "unknown" - } - } - - - - - // /// 为Azure语音识别专门配置的音频会话 - // /// 此函数优化Azure语音识别的音频处理配置,提供更强的回音消除能力 - // /// - Parameters: - // /// - force: 是否强制重新配置 - // /// - configureAdditionalSettings: 配置完成后进行额外的音频设置 - // /// - Returns: 配置是否成功 - // @objc func configureForAzureSpeechRecognition(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool { - // NSLog("[AudioSessionManager] 配置Azure ASR语音识别的音频会话") - // do { - // // 配置音频会话以优化语音识别 - // try configureSession( - // category: .playAndRecord, // 允许同时录音和播放 - // mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 - // options: [ - // .allowBluetooth, // 允许蓝牙设备 - // .defaultToSpeaker, // 默认使用扬声器 - // .mixWithOthers // 允许与其他应用混音 - // ], - // force: force // 是否强制重新配置 - // ) - - // // 额外的优化配置 - // if configureAdditionalSettings { - // // 获取当前是否使用耳机 - // let currentRoute = audioSession.currentRoute - // let hasHeadphones = currentRoute.outputs.contains { - // $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP - // } - - // // 设置采样率为16kHz(Azure Speech API推荐) - // try audioSession.setPreferredSampleRate(16000.0) - - // // 设置较小的缓冲区大小以减少延迟 - // try audioSession.setPreferredIOBufferDuration(0.01) - - // // 根据是否有耳机连接调整输入增益 - // if !hasHeadphones { - // // 无耳机时降低输入增益以减少回音 - // try audioSession.setInputGain(0.8) - // NSLog("[AudioSessionManager] 启用扬声器回音消除优化") - // } else { - // // 使用耳机时可以使用较高增益 - // try audioSession.setInputGain(1.0) - // NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式") - // } - // } - - // NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话") - // return true - // } catch { - // NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)") - // return false - // } - // } - - -@objc func configureForVoiceInteraction(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool { - NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话") - do { - // 配置音频会话以优化语音识别 - try configureSession( - category: .playAndRecord, // 允许同时录音和播放 - mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果 - options: [ - .allowBluetooth, // 允许蓝牙设备 - .defaultToSpeaker, // 默认使用扬声器 - .mixWithOthers // 允许与其他应用混音 - ], - force: force // 是否强制重新配置 - ) - - try audioSession.overrideOutputAudioPort(.speaker) - - // 额外的优化配置 - if configureAdditionalSettings { - // 获取当前是否使用耳机 - let currentRoute = audioSession.currentRoute - let hasHeadphones = currentRoute.outputs.contains { - $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP - } - - // 设置采样率为16kHz(Azure Speech API推荐) - try audioSession.setPreferredSampleRate(16000.0) - - // 设置较小的缓冲区大小以减少延迟 - try audioSession.setPreferredIOBufferDuration(0.01) - - // 根据是否有耳机连接调整输入增益 - if !hasHeadphones { - // 无耳机时降低输入增益以减少回音 - try audioSession.setInputGain(0.8) - NSLog("[AudioSessionManager] 启用扬声器回音消除优化") - } else { - // 使用耳机时可以使用较高增益 - try audioSession.setInputGain(1.0) - NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式") - } - } - - NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话") - return true - } catch { - NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)") - return false - } - } - - - /// 配置用于语音交互(同时支持ASR和TTS)的音频会话 - /// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景 - /// - Returns: 配置是否成功 - // @objc func configureForVoiceInteraction(force: Bool = false) -> Bool { - // do { - // // 配置音频会话以支持语音交互 - // // 使用playAndRecord类别允许同时录音和播放 - // // 使用spokenAudio模式优化语音交互 - // try configureSession( - // category: .playAndRecord, - // mode: .voiceChat, - // options: [ - // // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出 - // .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出 - // .defaultToSpeaker, // 默认使用扬声器输出 - // .mixWithOthers - // ], - // force: force // 强制重新配置,确保设置生效 - // ) - - // // 设置首选输入设备 - // try configurePreferredInput() - - // NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话") - // return true - // } catch { - // NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)") - // return false - // } - // } - -} \ No newline at end of file diff --git a/ios/Runner/AzureAsrHelper.swift b/ios/Runner/AzureAsrHelper.swift deleted file mode 100644 index 5a2b73189..000000000 --- a/ios/Runner/AzureAsrHelper.swift +++ /dev/null @@ -1,813 +0,0 @@ -import Foundation -import MicrosoftCognitiveServicesSpeech -import AVFoundation -import AudioToolbox - -/// Azure ASR工具类,负责实现语音识别服务接口 -@available(iOS 13.0, *) -class AzureAsrHelper: NSObject { - // MARK: - 属性 - - /// 是否使用自定义音频处理器 - 设置为false时将使用系统默认麦克风输入 - private let useCustomAudioProcessor = true - - /// 事件处理回调 - private var eventHandler: (([String: Any]) -> Void)? - - /// 语音配置信息 - private var speechSubscriptionKey: String = "" - private var serviceRegion: String = "" - - /// 语音识别相关 - private var speechConfig: SPXSpeechConfiguration? - private var recognizer: SPXSpeechRecognizer? - private var audioConfig: SPXAudioConfiguration? - - /// 添加自定义音频处理相关 - private var pushStream: SPXPushAudioInputStream? - private var audioProcessor: CustomAudioProcessor? - private var isProcessingAudio = false - private var audioProcessingTimer: Timer? - - /// 状态标志 - private var isInitialized = false - private var _isContinuousRecognitionActive = false - - /// 当前语言和支持的语言 - private var currentLanguage = "zh-CN" - private var supportedLanguages: [String] = ["zh-CN", "en-US"] - private var isAutoDetectLanguage = false - - /// 音频会话管理器 - private let audioSessionManager = AudioSessionManager.shared - - // MARK: - 初始化 - - override init() { - super.init() - } - - deinit { - dispose() - } - - - // MARK: - ASR Service 接口实现 - - /// 初始化语音识别服务 - /// - Parameters: - /// - speechSubscriptionKey: Azure 语音服务订阅密钥 - /// - serviceRegion: Azure 服务区域 (如 eastasia) - /// - supportedLanguages: 支持的语言代码数组 (可选) - /// - Returns: 初始化是否成功 - func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool { - print("[AzureAsrHelper] 初始化 Azure 语音服务") - - // 检查配置是否为空 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureAsrHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - // 检查语言参数 - if let languages = supportedLanguages, languages.isEmpty { - print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言") - } - - // 释放之前的资源 - dispose() - - // 记录配置信息 - self.speechSubscriptionKey = speechSubscriptionKey - self.serviceRegion = serviceRegion - - // 设置语言 - if let languages = supportedLanguages, !languages.isEmpty { - self.supportedLanguages = languages - } - - // 根据支持的语言数量决定是否启用自动语言检测 - isAutoDetectLanguage = self.supportedLanguages.count >= 2 - - // 如果只有一种语言,设置为当前语言 - if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty { - currentLanguage = self.supportedLanguages[0] - } - - // 创建识别器和设置回调 - if !createRecognizerAndSetupCallbacks() { - return false - } - - print("[AzureAsrHelper] Azure 语音服务初始化成功") - isInitialized = true - return true - } - - /// 创建识别器并设置回调 - private func createRecognizerAndSetupCallbacks() -> Bool { - // 释放之前的 recognizer - recognizer = nil - audioConfig = nil - - do { - // 创建语音配置 - speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion) - - // 设置音频输入参数 - try setupAudioSession() - - // 根据设置决定是否使用自定义音频处理器 - if useCustomAudioProcessor { - // 创建自定义音频流和处理器,替代默认的麦克风输入 - pushStream = try SPXPushAudioInputStream() - audioConfig = try SPXAudioConfiguration(streamInput: pushStream!) - - // 初始化自定义音频处理器 - audioProcessor = CustomAudioProcessor() - } else { - // 使用默认麦克风输入 - audioConfig = try SPXAudioConfiguration() - } - - // 设置语言配置 - if isAutoDetectLanguage { - // 设置自动语言检测 - speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) - - // 创建自动语言检测配置 - let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) - - // 创建识别器 - recognizer = try SPXSpeechRecognizer( - speechConfiguration: speechConfig!, - autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig, - audioConfiguration: audioConfig! - ) - } else { - // 设置指定的识别语言 - speechConfig?.speechRecognitionLanguage = currentLanguage - - // 创建识别器 - recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) - } - - // 设置所有回调 - setupAllCallbacks() - - return true - } catch { - print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)") - eventHandler?(["type": "error", "message": "创建识别器失败: \(error.localizedDescription)"]) - return false - } - } - - /// 设置音频会话 - private func setupAudioSession() throws { - print("[AzureAsrHelper] 开始配置音频会话...") - - // 使用AudioSessionManager配置音频会话,使用专门为Azure ASR优化的配置 - let success = audioSessionManager.configureForVoiceInteraction() - if !success { - print("[AzureAsrHelper] 警告: 通过AudioSessionManager配置音频会话失败") - } - } - - - /// 设置所有回调 - private func setupAllCallbacks() { - guard let recognizer = recognizer else { return } - - // 最终识别结果 - recognizer.addRecognizedEventHandler { [weak self] _, event in - guard let self = self else { return } - - if event.result.reason == SPXResultReason.recognizedSpeech { - let detectedLanguage = self.getDetectedLanguage(from: event.result) - print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)") - self.eventHandler?(["type": "result", - "text": event.result.text ?? "", - "detectedLanguage": detectedLanguage - ]) - } - } - - // 识别中事件 - recognizer.addRecognizingEventHandler { [weak self] _, event in - guard let self = self else { return } - - if event.result.reason == SPXResultReason.recognizingSpeech { - let detectedLanguage = self.getDetectedLanguage(from: event.result) - // print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)") - self.eventHandler?(["type": "recognizing", - "text": event.result.text ?? "", - "detectedLanguage": detectedLanguage - ]) - } - } - - // 会话事件 - recognizer.addSessionStartedEventHandler { [weak self] _, _ in - guard let self = self else { return } - - print("[AzureAsrHelper] 识别会话已开始") - self._isContinuousRecognitionActive = true - self.eventHandler?(["type": "sessionStarted"]) - } - - recognizer.addSessionStoppedEventHandler { [weak self] _, _ in - guard let self = self else { return } - - print("[AzureAsrHelper] 识别会话已结束") - self._isContinuousRecognitionActive = false - self.eventHandler?(["type": "sessionStopped"]) - } - - // 取消事件 - recognizer.addCanceledEventHandler { [weak self] _, event in - guard let self = self else { return } - - let reason = event.reason.rawValue - let errorDetails = event.errorDetails ?? "未知错误" - - print("[AzureAsrHelper] 识别取消: \(errorDetails)") - - self.eventHandler?(["type": "canceled", "reason": reason, "errorDetails": errorDetails]) - - self._isContinuousRecognitionActive = false - } - } - - /// 执行一次性语音识别 - /// - Returns: 是否成功启动识别 - func recognizeOnce() -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureAsrHelper] 错误: 语音服务未初始化") - eventHandler?(["type": "error", "message": "语音服务未初始化"]) - return false - } - - // 检查参数有效性 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureAsrHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - // 如果正在连续识别,先停止 - if _isContinuousRecognitionActive { - stopContinuousRecognition() - } - - // 确保识别器已创建 - if recognizer == nil && !createRecognizerAndSetupCallbacks() { - return false - } - - do { - // 启动音频处理 - startAudioProcessing() - - // 通知会话开始 - eventHandler?(["type": "sessionStarted"]) - - // 执行识别 - try recognizer?.recognizeOnceAsync { [weak self] result in - guard let self = self else { return } - - // 停止音频处理 - self.stopAudioProcessing() - - if result.reason == SPXResultReason.recognizedSpeech { - let detectedLanguage = self.getDetectedLanguage(from: result) - self.eventHandler?(["type": "result", - "text": result.text ?? "", - "detectedLanguage": detectedLanguage - ]) - } else if result.reason == SPXResultReason.noMatch { - print("[AzureAsrHelper] 无匹配结果") - self.eventHandler?(["type": "noMatch"]) - } else if result.reason == SPXResultReason.canceled { - do { - let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result) - let errorDetails = details.errorDetails ?? "未知错误" - self.eventHandler?(["type": "error", "message": "识别取消: \(errorDetails)"]) - } catch { - print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)") - self.eventHandler?(["type": "error", "message": "识别取消,无法获取详细原因"]) - } - } - } - - return true - } catch { - print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)") - eventHandler?(["type": "error", "message": "识别异常: \(error.localizedDescription)"]) - stopAudioProcessing() - return false - } - } - - /// 开始连续语音识别 - /// - Returns: 是否成功启动识别 - func startContinuousRecognition() -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureAsrHelper] 错误: 语音服务未初始化") - eventHandler?(["type": "error", "message": "语音服务未初始化"]) - return false - } - - // 检查参数有效性 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureAsrHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - // 检查是否已经在识别 - if _isContinuousRecognitionActive { - print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求") - return true - } - - // 确保识别器已创建 - if recognizer == nil { - print("[AzureAsrHelper] 尝试重新创建识别器...") - if !createRecognizerAndSetupCallbacks() { - eventHandler?(["type": "error", "message": "无法创建语音识别器"]) - return false - } - } - - // 启动音频处理 - startAudioProcessing() - - // 尝试启动连续识别 - do { - // 启动连续识别 - try recognizer?.startContinuousRecognition() - _isContinuousRecognitionActive = true - - print("[AzureAsrHelper] 连续识别已启动") - return true - } catch { - _isContinuousRecognitionActive = false - print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)") - - // 停止音频处理 - stopAudioProcessing() - - // 发送错误通知 - eventHandler?(["type": "error", "message": "开始连续识别失败: \(error.localizedDescription)"]) - - return false - } - } - - - /// 停止连续语音识别 - /// - Returns: 是否成功停止识别 - func stopContinuousRecognition() -> Bool { - // 停止音频处理 - stopAudioProcessing() - - // 检查是否初始化 - if !isInitialized { - print("[AzureAsrHelper] 错误: 语音服务未初始化") - eventHandler?(["type": "error", "message": "语音服务未初始化"]) - return false - } - - if !_isContinuousRecognitionActive { - print("[AzureAsrHelper] 未进行连续识别,忽略停止请求") - return true - } - - print("[AzureAsrHelper] 停止连续语音识别") - - // 防止空指针异常 - guard let recognizer = recognizer else { - print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃") - _isContinuousRecognitionActive = false - eventHandler?(["type": "sessionStopped"]) - return true - } - - // 先标记为非活跃状态,防止重复调用 - _isContinuousRecognitionActive = false - - // 异步执行停止操作,避免阻塞主线程 - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - - do { - // 在后台线程执行停止操作 - try recognizer.stopContinuousRecognition() - - // 主线程回调通知结果 - DispatchQueue.main.async { - print("[AzureAsrHelper] 连续识别已停止") - self.eventHandler?(["type": "sessionStopped"]) - } - } catch { - // 主线程回调通知错误 - DispatchQueue.main.async { - print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") - // 停止失败,恢复状态 - self._isContinuousRecognitionActive = true - self.eventHandler?(["type": "error", "message": "停止识别失败: \(error.localizedDescription)"]) - } - } - } - - // 立即返回,不阻塞调用方 - return true - } - - /// 检查连续识别是否活跃 - /// - Returns: 连续识别是否处于活跃状态 - func isContinuousRecognitionActive() -> Bool { - return _isContinuousRecognitionActive - } - - /// 释放资源 - func dispose() { - // 停止音频处理 - stopAudioProcessing() - - // 尝试停止所有识别操作 - if _isContinuousRecognitionActive { - do { - if let recognizer = recognizer { - try recognizer.stopContinuousRecognition() - print("[AzureAsrHelper] 连续识别已停止") - } - } catch { - print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)") - } - _isContinuousRecognitionActive = false - } - - // 释放识别器 - recognizer = nil - audioConfig = nil - speechConfig = nil - pushStream = nil - audioProcessor = nil - - // 重置状态 - isInitialized = false - - print("[AzureAsrHelper] 资源已释放") - } - - /// 从结果中获取检测到的语言 - private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { - if isAutoDetectLanguage { - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - return langResult.language ?? currentLanguage - } catch { - print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)") - return currentLanguage - } - } else { - return currentLanguage - } - } - - /// 设置事件处理器 - @objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) { - self.eventHandler = handler - } - - // MARK: - 音频处理 - - /// 开始音频处理 - private func startAudioProcessing() { - // 如果未使用自定义音频处理器,跳过 - if !useCustomAudioProcessor { - return - } - - guard !isProcessingAudio, let audioProcessor = audioProcessor else { return } - - isProcessingAudio = true - - // 启动音频处理器 - if !audioProcessor.startRecord() { - print("[AzureAsrHelper] 错误: 启动音频处理器失败") - eventHandler?(["type": "error", "message": "启动音频处理器失败"]) - return - } - - // 启动音频处理定时器 - audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in - guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else { - return - } - - // 读取处理后的音频数据 - var bytes = [UInt8](repeating: 0, count: 2560) - let bytesRead = processor.read(bytes: &bytes) - - if bytesRead > 0 { - // 推送数据到Azure语音服务 - let data = Data(bytes: bytes, count: bytesRead) - stream.write(data) - - // 通知音频数据可用(可选) - // self.eventHandler?(["type": "audioData", "data": bytes]) - } - } - - print("[AzureAsrHelper] 音频处理已启动") - } - - /// 停止音频处理 - private func stopAudioProcessing() { - // 如果未使用自定义音频处理器,跳过 - if !useCustomAudioProcessor { - return - } - - // 停止定时器 - audioProcessingTimer?.invalidate() - audioProcessingTimer = nil - - // 停止音频处理器 - audioProcessor?.stopRecord() - - isProcessingAudio = false - print("[AzureAsrHelper] 音频处理已停止") - } -} - -// MARK: - 自定义音频处理器 - -@available(iOS 13.0, *) -class CustomAudioProcessor: NSObject { - // 音频单元 - private var ioUnit: AudioUnit? - - // 音频格式 - private var audioFormat: AudioStreamBasicDescription - - // 音频缓冲 - private var audioBufferList: AudioBufferList - private var audioList: [Float] = [] - private let audioListQueue = DispatchQueue(label: "audioListQueue") - - // 回音消除状态 - private var isEchoCancellationEnabled = true - - override init() { - // 设置音频格式 - 16kHz, 16位, 单声道 - audioFormat = AudioStreamBasicDescription( - mSampleRate: 16000.0, - mFormatID: kAudioFormatLinearPCM, - mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, - mBytesPerPacket: 2, - mFramesPerPacket: 1, - mBytesPerFrame: 2, - mChannelsPerFrame: 1, - mBitsPerChannel: 16, - mReserved: 0 - ) - - // 初始化音频缓冲 - audioBufferList = AudioBufferList( - mNumberBuffers: 1, - mBuffers: AudioBuffer( - mNumberChannels: 1, - mDataByteSize: 4096, - mData: malloc(4096) - ) - ) - - super.init() - } - - deinit { - stopRecord() - free(audioBufferList.mBuffers.mData) - } - - /// 启动音频处理 - /// - Returns: 是否成功启动 - func startRecord() -> Bool { - print("[CustomAudioProcessor] 配置音频单元") - - // 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除 - var ioUnitDescription = AudioComponentDescription( - componentType: kAudioUnitType_Output, - componentSubType: kAudioUnitSubType_VoiceProcessingIO, - componentManufacturer: kAudioUnitManufacturer_Apple, - componentFlags: 0, - componentFlagsMask: 0 - ) - - // 查找音频组件 - guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else { - print("[CustomAudioProcessor] 错误: 未找到音频组件") - return false - } - - // 创建音频单元实例 - if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") { - ioUnit = nil - return false - } - - // 启用输入端口 - var enableInput: UInt32 = 1 - let kInputBus: AudioUnitElement = 1 - let kOutputBus: AudioUnitElement = 0 - if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, - kAudioUnitScope_Input, kInputBus, &enableInput, - UInt32(MemoryLayout.size)), "启用输入端口") { - return false - } - - // 禁用输出端口 (我们只需要输入) - var enableOutput: UInt32 = 0 - if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, - kAudioUnitScope_Output, kOutputBus, - &enableOutput, UInt32(MemoryLayout.size)), "禁用输出端口") { - return false - } - - // 设置缓冲区分配标志 - var flag: UInt32 = 0 - if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer, - kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout.size)), "设置缓冲区分配标志") { - return false - } - - // 设置音频格式 - let size = UInt32(MemoryLayout.size) - if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") { - return false - } - - if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") { - return false - } - - // 启用回音消除 - 注意: kAUVoiceIOProperty_BypassVoiceProcessing值为1时表示绕过处理,值为0表示启用处理 - if isEchoCancellationEnabled { - var echoCancellation: UInt32 = 0 // 0表示不绕过,即启用回音消除 - AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing, - kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout.size)) - } - - // 设置输入回调 - 当有新音频数据时调用 - var inputCallback = AURenderCallbackStruct( - inputProc: CustomAudioProcessor.onAudioDataAvailable, - inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) - ) - - if checkError(AudioUnitSetProperty(ioUnit!, - kAudioOutputUnitProperty_SetInputCallback, - kAudioUnitScope_Global, kInputBus, - &inputCallback, UInt32(MemoryLayout.size)), "设置输入回调") { - return false - } - - // 初始化音频单元 - var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元") - while hasError { - Thread.sleep(forTimeInterval: 0.1) - hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元") - } - - // 启动音频单元 - hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元") - - print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")") - return !hasError - } - - /// 停止音频处理 - func stopRecord() { - print("[CustomAudioProcessor] 停止音频处理器") - - if let ioUnit = ioUnit { - // 停止音频单元 - _ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元") - - // 关闭音频单元 - _ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元") - _ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元") - - self.ioUnit = nil - } - - // 清空音频数据缓冲 - audioListQueue.sync { - audioList.removeAll() - } - } - - /// 音频数据回调 - 当有新的音频数据可用时调用 - private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in - // 获取实例 - let processor = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() - - // 计算预期数据大小 - let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame - - // 确保缓冲区足够大 - if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize { - processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize)) - processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize - } - - // 渲染音频数据 - let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp, - inBusNumber, inNumberFrames, &processor.audioBufferList), - "渲染音频数据") - - // 将Int16数据转换为浮点数据进行处理 - var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames)) - let buffer = processor.audioBufferList.mBuffers - let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self) - - for j in 0...size)) { - // 归一化到[-1.0, 1.0]范围 - audioDataFloat[j] = Float(bufferData[j]) / 32768.0 - } - - // 保存处理后的数据 - if status == noErr { - processor.audioListQueue.async { - processor.audioList.append(contentsOf: audioDataFloat) - } - } - - return status - } - - /// 读取处理后的音频数据 - /// - Parameter bytes: 输出字节数组 - /// - Returns: 读取的字节数 - func read(bytes: inout [UInt8]) -> Int { - return audioListQueue.sync { - // 如果没有数据,返回0 - if audioList.isEmpty { - return 0 - } - - // 确保有足够的数据 (至少1280个样本) - if audioList.count < 1280 { - return 0 - } - - // 读取一帧数据 (1280个样本) - let frameLength = 1280 - let buffer = Array(audioList.prefix(frameLength)) - audioList.removeFirst(frameLength) - - // 将浮点数据转回Int16格式 - var int16Data = buffer.map { Int16($0 * 32767) } - - // 转换为字节数组 - let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count)) - bytes = [UInt8](data) - - // 每个样本2字节 (16位PCM) - return frameLength * 2 - } - } - - /// 检查错误并打印日志 - /// - Parameters: - /// - status: 操作状态 - /// - operation: 操作描述 - /// - Returns: 是否发生错误 - private func checkError(_ status: OSStatus, _ operation: String) -> Bool { - if status != noErr { - print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)") - return true - } - return false - } - - /// 检查OSStatus并返回状态 - /// - Parameters: - /// - status: 操作状态 - /// - operation: 操作描述 - /// - Returns: 原始状态 - private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus { - if status != noErr { - print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)") - } - return status - } -} \ No newline at end of file diff --git a/ios/Runner/AzureTtsHelper.swift b/ios/Runner/AzureTtsHelper.swift deleted file mode 100644 index f62c3f579..000000000 --- a/ios/Runner/AzureTtsHelper.swift +++ /dev/null @@ -1,454 +0,0 @@ -import Foundation -import MicrosoftCognitiveServicesSpeech -import AVFoundation - -/// Azure TTS工具类,负责实现TTS服务接口 -@available(iOS 13.0, *) -class AzureTtsHelper: NSObject { - // MARK: - 属性 - - /// 事件处理回调 - private var eventHandler: (([String: Any]) -> Void)? - - /// 语音配置信息 - private var speechSubscriptionKey: String = "" - private var serviceRegion: String = "" - - /// 语音合成配置 - private var speechConfig: SPXSpeechConfiguration? - - /// 语音合成器 - private var synthesizer: SPXSpeechSynthesizer? - - /// 是否初始化成功 - private var isInitialized = false - - /// 当前是否正在播放 - private var _isSpeaking = false - - /// 音频会话配置 - private var isAudioSessionConfigured = false - - /// 音频会话管理器 - private let audioSessionManager = AudioSessionManager.shared - - // MARK: - 语音设置 - - /// 当前语音 - private var currentVoice = "zh-CN-XiaoxiaoNeural" - - /// 支持的语音映射 - private var voiceMap: [String: String] = [ - "zh-CN": "zh-CN-XiaoxiaoNeural", - "en-US": "en-US-JennyNeural", - "ja-JP": "ja-JP-NanamiNeural", - "ko-KR": "ko-KR-SunHiNeural", - "zh-TW": "zh-TW-HsiaoChenNeural", - "zh-HK": "zh-HK-HiuMaanNeural" - ] - - /// 当前语音合成参数 - private var currentSpeechRate = "0%" - private var currentPitch = "0%" - private var currentVolume = "100%" - - // MARK: - 初始化 - - override init() { - super.init() - } - - deinit { - dispose() - } - - /// 设置事件处理器 - @objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) { - self.eventHandler = handler - } - - // MARK: - TTS 接口实现 - - /// 初始化语音合成服务 - /// - Parameters: - /// - speechSubscriptionKey: Azure 语音服务订阅密钥 - /// - serviceRegion: Azure 服务区域 (如 eastasia) - /// - language: 语言代码 (默认 zh-CN) - /// - Returns: 初始化是否成功 - func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool { - print("[AzureTtsHelper] 初始化语音合成服务") - - // 检查配置是否为空 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureTtsHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - // 释放之前的资源 - dispose() - - // 记录配置信息 - self.speechSubscriptionKey = speechSubscriptionKey - self.serviceRegion = serviceRegion - - // 配置音频会话 - if !configureAudioSession() { - print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化") - } - - do { - // 创建语音配置 - speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion) - - // 设置默认语音 - let defaultVoice = getDefaultVoiceForLanguage(language) - currentVoice = defaultVoice - speechConfig?.speechSynthesisVoiceName = defaultVoice - - // 创建语音合成器 - synthesizer = try SPXSpeechSynthesizer(speechConfig!) - - // 设置事件处理器 - setupSynthesizerEvents() - - isInitialized = true - print("[AzureTtsHelper] TTS 引擎初始化成功") - - return true - } catch { - print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)") - eventHandler?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"]) - return false - } - } - - /// 配置音频会话 - private func configureAudioSession() -> Bool { - do { - // 使用音频会话管理器配置媒体播放模式 - try audioSessionManager.configureForVoiceInteraction() - - isAudioSessionConfigured = true - print("[AzureTtsHelper] 音频会话配置成功") - return true - } catch { - print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)") - isAudioSessionConfigured = false - return false - } - } - - /// 设置语音 - /// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural") - /// - Returns: 设置是否成功 - func setVoice(voiceName: String) -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") - eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) - return false - } - - // 检查参数有效性 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureTtsHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - if voiceName.isEmpty { - print("[AzureTtsHelper] 错误: 声音名称为空") - eventHandler?(["type": "error", "message": "声音名称不能为空"]) - return false - } - - if voiceName == currentVoice { - print("[AzureTtsHelper] 已设置语音: \(voiceName)") - return true - } - - print("[AzureTtsHelper] 设置声音: \(voiceName)") - currentVoice = voiceName - - // 更新语音配置 - if let speechConfig = speechConfig { - speechConfig.speechSynthesisVoiceName = voiceName - return true - } - - return false - } - - /// 设置语音合成参数 - /// - Parameters: - /// - rate: 语速,范围 -100 到 100,默认为 0 - /// - pitch: 音调,范围 -100 到 100,默认为 0 - /// - volume: 音量,范围 0 到 100,默认为 100 - /// - Returns: 是否设置成功 - func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") - eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) - return false - } - - // 检查参数有效性 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureTtsHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - // 转换参数格式 - currentSpeechRate = formatRateParam(rate) - currentPitch = formatPitchParam(pitch) - currentVolume = formatVolumeParam(volume) - - print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)") - return true - } - - /// 合成文本为语音并播放 - /// - Parameter text: 要合成的文本 - /// - Returns: 操作是否成功启动 - func speakText(text: String) -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") - eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) - return false - } - - // 检查参数有效性 - if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { - print("[AzureTtsHelper] 错误: Azure 配置信息不完整") - eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) - return false - } - - if text.isEmpty { - print("[AzureTtsHelper] 警告: 要播放的文本为空") - return true - } - - // 确保音频会话已配置 - if !isAudioSessionConfigured { - _ = configureAudioSession() - } - - print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...") - - // 生成SSML - let ssml = generateSsml(text: text) - - // 直接进行SSML合成 - return speakSsmlInternal(text: ssml) - } - - /// 内部SSML合成和播放 - private func speakSsmlInternal(text: String) -> Bool { - guard let synthesizer = synthesizer else { - print("[AzureTtsHelper] 错误: 合成器未初始化") - eventHandler?(["type": "error", "message": "合成器未初始化"]) - return false - } - - _isSpeaking = true - eventHandler?(["type": "started"]) - - Task { - do { - // 使用异步方法进行合成并直接播放 - _ = try synthesizer.startSpeakingSsml(text) - - } catch { - print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)") - DispatchQueue.main.async { - self._isSpeaking = false - self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"]) - } - } - } - - return true - } - - /// 停止当前语音合成 - /// - Returns: 操作是否成功 - func stopSpeaking() -> Bool { - // 检查是否初始化 - if !isInitialized { - print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") - eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) - return false - } - - if !_isSpeaking { - print("[AzureTtsHelper] 未进行语音播放,忽略停止请求") - return true - } - - // 停止合成 - do { - try synthesizer?.stopSpeaking() - _isSpeaking = false - eventHandler?(["type": "canceled"]) - print("[AzureTtsHelper] 已停止语音合成") - return true - } catch { - print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)") - eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"]) - return false - } - } - - /// 检查是否正在播放 - /// - Returns: 当前是否正在播放语音 - func isSpeaking() -> Bool { - return _isSpeaking - } - - /// 释放资源 - func dispose() { - // 尝试停止所有语音合成 - if _isSpeaking { - do { - if let synthesizer = synthesizer { - try synthesizer.stopSpeaking() - print("[AzureTtsHelper] 语音合成已停止") - } - } catch { - print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)") - } - _isSpeaking = false - } - - // 释放合成器 - synthesizer = nil - speechConfig = nil - - // 重置状态 - isInitialized = false - isAudioSessionConfigured = false - - print("[AzureTtsHelper] 资源已释放") - } - - // MARK: - 私有辅助方法 - - /// 设置合成器事件处理 - private func setupSynthesizerEvents() { - guard let synthesizer = synthesizer else { return } - - // 添加书签到达事件处理 - synthesizer.addBookmarkReachedEventHandler { _, e in - print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"") - } - - // 合成完成事件 - synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in - guard let self = self else { return } - print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)") - DispatchQueue.main.async { - self._isSpeaking = false - self.eventHandler?(["type": "completed"]) - } - } - - // 合成取消事件 - synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in - guard let self = self else { return } - - let result = e.result - do { - let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result) - print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)") - - if cancellationDetails.reason == SPXCancellationReason.error { - print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)") - print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")") - } - - DispatchQueue.main.async { - self._isSpeaking = false - self.eventHandler?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"]) - } - } catch { - print("[AzureTtsHelper] 获取取消详情时出错: \(error)") - - DispatchQueue.main.async { - self._isSpeaking = false - self.eventHandler?(["type": "error", "message": "语音合成被取消"]) - } - } - } - - // 合成开始事件 - synthesizer.addSynthesisStartedEventHandler { _, _ in - // print("[AzureTtsHelper] 语音合成开始") - } - - // 合成中事件 - synthesizer.addSynthesizingEventHandler { _, _ in - // print("[AzureTtsHelper] 语音合成中") - } - } - - /// 生成 SSML 文本 - private func generateSsml(text: String) -> String { - return """ - - - - \(text) - - - - """ - } - - /// 格式化语速参数 - private func formatRateParam(_ rate: Int) -> String { - let clampedRate = rate.clamp(min: -100, max: 100) - if clampedRate == 0 { - return "0%" - } else if clampedRate < 0 { - return "\(Int(Double(clampedRate) * 0.9))%" - } else { - return "+\(clampedRate)%" - } - } - - /// 格式化音调参数 - private func formatPitchParam(_ pitch: Int) -> String { - let clampedPitch = pitch.clamp(min: -100, max: 100) - if clampedPitch == 0 { - return "0%" - } else { - return "\(Int(Double(clampedPitch) * 0.5))%" - } - } - - /// 格式化音量参数 - private func formatVolumeParam(_ volume: Int) -> String { - let clampedVolume = volume.clamp(min: 0, max: 100) - return "\(clampedVolume)%" - } - - /// 获取指定语言的默认语音 - private func getDefaultVoiceForLanguage(_ language: String) -> String { - return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural" - } -} - -// MARK: - 扩展 - -extension Int { - func clamp(min: Int, max: Int) -> Int { - if self < min { return min } - if self > max { return max } - return self - } -} \ No newline at end of file diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index 19d3697a4..f3c28516f 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,10 +1,8 @@ - - - + + - - + @@ -16,14 +14,13 @@ - + - + - diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift deleted file mode 100644 index 1a8369172..000000000 --- a/ios/Runner/BluetoothMediaButtonHelper.swift +++ /dev/null @@ -1,464 +0,0 @@ -import Foundation -import MediaPlayer -import AVFoundation -import UIKit - -/// 媒体按钮事件处理协议 -@available(iOS 13.0, *) -@objc protocol MediaButtonEventHandler: AnyObject { - func sendEvent(_ event: [String: Any]) -} - -/// 蓝牙媒体按键事件处理类 -/// 该类仅在iOS 13.0及以上版本可用 -@available(iOS 13.0, *) -class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { - // MARK: - 属性 - - @objc static let shared = BluetoothMediaButtonHelper() - - /// 日志标签 - private let TAG = "BluetoothMediaButtonHelper" - - /// 远程控制中心 - private var remoteCommandCenter: MPRemoteCommandCenter - - /// 事件处理器 - 用于发送事件到Flutter - private weak var eventHandler: MediaButtonEventHandler? - - /// 是否已启用监听 - private var isListening = false - - /// 静音音频播放器 - 用于保持音频会话活跃 - private var silentAudioPlayer: AVAudioPlayer? - - /// 播放计时器 - 用于定期更新播放信息 - private var playbackTimer: Timer? - - /// 当前播放时间 - 模拟播放进度 - private var currentPlaybackTime: TimeInterval = 0 - - /// 总时长 - 模拟 - private let totalDuration: TimeInterval = 300.0 - - /// 音频会话管理器 - private let audioSessionManager = AudioSessionManager.shared - - // MARK: - 初始化 - - override init() { - // 获取远程控制中心单例 - self.remoteCommandCenter = MPRemoteCommandCenter.shared() - - super.init() - - NSLog("\(TAG): 初始化媒体按钮处理器") - - // 初始化静音音频播放器 - initSilentAudioPlayer() - } - - deinit { - stopButtonListening() - NSLog("\(TAG): 媒体按钮处理器已释放") - } - - // MARK: - 公共方法 - - /// 设置事件处理器 - @objc func setEventHandler(_ handler: MediaButtonEventHandler) { - eventHandler = handler - NSLog("\(TAG): 已设置媒体按钮事件处理器") - } - - /// 开始监听蓝牙媒体按钮事件 - @objc func startButtonListening() -> Bool { - if isListening { - NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件") - return true - } - - NSLog("\(TAG): 开始监听蓝牙媒体按钮事件") - - // 立即注册远程命令 - 这不需要音频会话 - registerRemoteCommands() - - // 标记为已在监听 - isListening = true - - // 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话 - setupNowPlaying(isPlaying: true) - - // 尝试启用静音音频播放(但即使失败,远程命令仍然可用) - do { - // try audioSessionManager.configureForBluetoothMediaButtons() - startSilentAudio() - - // 延时1秒 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.setupNowPlaying(isPlaying: false) - self.stopSilentAudio() - } - - // startUpdatingPlaybackInfo() - } catch { - NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)") - // 继续执行,因为远程命令在某些设备上不需要播放音频也能工作 - } - - NSLog("\(TAG): 蓝牙媒体按钮监听已启动") - return true - } - - /// 停止监听蓝牙媒体按钮事件 - @objc func stopButtonListening() -> Bool { - if !isListening { - return true - } - - NSLog("\(TAG): 停止监听蓝牙媒体按钮事件") - - // 停止播放静音音频 - stopSilentAudio() - - // 停止更新播放信息 - stopUpdatingPlaybackInfo() - - // 取消注册远程命令 - unregisterRemoteCommands() - - // 清除NowPlaying信息 - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil - } - - isListening = false - NSLog("\(TAG): 蓝牙媒体按钮监听已停止") - return true - } - - // MARK: - 私有方法 - - /// 初始化静音音频播放器 - private func initSilentAudioPlayer() { - do { - // 尝试加载sample.mp3文件 - if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") { - let audioUrl = URL(fileURLWithPath: audioPath) - - silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl) - silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放 - silentAudioPlayer?.volume = 0.0 // 设置为全音量 - silentAudioPlayer?.prepareToPlay() // 预加载 - silentAudioPlayer?.delegate = self - - NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功") - return - } - - NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据") - - } catch { - NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)") - } - } - - /// 开始播放静音音频 - private func startSilentAudio() { - guard let player = silentAudioPlayer else { - NSLog("\(TAG): 无法开始播放,播放器未初始化") - return - } - - if !player.isPlaying { - let success = player.play() - if success { - NSLog("\(TAG): 开始播放静音音频") - } else { - NSLog("\(TAG): 播放启动失败") - } - } else { - NSLog("\(TAG): 静音音频已在播放中") - } - } - - /// 停止播放静音音频 - private func stopSilentAudio() { - silentAudioPlayer?.stop() - NSLog("\(TAG): 停止播放静音音频") - } - - /// 开始更新播放信息 - private func startUpdatingPlaybackInfo() { - // 初始设置 - setupNowPlaying(isPlaying: true) - - // 定期更新播放信息 - 恢复定时器,确保持续更新播放状态 - playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - guard let self = self else { return } - - // 更新播放时间(模拟) - self.currentPlaybackTime += 1.0 - if self.currentPlaybackTime >= self.totalDuration { - self.currentPlaybackTime = 0.0 - } - - // 更新NowPlaying信息 - self.setupNowPlaying(isPlaying: true) - } - - NSLog("\(TAG): 开始更新播放信息") - } - - /// 停止更新播放信息 - private func stopUpdatingPlaybackInfo() { - playbackTimer?.invalidate() - playbackTimer = nil - - // 清空NowPlaying信息 - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil - } - - NSLog("\(TAG): 停止更新播放信息") - } - - /// 设置NowPlaying信息 - private func setupNowPlaying(isPlaying: Bool) { - NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)") - - // 创建音频专辑图片 - let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片 - var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: "DeepSound 语音助手", - MPMediaItemPropertyArtist: "DeepSound", - MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放 - MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime, - MPMediaItemPropertyPlaybackDuration: totalDuration - ] - - // 如果有专辑图片,添加到NowPlaying信息中 - if let albumArt = albumArt { - let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in - return albumArt - } - nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork - } - - // 设置NowPlaying信息 (在主线程执行) - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - NSLog("\(self.TAG): NowPlaying信息设置完成") - } - } - - /// 注册远程命令 - private func registerRemoteCommands() { - let commandCenter = MPRemoteCommandCenter.shared() - - // 播放/暂停命令 - commandCenter.togglePlayPauseCommand.isEnabled = true - commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) - commandCenter.playCommand.isEnabled = true - commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) - commandCenter.pauseCommand.isEnabled = true - commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) - - // 下一曲/上一曲命令 - commandCenter.nextTrackCommand.isEnabled = true - commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:))) - commandCenter.previousTrackCommand.isEnabled = true - commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:))) - - // 快进/快退命令 - commandCenter.seekForwardCommand.isEnabled = true - commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:))) - commandCenter.seekBackwardCommand.isEnabled = true - commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:))) - - // 停止命令 - commandCenter.stopCommand.isEnabled = true - commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:))) - - - - NSLog("\(TAG): 远程命令已注册") - } - - /// 取消注册远程命令 - private func unregisterRemoteCommands() { - NSLog("\(TAG): 取消注册远程命令") - - // 基本控制命令 - remoteCommandCenter.playCommand.isEnabled = false - remoteCommandCenter.playCommand.removeTarget(nil) - - remoteCommandCenter.pauseCommand.isEnabled = false - remoteCommandCenter.pauseCommand.removeTarget(nil) - - remoteCommandCenter.togglePlayPauseCommand.isEnabled = false - remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil) - - remoteCommandCenter.stopCommand.isEnabled = false - remoteCommandCenter.stopCommand.removeTarget(nil) - - - } - - /// 发送按钮事件到Flutter - private func sendButtonEvent(type: String) { - // 创建事件数据 - let eventData: [String: Any] = [ - "type": "mediaButtonEvent", - "buttonType": type, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - - // 发送事件到处理器 - eventHandler?.sendEvent(eventData) - } - - - // MARK: - 命令处理方法 - - /// 处理播放/暂停命令 - @objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到播放/暂停命令") - - // 发送事件到Flutter - sendButtonEvent(type: "play_pause") - - // 调用VoiceInteractionService处理媒体按钮事件 - if VoiceInteractionService.shared.isServiceRunning() { - NSLog("\(TAG): 转发到VoiceInteractionService处理") - VoiceInteractionService.shared.handleMediaButtonAction() - } - - return .success - } - - /// 处理下一曲命令 - @objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到下一曲命令") - - // 发送事件到Flutter - sendButtonEvent(type: "next_track") - - // 也可以转发到VoiceInteractionService - if VoiceInteractionService.shared.isServiceRunning() { - VoiceInteractionService.shared.handleMediaButtonAction() - } - - return .success - } - - /// 处理上一曲命令 - @objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到上一曲命令") - - // 发送事件到Flutter - sendButtonEvent(type: "previous_track") - - // 也可以转发到VoiceInteractionService - if VoiceInteractionService.shared.isServiceRunning() { - VoiceInteractionService.shared.handleMediaButtonAction() - } - - return .success - } - - /// 处理快进命令 - @objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到快进命令") - - // 发送事件到Flutter - sendButtonEvent(type: "seek_forward") - - return .success - } - - /// 处理快退命令 - @objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到快退命令") - - // 发送事件到Flutter - sendButtonEvent(type: "seek_backward") - - return .success - } - - /// 处理停止命令 - @objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { - NSLog("\(TAG): 收到停止命令") - - // 发送事件到Flutter - sendButtonEvent(type: "stop") - - // 调用VoiceInteractionService处理媒体按钮事件 - if VoiceInteractionService.shared.isServiceRunning() { - VoiceInteractionService.shared.handleMediaButtonAction() - } - - return .success - } - - // MARK: - AVAudioPlayerDelegate方法 - - /// 音频播放结束回调 - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - NSLog("\(TAG): 音频播放结束, 成功: \(flag)") - - // 如果仍在监听状态,重新开始播放 - if isListening && player == silentAudioPlayer { - player.play() - } - } - - /// 音频播放错误回调 - func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { - if let error = error { - NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)") - } else { - NSLog("\(TAG): 音频解码发生未知错误") - } - - // 重新初始化播放器 - if player == silentAudioPlayer && isListening { - initSilentAudioPlayer() - startSilentAudio() - } - } - - /// 清理和释放资源 - func dispose() { - NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源") - // 停止监听 - stopButtonListening() - - // 释放播放器和计时器资源 - silentAudioPlayer = nil - if let timer = playbackTimer { - timer.invalidate() - playbackTimer = nil - } - - // 清除引用 - eventHandler = nil - - NSLog("\(TAG): 资源已释放") - } -} - -extension UInt32 { - var data: Data { - var int = self - return Data(bytes: &int, count: MemoryLayout.size) - } -} - -extension UInt16 { - var data: Data { - var int = self - return Data(bytes: &int, count: MemoryLayout.size) - } -} \ No newline at end of file diff --git a/ios/Runner/ClassicBluetoothHelper.swift b/ios/Runner/ClassicBluetoothHelper.swift deleted file mode 100644 index 2694075e0..000000000 --- a/ios/Runner/ClassicBluetoothHelper.swift +++ /dev/null @@ -1,243 +0,0 @@ -import Foundation -import CoreBluetooth -import AVFoundation -import UIKit - -/// 蓝牙事件处理协议 -@objc protocol BluetoothEventHandler: AnyObject { - func sendEvent(_ event: [String: Any]) -} - -@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate { - // MARK: - 属性 - - // 音频会话管理器 - private let audioSessionManager = AudioSessionManager.shared - - // 蓝牙管理器 - private var centralManager: CBCentralManager? - - // 事件处理器 - private weak var eventHandler: BluetoothEventHandler? - - // MARK: - 初始化与设置 - - override init() { - super.init() - } - - // 设置事件处理器 - @objc func setEventHandler(_ handler: BluetoothEventHandler) { - eventHandler = handler - } - - - // MARK: - CBCentralManagerDelegate - - func centralManagerDidUpdateState(_ central: CBCentralManager) { - let stateString = getBluetoothStateString(state: central.state) - sendBluetoothStateEvent(state: stateString) - } - - // MARK: - 设备监控 - - // 获取蓝牙状态字符串 - private func getBluetoothStateString(state: CBManagerState) -> String { - switch state { - case .poweredOn: - return "on" - case .poweredOff: - return "off" - default: - return "unknown" - } - } - - // 发送蓝牙状态事件 - private func sendBluetoothStateEvent(state: String) { - let event: [String: Any] = [ - "type": "bluetoothStateChanged", - "state": state, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - eventHandler?.sendEvent(event) - } - - // 启动蓝牙设备监控 - @objc func initialize() { - if centralManager == nil { - centralManager = CBCentralManager(delegate: self, queue: nil) - } - - - - // 发送当前蓝牙状态 - if let manager = centralManager { - let stateString = getBluetoothStateString(state: manager.state) - sendBluetoothStateEvent(state: stateString) - } - // 移除之前的监听器 - NotificationCenter.default.removeObserver(self) - // 添加新的监听器 - audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:))) - // BluetoothMediaButtonHelper.shared.startButtonListening() - } - - // 处理音频路由变化 - @objc private func handleRouteChange(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, - let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { - return - } - - // 获取当前设备 - let currentDevices = getConnectedAudioDevices() - - // 检查原因 - switch reason { - case .newDeviceAvailable: - // 新设备已连接 - for device in currentDevices { - // 发送设备连接事件 - NSLog("发送设备连接事件 - 设备: \(device["name"] ?? "未知设备") [\(device["address"] ?? "未知地址")]") - sendDeviceConnectedEvent(device: device) - } - - case .oldDeviceUnavailable: - // 获取断开之前的路由 - if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { - // 过滤蓝牙相关设备 - for output in previousRoute.outputs where isBluetoothOutput(output.portType) { - let device: [String: String] = [ - "name": output.portName, - "address": cleanDeviceAddress(output.uid) - ] - // 发送设备断开事件 - sendDeviceDisconnectedEvent(device: device) - } - } - - default: - break - } - } - - // 检查是否为蓝牙输出 - private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { - return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE - } - - // MARK: - 设备事件 - - // 发送设备连接事件 - private func sendDeviceConnectedEvent(device: [String: String]) { - guard let name = device["name"], let address = device["address"] else { return } - - let deviceMap: [String: Any] = [ - "name": name, - "address": address, - "type": "headset" - ] - - let event: [String: Any] = [ - "type": "deviceConnected", - "device": deviceMap, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - - eventHandler?.sendEvent(event) - } - - // 发送设备断开事件 - private func sendDeviceDisconnectedEvent(device: [String: String]) { - guard let name = device["name"], let address = device["address"] else { return } - - let deviceMap: [String: Any] = [ - "name": name, - "address": address, - "type": "headset" - ] - - let event: [String: Any] = [ - "type": "deviceDisconnected", - "device": deviceMap, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - - eventHandler?.sendEvent(event) - } - - // MARK: - 公共API - - // 获取当前连接的耳机设备 - @objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) { - // 检查蓝牙状态 - if centralManager?.state != .poweredOn { - completion(nil, "蓝牙未启用") - return - } - - do { - // 获取设备 - let devices = getConnectedAudioDevices() - completion(devices, nil) - } catch { - completion(nil, "获取设备失败: \(error.localizedDescription)") - } - } - - // 获取当前连接的音频设备 - private func getConnectedAudioDevices() -> [[String: String]] { - var result: [[String: String]] = [] - - // 获取当前音频路由 - let outputs = audioSessionManager.getCurrentRoute().outputs - - // 过滤蓝牙相关设备 - for output in outputs where isBluetoothOutput(output.portType) { - let device: [String: String] = [ - "name": output.portName, - "address": cleanDeviceAddress(output.uid) // 处理设备地址,移除后缀 - ] - result.append(device) - } - - return result - } - - // 释放资源 - @objc func dispose() { - audioSessionManager.removeRouteChangeListener(self) - eventHandler = nil - } - - - - // 检查蓝牙是否启用 - @objc func isBluetoothEnabled() -> Bool { - return centralManager?.state == .poweredOn - } - - // 清理设备地址,移除后缀 - private func cleanDeviceAddress(_ address: String) -> String { - // 检查是否包含常见分隔符 - if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") { - // 只保留分隔符之前的部分 - return String(address[.. -CFBundleURLTypes - - - CFBundleURLName - com.yunqiinnovation.deepsound - CFBundleURLSchemes - - fb3829022037361855 - com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 - - - -FacebookAppID -3829022037361855 -FacebookClientToken -193d0a2b1757c41cfa557ea8e8f21c33 -FacebookDisplayName -Deepsound - -LSApplicationQueriesSchemes - - fbapi - fb-messenger-api - fbauth2 - fbshareextension - googlechrome - googlechromes - - ITSAppUsesNonExemptEncryption - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - DeepSound + Deep Voice CFBundleExecutable $(EXECUTABLE_NAME) - NSUserTrackingUsageDescription - This application needs to access your tracking data in order to offer better personalized services and advertisements CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -54,23 +22,63 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + fb3829022037361855 + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + wx5bbb7be9324f3e2d + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + external-accessory + + LSApplicationQueriesSchemes + + weixin + weixinULAPI + weixinURLParamsAPI + fbapi + fbapi20130214 + fbapi20130410 + fbapi20130702 + fbapi20131010 + fbapi20131219 + fbapi20140410 + fbapi20140116 + fbapi20150313 + fbapi20150629 + fbapi20160328 + fbauth + fb-messenger-share-api + fbauth2 + fbshareextension + LSRequiresIPhoneOS - NSBluetoothAlwaysUsageDescription - Bluetooth permission is required to connect and manage audio devices - NSBluetoothPeripheralUsageDescription - Bluetooth permission is required to connect and manage audio devices - NSMicrophoneUsageDescription - Microphone permission is required for voice recognition - NSSpeechRecognitionUsageDescription - Speech recognition permission is required to provide voice interaction services + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsInWebContent + + UIApplicationSupportsIndirectInputEvents - UIBackgroundModes - - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -88,5 +96,27 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSLocationWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSLocationAlwaysAndWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSMicrophoneUsageDescription + 需要麦克风权限用于语音识别和录音功能 + FacebookClientToken + 193d0a2b1757c41cfa557ea8e8f21c33 + FacebookAppID + 3829022037361855 + FacebookAdvertiserIDCollectionEnabled + + FacebookAutoLogAppEventsEnabled + + NSAccessorySetupKitSupports + + Bluetooth + diff --git a/ios/Runner/Resources/.gitkeep b/ios/Runner/Resources/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Runner/Resources/sample.mp3 b/ios/Runner/Resources/sample.mp3 deleted file mode 100644 index 31ebc28ce..000000000 Binary files a/ios/Runner/Resources/sample.mp3 and /dev/null differ diff --git a/ios/Runner/Resources/silence.mp3 b/ios/Runner/Resources/silence.mp3 deleted file mode 100644 index f5bd92f88..000000000 Binary files a/ios/Runner/Resources/silence.mp3 and /dev/null differ diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h index 72042ccf3..308a2a560 100644 --- a/ios/Runner/Runner-Bridging-Header.h +++ b/ios/Runner/Runner-Bridging-Header.h @@ -1,7 +1 @@ -#ifndef Runner_Bridging_Header_h -#define Runner_Bridging_Header_h - #import "GeneratedPluginRegistrant.h" -#import - -#endif /* Runner_Bridging_Header_h */ diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/Runner/VoiceInteractionService.swift b/ios/Runner/VoiceInteractionService.swift deleted file mode 100644 index f19baf2c9..000000000 --- a/ios/Runner/VoiceInteractionService.swift +++ /dev/null @@ -1,565 +0,0 @@ -import Foundation -import UIKit -import AVFoundation - -/** - * 语音交互服务,iOS原生实现 - * - * 负责: - * 1) 监听蓝牙耳机按键 - * 2) 处理语音识别 - * 3) 与火山AI服务交互 - * 4) 文本转语音播放 - */ -@available(iOS 13.0, *) -class VoiceInteractionService: NSObject { - // 常量 - private let TAG = "VoiceInteractionService" - private let RECOGNITION_TIMEOUT: TimeInterval = 8.0 - - // 单例 - static let shared = VoiceInteractionService() - - // 服务状态 - private var isRunning = false - private var isActive = false - private var isRecognitionActive = false - private var isTimeoutPaused = false - private var hasSpeechDetected = false - private var isTtsSpeaking = false - - // 事件处理器 - private weak var eventHandler: VoiceInteractionEventHandler? - - // 按键处理 - private var lastKeyEventTime: TimeInterval = 0 - private var keyEventCount = 0 - - // 活动时间 - private var lastActivityTime: TimeInterval = 0 - - // 当前用户输入 - private var currentUserInput = "" - private var lastUserMessage = "" - private var lastAssistantMessage = "" - - // 服务组件 - private var azureAsrHelper: AzureAsrHelper? - private var azureTtsHelper: AzureTtsHelper? - private var volcanoAIService: VolcanoAIService? - - - private var silenceTimer: Timer? - - // 系统提示词 - private let systemPrompt = """ - 你是一个智能语音助手,能够简洁明了地回答用户的问题。 - 请保持回答简短、准确,避免过长的解释。 - 如果用户的问题不清楚,请礼貌地请求澄清。 - 不要使用复杂的术语,除非用户明确要求。 - 用户用语音和你交互。 - """ - - // 初始化方法 - private override init() { - super.init() - NSLog("%@: VoiceInteractionService 初始化中", TAG) - - - } - - /** - * 启动服务 - * - * @param apiKey Azure语音服务API密钥 - * @param region Azure语音服务区域 - * @param volcanoKey 火山AI服务API密钥 - * @return 启动是否成功 - */ - func start(azureKey: String, azureRegion: String, volcanoKey: String) -> Bool { - guard !isRunning else { - NSLog("%@: 服务已经在运行", TAG) - return true - } - - NSLog("%@: 启动服务中", TAG) - - // 重置状态 - resetState() - - // 初始化各个服务组件 - initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey) - - // 启动monitorService定时器 - silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true) - NSLog("%@: monitorService定时器已启动", TAG) - - // 标记服务为运行状态 - isRunning = true - - NSLog("%@: 服务启动完成,等待蓝牙按键事件", TAG) - return true - } - - /** - * 停止服务 - */ - func stop() { - guard isRunning else { - NSLog("%@: 服务未运行", TAG) - return - } - - NSLog("%@: 停止服务中", TAG) - - // 停止monitorService定时器 - silenceTimer?.invalidate() - silenceTimer = nil - - // 停止语音识别 - if isRecognitionActive { - stopVoiceRecognition() - } - - // 停止TTS - stopCurrentTTS() - - // 释放服务实例 - azureAsrHelper?.dispose() - azureTtsHelper?.dispose() - - // 更新状态 - resetState() - isRunning = false - - NSLog("%@: 服务已停止", TAG) - } - - /** - * 获取服务运行状态 - */ - func isServiceRunning() -> Bool { - return isRunning - } - - /** - * 重置状态 - */ - private func resetState() { - isActive = false - isRecognitionActive = false - isTimeoutPaused = false - hasSpeechDetected = false - isTtsSpeaking = false - lastUserMessage = "" - lastAssistantMessage = "" - } - - /** - * 初始化服务 - */ - private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) { - NSLog("%@: 初始化服务组件", TAG) - - // 初始化Azure ASR - azureAsrHelper = AzureAsrHelper() - if let helper = azureAsrHelper { - helper.setEventHandler({ [weak self] eventData in - // 处理ASR事件 - self?.handleAsrEvent(eventData: eventData) - }) - } - - // 初始化Azure TTS - azureTtsHelper = AzureTtsHelper() - if let helper = azureTtsHelper { - helper.setEventHandler({ [weak self] eventData in - // 处理TTS事件 - self?.handleTtsEvent(eventData: eventData) - }) - } - - // 初始化火山AI服务 - volcanoAIService = VolcanoAIService() - - // 配置Azure ASR - if !azureKey.isEmpty && !azureRegion.isEmpty { - azureAsrHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, supportedLanguages: ["zh-CN"]) - NSLog("%@: Azure ASR初始化完成", TAG) - } else { - NSLog("%@: Azure配置信息不完整,无法初始化Azure ASR", TAG) - } - - // 配置Azure TTS - if !azureKey.isEmpty && !azureRegion.isEmpty { - azureTtsHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, language: "zh-CN") - NSLog("%@: Azure TTS初始化完成", TAG) - } else { - NSLog("%@: Azure配置信息不完整,无法初始化Azure TTS", TAG) - } - - // 配置火山AI - if !volcanoKey.isEmpty { - volcanoAIService?.initialize(apiKey: volcanoKey) - NSLog("%@: 火山AI服务初始化完成", TAG) - } else { - NSLog("%@: 火山AI配置信息不完整,无法初始化火山AI服务", TAG) - } - } - - /** - * 监控服务状态 - */ - @objc private func monitorService() { - // 确保服务保持活跃状态 - if !isActive { - isActive = true - } - - // 检查语音识别状态 - if isRecognitionActive { - let currentTime = Date().timeIntervalSince1970 - let elapsedTime = currentTime - lastActivityTime - - // NSLog("%@: 已过去时间: %@, 是否检测到语音: %@, 是否TTS播放: %@", TAG, String(elapsedTime), String(hasSpeechDetected), String(isTtsSpeaking), String(elapsedTime)) - - // 如果超过超时时间没有检测到语音,且不在TTS播放中,暂停语音识别 - if !hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT { - // NSLog("%@: 超过%@秒未检测到语音,停止识别", TAG, String(RECOGNITION_TIMEOUT)) - isTimeoutPaused = true - playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。") - - // 停止语音识别并确保资源完全释放 - stopVoiceRecognition() - - // 清理识别状态 - isRecognitionActive = false - hasSpeechDetected = false - } - } - - } - - /** - * 处理ASR事件 - */ - private func handleAsrEvent(eventData: [String: Any]) { - if let type = eventData["type"] as? String { - // NSLog("%@: 处理ASR事件: %@", TAG, type) - - switch type { - case "recognizing": - if let text = eventData["text"] as? String, !text.isEmpty { - hasSpeechDetected = true - stopCurrentTTS() - updateLastActivityTime() - } else if let data = eventData["data"] as? [String: Any], - let text = data["text"] as? String, - !text.isEmpty { - hasSpeechDetected = true - stopCurrentTTS() - updateLastActivityTime() - } - - case "result": - var text: String? - - // 直接读取text或从data中读取 - if let directText = eventData["text"] as? String, !directText.isEmpty { - text = directText - } else if let data = eventData["data"] as? [String: Any], - let dataText = data["text"] as? String, - !dataText.isEmpty { - text = dataText - } - - if let finalText = text { - updateLastActivityTime() - - // 保存用户消息 - lastUserMessage = finalText - - processWithVolcanoAI(finalText) - } - - // 重置状态,继续识别 - hasSpeechDetected = false - - case "sessionStarted": - updateLastActivityTime() - - case "sessionStopped": - isRecognitionActive = false - - case "canceled": - isRecognitionActive = false - - case "error": - isRecognitionActive = false - if let message = eventData["message"] as? String { - NSLog("%@: ASR错误: %@", TAG, message) - } - playNotification("语音识别出错") - - default: - break - } - } - } - - /** - * 处理TTS事件 - */ - private func handleTtsEvent(eventData: [String: Any]) { - if let type = eventData["type"] as? String { - // NSLog("%@: 处理TTS事件: %@", TAG, type) - - switch type { - case "started": - isTtsSpeaking = true - - case "completed": - isTtsSpeaking = false - updateLastActivityTime() - - case "canceled": - isTtsSpeaking = false - - case "error": - isTtsSpeaking = false - if let message = eventData["message"] as? String { - NSLog("%@: TTS错误: %@", TAG, message) - } - - default: - break - } - } - } - - /** - * 处理双击事件 - */ - func handleMediaButtonAction() { - NSLog("%@: 处理媒体按钮事件", TAG) - - // 屏蔽短时间重复响应的问题 - let currentTime = Date().timeIntervalSince1970 - let timeDiff = currentTime - lastKeyEventTime - if timeDiff < 0.5 { - NSLog("%@: 短时间重复响应,忽略", TAG) - return - } - lastKeyEventTime = currentTime - - // 停止当前TTS播放 - stopCurrentTTS() - - // 检查应用是否在后台 - if UIApplication.shared.applicationState == .background { - NSLog("%@: 应用在后台,无法开始语音识别", TAG) - playNotification("请打开应用后重试!") - return - } - // 播放提示音 - playPrompt("我在!") - - // 重置超时暂停标志 - isTimeoutPaused = false - - // 启动或重置语音识别 - if !isRecognitionActive { - NSLog("%@: 语音识别未激活,开始启动", TAG) - - startVoiceRecognition() - } else { - NSLog("%@: 语音识别已激活,更新活动时间", TAG) - updateLastActivityTime() - hasSpeechDetected = false - } - } - - /** - * 开始语音识别 - */ - private func startVoiceRecognition() { - if isRecognitionActive { return } - - - - // 通知Flutter语音识别已启动 - notifyVoiceRecognitionStarted() - - isActive = true - isRecognitionActive = true - hasSpeechDetected = false - updateLastActivityTime() - - if let asrHelper = azureAsrHelper { - let success = asrHelper.startContinuousRecognition() - if !success { - isRecognitionActive = false - NSLog("%@: 启动语音识别失败", TAG) - playNotification("启动语音识别失败") - } - } else { - isRecognitionActive = false - NSLog("%@: Azure ASR服务未初始化", TAG) - playNotification("语音识别服务未初始化") - } - } - - /** - * 停止语音识别 - */ - private func stopVoiceRecognition() { - if !isRecognitionActive { return } - - NSLog("%@: 停止语音识别", TAG) - - if let asrHelper = azureAsrHelper { - let success = asrHelper.stopContinuousRecognition() - if !success { - NSLog("%@: 停止语音识别失败", TAG) - } - } - - isRecognitionActive = false - hasSpeechDetected = false - } - - /** - * 使用火山AI处理语音识别结果 - */ - private func processWithVolcanoAI(_ text: String) { - // 保存当前用户输入 - currentUserInput = text - - // 在后台线程处理 - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self, let volcanoAIService = self.volcanoAIService else { - return - } - - do { - // 创建消息 - let message = volcanoAIService.createUserMessage(content: text) - let messages = [message] - - // 发送请求 - let response = try volcanoAIService.sendMessage(messages: messages, systemPrompt: self.systemPrompt) - - // 保存助手回复 - self.lastAssistantMessage = response - - // 播放AI回复 - self.speakAIResponse(response) - - // 同步聊天记录到Flutter端 - self.notifyChatHistoryUpdated(userMessage: text, assistantMessage: response) - } catch { - NSLog("%@: AI处理出错: %@", self.TAG, error.localizedDescription) - self.playNotification("AI处理出错") - } - } - } - - /** - * 播放AI回复 - */ - private func speakAIResponse(_ text: String) { - isTtsSpeaking = true - azureTtsHelper?.speakText(text: text) - } - - /** - * 播放提示音 - */ - private func playPrompt(_ message: String) { - isTtsSpeaking = true - azureTtsHelper?.speakText(text: message) - } - - /** - * 播放通知提示音 - */ - private func playNotification(_ message: String) { - isTtsSpeaking = true - azureTtsHelper?.speakText(text: message) - } - - /** - * 停止当前TTS播放 - */ - private func stopCurrentTTS() { - if isTtsSpeaking { - azureTtsHelper?.stopSpeaking() - isTtsSpeaking = false - } - } - - /** - * 更新最后活动时间 - */ - private func updateLastActivityTime() { - lastActivityTime = Date().timeIntervalSince1970 - } - - /** - * 暂停语音交互 - */ - func pauseVoiceInteraction() { - NSLog("%@: 暂停语音交互", TAG) - - // 停止当前语音播放 - stopCurrentTTS() - - // 停止语音识别 - if isRecognitionActive { - stopVoiceRecognition() - } - - // 设置状态为暂停,但服务保持运行 - isTimeoutPaused = true - } - - /** - * 通知语音识别已开始 - */ - private func notifyVoiceRecognitionStarted() { - // 发送识别开始事件 - sendEvent(type: "recognition_started", data: [:]) - } - - /** - * 通知聊天历史更新 - */ - private func notifyChatHistoryUpdated(userMessage: String, assistantMessage: String) { - // 发送聊天历史更新事件 - sendEvent(type: "chat_history_updated", data: [ - "agentId": "personal_assistant", - "userMessage": userMessage, - "assistantMessage": assistantMessage - ]) - } - - /** - * 设置事件处理器 - */ - func setEventHandler(_ handler: VoiceInteractionEventHandler) { - self.eventHandler = handler - } - - /** - * 发送事件到Flutter端 - */ - private func sendEvent(type: String, data: [String: Any] = [:]) { - var eventData = data - eventData["type"] = type - eventData["timestamp"] = Int(Date().timeIntervalSince1970 * 1000) - - // 使用事件处理器发送事件 - eventHandler?.sendEvent(eventData) - } - -} \ No newline at end of file diff --git a/ios/Runner/VolcanoAIService.swift b/ios/Runner/VolcanoAIService.swift deleted file mode 100644 index 4289b3b1b..000000000 --- a/ios/Runner/VolcanoAIService.swift +++ /dev/null @@ -1,391 +0,0 @@ -import Foundation - -/** - * 火山AI服务的iOS原生实现 - * - * 参考Android端的VolcanoAIService实现,提供同步和异步的API调用方式 - */ -class VolcanoAIService { - private let TAG = "VolcanoAIService" - private let baseUrl = "https://ark.cn-beijing.volces.com/api/v3" - private let chatEndpoint = "/chat/completions" - private let session: URLSession - - private var apiKey: String = "" - private var isInitialized = false - - init() { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30.0 - config.timeoutIntervalForResource = 30.0 - self.session = URLSession(configuration: config) - } - - /** - * 初始化火山AI服务 - * - * @param apiKey 火山AI API密钥 - * @return 初始化是否成功 - */ - func initialize(apiKey: String) -> Bool { - self.apiKey = apiKey - isInitialized = !apiKey.isEmpty - - if !isInitialized { - NSLog("%@: 初始化失败:API key 不能为空", TAG) - } else { - NSLog("%@: 火山AI服务初始化成功", TAG) - } - - return isInitialized - } - - /** - * 生成个性化问候语 - * - * @param agentName 代理名称 - * @param systemPrompt 系统提示词 - * @param callback 回调函数,返回生成的问候语 - */ - func generateGreeting(agentName: String, systemPrompt: String, callback: @escaping (String?, Error?) -> Void) { - let messages: [[String: Any]] = [ - ["role": "system", "content": systemPrompt], - ["role": "user", "content": "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。"] - ] - - let messagesData = try? JSONSerialization.data(withJSONObject: messages, options: []) - let messagesArray = try? JSONSerialization.jsonObject(with: messagesData!, options: []) as? [[String: Any]] - - var result = "" - - sendMessageStream(messages: messagesArray!, systemPrompt: systemPrompt, streamCallback: StreamCallback( - onToken: { token in - result.append(token) - }, - onComplete: { - callback(result, nil) - }, - onError: { error in - callback(nil, error) - } - )) - } - - /** - * 发送消息(非流式输出) - * - * @param messages 消息列表 - * @param systemPrompt 系统提示词 - * @return 返回AI的回复 - * @throws VolcanoAIError 如果API调用失败 - */ - func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String { - // 检查是否已初始化 - if !isInitialized || apiKey.isEmpty { - throw VolcanoAIError.serviceNotInitialized - } - - var fullMessages: [[String: Any]] = [ - ["role": "system", "content": systemPrompt] - ] - - fullMessages.append(contentsOf: messages) - - let requestBody: [String: Any] = [ - "model": "doubao-1-5-lite-32k-250115", - "messages": fullMessages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": false - ] - - guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else { - throw VolcanoAIError.invalidURL - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - - do { - request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: []) - } catch { - throw VolcanoAIError.invalidRequestBody - } - - let semaphore = DispatchSemaphore(value: 0) - var responseData: Data? - var responseError: Error? - - let task = session.dataTask(with: request) { data, response, error in - if let error = error { - responseError = VolcanoAIError.networkError(error.localizedDescription) - semaphore.signal() - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - responseError = VolcanoAIError.invalidResponse - semaphore.signal() - return - } - - if !(200...299).contains(httpResponse.statusCode) { - var errorMessage = "Unknown error occurred" - if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let error = json["error"] as? [String: Any], - let message = error["message"] as? String { - errorMessage = message - } - responseError = VolcanoAIError.apiError(errorMessage) - semaphore.signal() - return - } - - responseData = data - semaphore.signal() - } - - task.resume() - _ = semaphore.wait(timeout: .distantFuture) - - if let error = responseError { - throw error - } - - guard let data = responseData else { - throw VolcanoAIError.emptyResponse - } - - do { - guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let choices = json["choices"] as? [[String: Any]], - let firstChoice = choices.first, - let message = firstChoice["message"] as? [String: Any], - let content = message["content"] as? String else { - throw VolcanoAIError.invalidResponseFormat - } - - return content - } catch { - throw VolcanoAIError.invalidResponseFormat - } - } - - /** - * 发送消息(流式输出) - * - * @param messages 消息列表 - * @param systemPrompt 系统提示词 - * @param streamCallback 回调函数,用于接收流式输出的结果 - */ - func sendMessageStream(messages: [[String: Any]], systemPrompt: String, streamCallback: StreamCallback) { - // 检查是否已初始化 - if !isInitialized || apiKey.isEmpty { - streamCallback.onError(VolcanoAIError.serviceNotInitialized) - return - } - - var fullMessages: [[String: Any]] = [ - ["role": "system", "content": systemPrompt] - ] - - fullMessages.append(contentsOf: messages) - - let requestBody: [String: Any] = [ - "model": "doubao-1-5-lite-32k-250115", - "messages": fullMessages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": true - ] - - guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else { - streamCallback.onError(VolcanoAIError.invalidURL) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - request.addValue("text/event-stream", forHTTPHeaderField: "Accept") - - do { - request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: []) - } catch { - streamCallback.onError(VolcanoAIError.invalidRequestBody) - return - } - - let task = session.dataTask(with: request) { data, response, error in - if let error = error { - streamCallback.onError(VolcanoAIError.networkError(error.localizedDescription)) - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - streamCallback.onError(VolcanoAIError.invalidResponse) - return - } - - if !(200...299).contains(httpResponse.statusCode) { - var errorMessage = "Unknown error occurred" - if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let error = json["error"] as? [String: Any], - let message = error["message"] as? String { - errorMessage = message - } - streamCallback.onError(VolcanoAIError.apiError(errorMessage)) - return - } - - guard let data = data else { - streamCallback.onError(VolcanoAIError.emptyResponse) - return - } - - // 处理SSE流数据 - let responseString = String(data: data, encoding: .utf8) ?? "" - let lines = responseString.components(separatedBy: "\n") - - for line in lines { - if line.isEmpty { continue } - - if line.hasPrefix("data: ") { - let data = String(line.dropFirst(6)) - if data == "[DONE]" { - streamCallback.onComplete() - break - } - - do { - if let jsonData = data.data(using: .utf8), - let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let choices = json["choices"] as? [[String: Any]], - let firstChoice = choices.first, - let delta = firstChoice["delta"] as? [String: Any], - let content = delta["content"] as? String { - streamCallback.onToken(content) - } - } catch { - // 忽略无效的JSON数据 - continue - } - } - } - } - - task.resume() - } - - /** - * 同步方式发送消息(流式输出) - * - * 注意:此方法会阻塞当前线程,请在后台线程中调用 - * - * @param messages 消息列表 - * @param systemPrompt 系统提示词 - * @return 返回完整的AI回复 - * @throws VolcanoAIError 如果API调用失败 - */ - func sendMessageStreamSync(messages: [[String: Any]], systemPrompt: String) throws -> String { - var result = "" - let semaphore = DispatchSemaphore(value: 0) - var responseError: Error? - - sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: StreamCallback( - onToken: { token in - result.append(token) - }, - onComplete: { - semaphore.signal() - }, - onError: { error in - responseError = error - semaphore.signal() - } - )) - - // 等待流式输出完成或出错 - _ = semaphore.wait(timeout: .now() + 60) - - if let error = responseError { - throw error - } - - return result - } - - /** - * 创建用户消息 - */ - func createUserMessage(content: String) -> [String: Any] { - return ["role": "user", "content": content] - } - - /** - * 创建系统消息 - */ - func createSystemMessage(content: String) -> [String: Any] { - return ["role": "system", "content": content] - } - - /** - * 创建助手消息 - */ - func createAssistantMessage(content: String) -> [String: Any] { - return ["role": "assistant", "content": content] - } - - /** - * 流式输出回调类 - */ - class StreamCallback { - let onToken: (String) -> Void - let onComplete: () -> Void - let onError: (Error) -> Void - - init(onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void) { - self.onToken = onToken - self.onComplete = onComplete - self.onError = onError - } - } -} - -/** - * 火山AI错误枚举 - */ -enum VolcanoAIError: Error { - case serviceNotInitialized - case invalidURL - case invalidRequestBody - case networkError(String) - case invalidResponse - case emptyResponse - case invalidResponseFormat - case apiError(String) - - var localizedDescription: String { - switch self { - case .serviceNotInitialized: - return "火山AI服务未初始化或API key为空,请先调用initialize方法" - case .invalidURL: - return "无效的URL" - case .invalidRequestBody: - return "无效的请求体" - case .networkError(let message): - return "网络错误: \(message)" - case .invalidResponse: - return "无效的响应" - case .emptyResponse: - return "空响应" - case .invalidResponseFormat: - return "无效的响应格式" - case .apiError(let message): - return "API错误: \(message)" - } - } -} \ No newline at end of file diff --git a/ios/Runner/sample.mp3 b/ios/Runner/sample.mp3 deleted file mode 100644 index 31ebc28ce..000000000 Binary files a/ios/Runner/sample.mp3 and /dev/null differ diff --git a/ios/Runner/silence.mp3 b/ios/Runner/silence.mp3 deleted file mode 100644 index f5bd92f88..000000000 Binary files a/ios/Runner/silence.mp3 and /dev/null differ diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 8921a6fca..7f5b428bd 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -4,9 +4,7 @@ import '../../data/services/speech_factory.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/language_manager.dart'; -import '../../data/services/volcano_ai_service.dart'; import '../../core/utils/logger.dart'; -import '../../data/services/open_ai_service_adapter.dart'; import '../../data/services/bluetooth_manager.dart'; import '../../data/services/ble_manager.dart'; @@ -17,10 +15,11 @@ class InitialBinding extends Bindings { Logger.warning('InitialBinding dependencies'); // 语言管理器(需要最先初始化) - Get.put(LanguageManager(), permanent: true); + // 语言管理器(需要最先初始化) + Get.lazyPut(() => LanguageManager(), fenix: true); //初始化语音工厂 - Get.put(SpeechFactory(), permanent: true); + Get.lazyPut(() => SpeechFactory(), fenix: true); Get.find().initialize(initialType: SpeechServiceType.azure); // 历史记录服务 @@ -30,30 +29,14 @@ class InitialBinding extends Bindings { Get.lazyPut(() => VolcanoTranslationService(), fenix: true); - // OpenAI服务适配器 - Get.lazyPut(() { - final adapter = OpenAIServiceAdapter(); - adapter.initialize(); - return adapter; - }, fenix: true); - // 蓝牙服务 - Get.put(BluetoothManager(), permanent: true); + Get.lazyPut(() => BluetoothManager(), fenix: true); // BleManager - Get.put(BleManager(), permanent: true); + Get.lazyPut(() => BleManager(), fenix: true); // MusicManager - Get.put(MusicManager(), permanent: true); + Get.lazyPut(() => MusicManager(), fenix: true); // 火山AI服务 - Get.lazyPut(() => VolcanoAIService(), fenix: true); - - // 蓝牙媒体按钮服务 - // Get.lazyPut( - // () => BluetoothMediaButtonService(), - // fenix: true); - - // // 语音交互服务 - // Get.lazyPut(() => VoiceInteractionService(), - // fenix: true); + // Get.lazyPut(() => VolcanoAIService(), fenix: true); // 杰理蓝牙服务 diff --git a/lib/core/themes/theme_service.dart b/lib/core/themes/theme_service.dart index fd438f9d8..a70c3617c 100644 --- a/lib/core/themes/theme_service.dart +++ b/lib/core/themes/theme_service.dart @@ -21,7 +21,7 @@ class ThemeService { Logger.info('从存储读取主题设置: $value'); return value ?? false; } catch (e) { - Logger.error('读取主题设置失败: $e'); + Logger.error('读取主题设置失败: ${e.toString()}'); return false; } } @@ -32,7 +32,7 @@ class ThemeService { Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); await _box.write(_key, isDarkMode); } catch (e) { - Logger.error('保存主题设置失败: $e'); + Logger.error('保存主题设置失败: ${e.toString()}'); } } } \ No newline at end of file diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 3c5a15470..eba8701f5 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -15,77 +15,84 @@ class Logger { /// 当前日志级别,低于此级别的日志不会被输出 static LogLevel _currentLevel = LogLevel.debug; + /// 默认标签 + static const String _defaultTag = 'App'; + /// 设置日志级别 static void setLevel(LogLevel level) { _currentLevel = level; } + /// 输出调试日志 (不带标签版本) + static void debug(String message) { + if (_currentLevel.index <= LogLevel.debug.index) { + _log('DEBUG', _defaultTag, message); + } + } + /// 输出调试日志 - static void debug(String message, {String? tag}) { + static void d(String tag, String message) { if (_currentLevel.index <= LogLevel.debug.index) { - _log('DEBUG', message, tag: tag); + _log('DEBUG', tag, message); + } + } + + /// 输出信息日志 (不带标签版本) + static void info(String message) { + if (_currentLevel.index <= LogLevel.info.index) { + _log('INFO', _defaultTag, message); } } /// 输出信息日志 - static void info(String message, {String? tag}) { + static void i(String tag, String message) { if (_currentLevel.index <= LogLevel.info.index) { - _log('INFO', message, tag: tag); + _log('INFO', tag, message); + } + } + + /// 输出警告日志 (不带标签版本) + static void warning(String message) { + if (_currentLevel.index <= LogLevel.warning.index) { + _log('WARNING', _defaultTag, message); } } /// 输出警告日志 - static void warning(String message, {String? tag}) { + static void w(String tag, String message) { if (_currentLevel.index <= LogLevel.warning.index) { - _log('WARNING', message, tag: tag); + _log('WARNING', tag, message); } } - /// 输出错误日志 - static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) { + /// 输出错误日志 (不带标签版本) + static void error(String message, [dynamic error, StackTrace? stackTrace]) { if (_currentLevel.index <= LogLevel.error.index) { - _log('ERROR', message, tag: tag); + _log('ERROR', _defaultTag, message); if (error != null) { - _log('ERROR', 'Original error: $error', tag: tag); + _log('ERROR', _defaultTag, 'Original error: $error'); } if (stackTrace != null) { - _log('ERROR', 'Stack trace: $stackTrace', tag: tag); + _log('ERROR', _defaultTag, 'Stack trace: $stackTrace'); } } } - /// 获取当前调用栈的类名 - static String? _getCallerClassName() { - try { - final stackTrace = StackTrace.current.toString(); - final frames = stackTrace.split('\n'); - - // 查找调用者帧 (跳过Logger框架内部的帧) - for (int i = 0; i < frames.length; i++) { - final frame = frames[i].trim(); - if (frame.contains('Logger.') || frame.contains('_log')) { - continue; - } - - // 简单的解析类名 - final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s'); - final match = regex.firstMatch(frame); - if (match != null && match.groupCount >= 1) { - return match.group(1); - } - break; + /// 输出错误日志 + static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) { + if (_currentLevel.index <= LogLevel.error.index) { + _log('ERROR', tag, message); + if (error != null) { + _log('ERROR', tag, 'Original error: $error'); + } + if (stackTrace != null) { + _log('ERROR', tag, 'Stack trace: $stackTrace'); } - } catch (e) { - // 忽略解析错误 } - return null; } /// 内部日志输出方法 - static void _log(String level, String message, {String? tag}) { - final timestamp = DateTime.now().toString(); - final className = tag ?? _getCallerClassName() ?? ''; - final tagInfo = className.isNotEmpty ? '[$className] ' : ''; - developer.log('[$timestamp][$level][$tagInfo]: $message'); + static void _log(String level, String tag, String message) { + developer.log('[$tag]: $message'); } } diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 8ce3b77b6..138d559cd 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -54,7 +54,7 @@ class DeviceInfo { this.rightEarCharging = false, this.caseCharging = false, this.firmwareVersion = '', - this.firmwareProductId = 's3', + this.firmwareProductId = '', this.firmwareProductColor = '', DateTime? lastUpdated, }) : lastUpdated = lastUpdated ?? DateTime.now(); @@ -168,7 +168,7 @@ class BleManager extends GetxService { Future.delayed(const Duration(milliseconds: 300), () { // 可选:检查是否仍处于连接状态 if (_isConnected.value) { - fetchDeviceInfo(); + // fetchDeviceInfo(); } }); } else if (state == BleConnectionState.disconnected || @@ -198,7 +198,7 @@ class BleManager extends GetxService { }, ); } catch (e) { - Logger.error('设置BLE事件监听器失败: $e'); + Logger.error('设置BLE事件监听器失败: ${e.toString()}'); } } @@ -225,7 +225,7 @@ class BleManager extends GetxService { } } } catch (e) { - Logger.error('处理设备信息异常: $e'); + Logger.error('处理设备信息异常: ${e.toString()}'); } } @@ -303,10 +303,9 @@ class BleManager extends GetxService { caseCharging: caseCharging, ); - Logger.info( - '设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%'); + Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%'); } catch (e) { - Logger.error('更新电池信息失败: $e'); + Logger.error('更新电池信息失败: ${e.toString()}'); } } @@ -329,7 +328,7 @@ class BleManager extends GetxService { Logger.info('设备版本信息已更新: 版本=$version'); } catch (e) { - Logger.error('更新版本信息失败: $e'); + Logger.error('更新版本信息失败: ${e.toString()}'); } } @@ -352,7 +351,7 @@ class BleManager extends GetxService { Logger.info('设备设备id已更新: 设备id=$productId'); } catch (e) { - Logger.error('更新设备id失败: $e'); + Logger.error('更新设备id失败: ${e.toString()}'); } } @@ -440,7 +439,7 @@ class BleManager extends GetxService { Logger.warning('产品id获取指令发送失败'); } } catch (e) { - Logger.error('获取BLE设备信息失败: $e'); + Logger.error('获取BLE设备信息失败: ${e.toString()}'); } } @@ -455,7 +454,7 @@ class BleManager extends GetxService { return _connectionState.value; } catch (e) { - Logger.error('获取BLE连接状态失败: $e'); + Logger.error('获取BLE连接状态失败: ${e.toString()}'); return BleConnectionState.error; } } @@ -497,6 +496,8 @@ class BleManager extends GetxService { 'openaiModel': openaiModel, 'systemPrompt': systemPrompt, 'mcpServer': jsonEncode(jsonData), + 'volcanoAppId': '2316081933', + 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', }; // 记录日志(不记录敏感信息) @@ -531,7 +532,7 @@ class BleManager extends GetxService { // 如果设备信息存在,则获取详细信息 if (deviceInfo.value != null) { // 获取详细设备信息 - fetchDeviceInfo(); + // fetchDeviceInfo(); } } } else { @@ -540,7 +541,7 @@ class BleManager extends GetxService { return result; } catch (e) { - Logger.error('启动BLE服务失败: $e'); + Logger.error('启动BLE服务失败: ${e.toString()}'); return false; } } @@ -560,7 +561,7 @@ class BleManager extends GetxService { return result; } catch (e) { - Logger.error('停止BLE服务失败: $e'); + Logger.error('停止BLE服务失败: ${e.toString()}'); return false; } } @@ -570,7 +571,7 @@ class BleManager extends GetxService { try { return await _bleService.getBatteryLevel(); } catch (e) { - Logger.error('获取电池电量失败: $e'); + Logger.error('获取电池电量失败: ${e.toString()}'); return 0; } } @@ -580,7 +581,7 @@ class BleManager extends GetxService { try { return await _bleService.getBatteryInfo(); } catch (e) { - Logger.error('获取电池信息失败: $e'); + Logger.error('获取电池信息失败: ${e.toString()}'); return false; } } @@ -590,7 +591,7 @@ class BleManager extends GetxService { try { return await _bleService.getProductId(); } catch (e) { - Logger.error('获取设备版本失败: $e'); + Logger.error('获取设备版本失败: ${e.toString()}'); return false; } } @@ -600,7 +601,7 @@ class BleManager extends GetxService { try { return await _bleService.getDeviceVersion(); } catch (e) { - Logger.error('获取设备版本失败: $e'); + Logger.error('获取设备版本失败: ${e.toString()}'); return false; } } @@ -610,7 +611,7 @@ class BleManager extends GetxService { try { return await _bleService.openEncoder(); } catch (e) { - Logger.error('打开编码器失败: $e'); + Logger.error('打开编码器失败: ${e.toString()}'); return false; } } @@ -620,7 +621,7 @@ class BleManager extends GetxService { try { return await _bleService.openDecoder(); } catch (e) { - Logger.error('打开解码器失败: $e'); + Logger.error('打开解码器失败: ${e.toString()}'); return false; } } @@ -630,7 +631,7 @@ class BleManager extends GetxService { try { return await _bleService.openA2DPDecoder(); } catch (e) { - Logger.error('打开解码器失败: $e'); + Logger.error('打开解码器失败: ${e.toString()}'); return false; } } @@ -640,7 +641,7 @@ class BleManager extends GetxService { try { return await _bleService.closeCodec(); } catch (e) { - Logger.error('关闭编解码器失败: $e'); + Logger.error('关闭编解码器失败: ${e.toString()}'); return false; } } @@ -650,7 +651,7 @@ class BleManager extends GetxService { try { return await _bleService.playA2DP(); } catch (e) { - Logger.error('播放A2DP失败: $e'); + Logger.error('播放A2DP失败: ${e.toString()}'); return false; } } @@ -662,4 +663,4 @@ class BleManager extends GetxService { } return Future.value(Get.find()); } -} +} \ No newline at end of file diff --git a/lib/data/services/bluetooth_manager.dart b/lib/data/services/bluetooth_manager.dart index e6e26ec02..c52a7f4b5 100644 --- a/lib/data/services/bluetooth_manager.dart +++ b/lib/data/services/bluetooth_manager.dart @@ -75,11 +75,11 @@ class BluetoothManager extends GetxService { } }, onError: (error) { - Logger.error('蓝牙事件流错误: $error'); + Logger.error('蓝牙事件流错误: ${error.toString()}'); }, ); } catch (e) { - Logger.error('设置蓝牙事件监听器失败: $e'); + Logger.error('设置蓝牙事件监听器失败: ${e.toString()}'); } } @@ -106,7 +106,7 @@ class BluetoothManager extends GetxService { Logger.info('未找到已连接的蓝牙设备'); } catch (e) { - Logger.error('刷新设备连接失败: $e'); + Logger.error('刷新设备连接失败: ${e.toString()}'); } } @@ -117,7 +117,7 @@ class BluetoothManager extends GetxService { _bluetoothState.value = isEnabled ? 'on' : 'off'; return isEnabled; } catch (e) { - Logger.error('检查蓝牙状态失败: $e'); + Logger.error('检查蓝牙状态失败: ${e.toString()}'); return false; } } diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart index 8749c5714..9a1688314 100644 --- a/lib/data/services/chat_history_service.dart +++ b/lib/data/services/chat_history_service.dart @@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService { // 检查并清理过多的历史记录 await _cleanupOldMessages(agentId); } catch (e) { - Logger.error('保存聊天记录失败', e); + Logger.error('保存聊天记录失败', e.toString()); } } diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart index 022f2aa2e..4701bf674 100644 --- a/lib/data/services/deapsound_ai_service.dart +++ b/lib/data/services/deapsound_ai_service.dart @@ -112,12 +112,12 @@ class DeapsoundAIService implements AiService { } } } catch (e) { - Logger.error('JSON解析错误: $e'); + Logger.error('JSON解析错误: ${e.toString()}'); } } } } catch (e) { - Logger.error('请求异常: $e'); + Logger.error('请求异常: ${e.toString()}'); } finally { client.close(); } diff --git a/lib/data/services/kouzi_ai_service.dart b/lib/data/services/kouzi_ai_service.dart index cf479ac56..c19f23d1a 100644 --- a/lib/data/services/kouzi_ai_service.dart +++ b/lib/data/services/kouzi_ai_service.dart @@ -82,14 +82,14 @@ class KouziAIService implements AiService { } } } catch (e) { - Logger.error('JSON解析错误: $e'); + Logger.error('JSON解析错误: ${e.toString()}'); } } } } catch (e) { - Logger.error('请求异常: $e'); + Logger.error('请求异常: ${e.toString()}'); } finally { client.close(); } } -} +} \ No newline at end of file diff --git a/lib/data/services/login_service.dart b/lib/data/services/login_service.dart index d09ce9672..2844ef0f0 100644 --- a/lib/data/services/login_service.dart +++ b/lib/data/services/login_service.dart @@ -8,6 +8,7 @@ class LoginService { FlWX().register( appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255', appId: 'wx5bbb7be9324f3e2d', + universalLink: 'https://yunqiinnovation.com/app/', params: FlWXCallbackParams(onHttp: (url) async { return ''; })); @@ -18,13 +19,12 @@ class LoginService { Future _initializeFirebase() async { try { // 尝试获取已存在的实例 - Firebase.app('DeepSound'); + Firebase.app(); print('已存在的 Firebase 实例被重用'); } catch (e) { try { // 如果不存在则初始化新实例 await Firebase.initializeApp( - name: 'DeepSound', options: DefaultFirebaseOptions.currentPlatform, ); print('Firebase 初始化成功'); diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart index b1c7a1ec5..dd9a79c68 100644 --- a/lib/data/services/speech_factory.dart +++ b/lib/data/services/speech_factory.dart @@ -88,7 +88,7 @@ class SpeechFactory extends GetxService { Logger.info( '语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); } catch (e) { - Logger.error('语音服务初始化失败: $e'); + Logger.error('语音服务初始化失败: ${e.toString()}'); rethrow; } } @@ -108,4 +108,4 @@ class SpeechFactory extends GetxService { } return Get.find(); } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 5c87577ee..fe0122717 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { - Logger.error('Azure 语音识别服务初始化失败: $e'); + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } @@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService { throw Exception('无效的识别结果格式'); } catch (e) { - Logger.error('语音识别失败: $e'); + Logger.error('语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = true; return _eventStreamController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); _cleanupEventStream(); rethrow; } @@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService { _cleanupEventStream(); return result; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); _cleanupEventStream(); rethrow; } @@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService { final String eventType = eventMap['type'] as String? ?? ''; // 添加日志帮助调试 - Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap'); switch (eventType) { case 'result': @@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService { final String errorDetails = eventMap['errorDetails'] as String? ?? ''; if (reason.isNotEmpty || errorDetails.isNotEmpty) { - Logger.error('识别取消: $reason - $errorDetails'); + Logger.error('识别取消: $reason - ${errorDetails.toString()}'); } _eventStreamController?.add(RecognitionEvent( @@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService { case 'error': final String error = eventMap['message'] as String? ?? ''; - Logger.error('识别错误: $error'); + Logger.error('识别错误: ${error.toString()}'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.error, error: error, @@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService { /// 处理识别事件流错误 void _handleRecognitionError(Object error) { - Logger.error('识别事件流错误: $error'); + Logger.error('识别事件流错误: ${error.toString()}'); _eventStreamController?.addError(error); _cleanupEventStream(); } @@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别资源已释放'); } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); _cleanupEventStream(); _isInitialized = false; } @@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService { return true; } catch (e) { - Logger.error('推送音频数据失败: $e'); + Logger.error('推送音频数据失败: ${e.toString()}'); return false; } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 2e24a90b8..d6eccdf9b 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService { _handleTtsEvent(event); } }, onError: (error) { - Logger.error('TTS事件流错误: $error'); + Logger.error('TTS事件流错误: ${error.toString()}'); _eventStreamController.addError(error); }); } @@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService { void _handleTtsEvent(Map event) { final String eventType = event['type'] as String? ?? ''; - // Logger.debug('收到TTS事件: $eventType, 数据: $event'); + // Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent'); switch (eventType) { case 'synthesis_started': @@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService { _isInitialized = result; return result; } catch (e) { - Logger.error('初始化失败: $e'); + Logger.error('初始化失败: ${e.toString()}'); return false; } } @@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService { if (result) _currentVoice = voiceName; return result; } catch (e) { - Logger.error('设置语音失败: $e'); + Logger.error('设置语音失败: ${e.toString()}'); return false; } } @@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService { return result == true; } catch (e) { - Logger.error('语音合成失败: $e'); + Logger.error('语音合成失败: ${e.toString()}'); return false; } } @@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); return false; } } @@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService { Logger.info('TTS队列播放完成'); return result; } catch (e) { - Logger.error('刷新TTS流失败: $e'); + Logger.error('刷新TTS流失败: ${e.toString()}'); return false; } } @@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService { _textQueue.removeAt(0); } } catch (e) { - Logger.error('处理语音队列出错: $e'); + Logger.error('处理语音队列出错: ${e.toString()}'); // 出错时移除当前项并尝试继续 if (_textQueue.isNotEmpty) { @@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('停止语音合成出错: $e'); + Logger.error('停止语音合成出错: ${e.toString()}'); return false; } } @@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService { await _channel.invokeMethod('dispose'); _isInitialized = false; } catch (e) { - Logger.error('释放资源失败: $e'); + Logger.error('释放资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart index 438953af8..c8716ad8f 100644 --- a/lib/data/services/speech_impl/flutter_asr_service.dart +++ b/lib/data/services/speech_impl/flutter_asr_service.dart @@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService { } catch (e) { _isAvailable.value = false; _asrState.value = AsrState.error; - Logger.error('语音识别初始化失败: $e'); + Logger.error('语音识别初始化失败: ${e.toString()}'); } } @@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService { return result; } catch (e) { - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); _asrState.value = AsrState.error; return false; } @@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService { ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')); } } catch (e) { - Logger.error('重启连续识别失败: $e'); + Logger.error('重启连续识别失败: ${e.toString()}'); // 发送错误给控制器 _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e')); @@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService { await _initSpeech(); return _isAvailable.value; } catch (e) { - Logger.error('语音识别初始化失败: $e'); + Logger.error('语音识别初始化失败: ${e.toString()}'); return false; } } @@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService { }, ); } catch (e) { - Logger.error('一次性语音识别失败: $e'); + Logger.error('一次性语音识别失败: ${e.toString()}'); return RecognitionEvent.error('$e'); } } @@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService { _isListening = true; return _continuousRecognitionController!.stream; } catch (e) { - Logger.error('启动连续语音识别失败: $e'); + Logger.error('启动连续语音识别失败: ${e.toString()}'); // 如果已经创建了控制器,使用它 if (_continuousRecognitionController != null) { @@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService { _asrState.value = AsrState.done; return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); // 强制重置状态 _isListening = false; @@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService { _isListening = false; _asrState.value = AsrState.notInitialized; } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_tts_service.dart b/lib/data/services/speech_impl/flutter_tts_service.dart index 810f82787..aca6fca66 100644 --- a/lib/data/services/speech_impl/flutter_tts_service.dart +++ b/lib/data/services/speech_impl/flutter_tts_service.dart @@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('FlutterTtsService 已初始化'); } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); } } @@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); return true; } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); return false; } } @@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService { _currentVoice.value = _supportedVoices.first; } } catch (e) { - Logger.error('加载TTS语音失败: $e'); + Logger.error('加载TTS语音失败: ${e.toString()}'); // 添加默认语音 if (_supportedVoices.isEmpty) { @@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('设置TTS语音: $voiceName'); return true; } catch (e) { - Logger.error('设置TTS语音失败: $e'); + Logger.error('设置TTS语音失败: ${e.toString()}'); return false; } } @@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('设置语音参数失败: $e'); + Logger.error('设置语音参数失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('设置语音参数失败: $e')); @@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService { // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); - Logger.error('TTS播放失败: $e'); + Logger.error('TTS播放失败: ${e.toString()}'); return false; } } @@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS队列播放完成'); return true; } catch (e) { - Logger.error('等待TTS队列播放完成失败: $e'); + Logger.error('等待TTS队列播放完成失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService { } } } catch (e) { - Logger.error('TTS处理队列失败: $e'); + Logger.error('TTS处理队列失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS停止播放完成'); return true; } catch (e) { - Logger.error('TTS停止播放失败: $e'); + Logger.error('TTS停止播放失败: ${e.toString()}'); // 即使出错也尝试强制重置状态 _isSpeaking.value = false; @@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS资源已释放'); } catch (e) { - Logger.error('释放TTS资源失败: $e'); + Logger.error('释放TTS资源失败: ${e.toString()}'); } } @@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService { await flushStream(); return true; } catch (e) { - Logger.error('处理文本流时出错: $e'); + Logger.error('处理文本流时出错: ${e.toString()}'); return false; } } diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index 2f57939a9..43079325f 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { ); return true; } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); return false; } } @@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { Logger.info('设置TTS语音: $voiceName'); return true; } catch (e) { - Logger.error('设置TTS语音失败: $e'); + Logger.error('设置TTS语音失败: ${e.toString()}'); return false; } } @@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { _handle(message); }, onError: (error) { - Logger.error('连接失败: $error'); + Logger.error('连接失败: ${error.toString()}'); }, onDone: () { Logger.error('WebSocket 连接已关闭!'); @@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { cancelOnError: true, ); } catch (e) { - Logger.error('初始化异常: $e'); + Logger.error('初始化异常: ${e.toString()}'); } } @@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { _isSpeaking.value = false; - Logger.error('语音合成失败: $e'); + Logger.error('语音合成失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('语音合成失败: $e')); @@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('处理流式文本失败: $e')); @@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('刷新TTS流失败: $e'); + Logger.error('刷新TTS流失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('刷新TTS流失败: $e')); @@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { await speakOnce(item); } } catch (e) { - Logger.error('处理语音队列出错: $e'); + Logger.error('处理语音队列出错: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('处理语音队列出错: $e')); @@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } return true; } catch (e) { - Logger.error('停止语音合成出错: $e'); + Logger.error('停止语音合成出错: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('停止语音合成出错: $e')); return false; @@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { await _eventController.close(); _isInitialized = false; } catch (e) { - Logger.error('释放资源失败: $e'); + Logger.error('释放资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 3ffc4e75e..26f18e1c5 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('火山语音API已初始化'); return true; } catch (e) { - Logger.error('初始化火山语音API失败: $e'); + Logger.error('初始化火山语音API失败: ${e.toString()}'); return false; } } @@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService { return resultCompleter.future; } catch (e) { - Logger.error('单次语音识别失败: $e'); + Logger.error('单次语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService { return _recognitionController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); await _cleanupResources(); // 返回一个包含错误的流 @@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService { return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); // 确保资源被清理 await _cleanupResources(); @@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService { _isInitialized = false; Logger.info('火山语音API资源已释放'); } catch (e) { - Logger.error('释放火山语音API资源失败: $e'); + Logger.error('释放火山语音API资源失败: ${e.toString()}'); _isContinuousRecognitionActive = false; _isInitialized = false; } @@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('录音机初始化成功'); } catch (e) { // _recorder = null; - Logger.error('初始化录音机失败: $e'); + Logger.error('初始化录音机失败: ${e.toString()}'); rethrow; } } @@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService { _connectionCompleter!.completeError(e); } - Logger.error('连接WebSocket失败: $e'); + Logger.error('连接WebSocket失败: ${e.toString()}'); rethrow; } @@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(heartbeatMsg)); } catch (e) { - Logger.error('发送心跳包失败: $e'); + Logger.error('发送心跳包失败: ${e.toString()}'); } } } @@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(startCommand)); Logger.info('已发送开始识别指令'); } catch (e) { - Logger.error('发送开始识别指令失败: $e'); + Logger.error('发送开始识别指令失败: ${e.toString()}'); rethrow; } } @@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('录音已开始'); } catch (e) { _isRecording = false; - Logger.error('开始录音失败: $e'); + Logger.error('开始录音失败: ${e.toString()}'); rethrow; } */ @@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService { await _sendAudioData(audioData); } } catch (e) { - Logger.error('捕获和发送音频失败: $e'); + Logger.error('捕获和发送音频失败: ${e.toString()}'); } */ } @@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(audioMessage)); } catch (e) { - Logger.error('发送音频数据失败: $e'); + Logger.error('发送音频数据失败: ${e.toString()}'); } } @@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(endCommand)); Logger.info('已发送结束指令'); } catch (e) { - Logger.error('发送结束指令失败: $e'); + Logger.error('发送结束指令失败: ${e.toString()}'); } } @@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService { _isRecording = false; Logger.info('录音已停止'); } catch (e) { - Logger.error('停止录音失败: $e'); + Logger.error('停止录音失败: ${e.toString()}'); } } */ @@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService { } } } catch (e) { - Logger.error('处理WebSocket消息失败: $e'); + Logger.error('处理WebSocket消息失败: ${e.toString()}'); if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.addError('处理消息失败: $e'); @@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService { } } } catch (e) { - Logger.error('处理结果消息失败: $e'); + Logger.error('处理结果消息失败: ${e.toString()}'); } } @@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService { /// 处理WebSocket错误 void _handleWebSocketError(dynamic error) { - Logger.error('WebSocket错误: $error'); + Logger.error('WebSocket错误: ${e.toString()}rror'); if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { _connectionCompleter!.completeError(error); @@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.close(); _channel = null; } catch (e) { - Logger.error('关闭WebSocket连接失败: $e'); + Logger.error('关闭WebSocket连接失败: ${e.toString()}'); } } } @@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService { _isContinuousRecognitionActive = false; _isRecognitionEndRequested = false; } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/volcano_tts_api_service.dart b/lib/data/services/speech_impl/volcano_tts_api_service.dart index e01e253ed..8a3196c03 100644 --- a/lib/data/services/speech_impl/volcano_tts_api_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_api_service.dart @@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController { _handleMessage, onError: (error) { // 直接处理错误,不尝试重连 - Logger.error('WebSocket错误: $error'); + Logger.error('WebSocket错误: ${error.toString()}'); if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'error', @@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController { }); } } catch (e) { - Logger.error('处理TTS连接超时事件失败: $e'); + Logger.error('处理TTS连接超时事件失败: ${e.toString()}'); } } timeout = null; @@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController { return result; } catch (e) { // 只记录错误,不尝试重连 - Logger.error('连接过程中发生异常: $e'); + Logger.error('连接过程中发生异常: ${e.toString()}'); return false; } finally { isConnecting.value = false; @@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController { 'error': '会话开始超时' }); } catch (e) { - Logger.error('处理TTS会话超时事件失败: $e'); + Logger.error('处理TTS会话超时事件失败: ${e.toString()}'); } } timeout = null; @@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController { return result; } catch (e) { // 只记录错误,不尝试恢复 - Logger.error('开始TTS会话失败: $e'); + Logger.error('开始TTS会话失败: ${e.toString()}'); return false; } } @@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController { 'error': error.toString() }); - Logger.error('火山语音TTS错误: $error'); + Logger.error('火山语音TTS错误: ${error.toString()}'); } catch (e) { // 如果在处理错误时发生异常,记录到日志但不再抛出异常 - Logger.error('处理TTS错误时发生异常: $e,原始错误: $error'); + Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}'); } } @@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController { return success; } catch (e) { - Logger.error('TTS一次性合成失败: $e'); + Logger.error('TTS一次性合成失败: ${e.toString()}'); // 确保会话结束 endSession(); return false; diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index 47d7aa2eb..402aac5b8 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_service.dart @@ -87,11 +87,11 @@ class VolcanoTtsService extends GetxService implements TtsService { case 'error': _isSpeaking = false; - final errorCode = eventMap['errorCode'] as int? ?? 0; + final errorCode = eventMap['errorCode'] as String? ?? "-1"; final errorMessage = eventMap['errorMessage'] as String? ?? '未知错误'; _eventStreamController.add(TtsEvent( type: TtsEventType.error, - error: errorMessage, + error: '$errorMessage (错误码: $errorCode)', )); break; } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index ef6791e3f..00b151019 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别服务初始化成功'); return true; } catch (e) { - Logger.error('Azure 语音识别服务初始化失败: $e'); + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } @@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService { try { throw Exception('无效的识别结果格式'); } catch (e) { - Logger.error('语音识别失败: $e'); + Logger.error('语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = true; return _eventStreamController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); rethrow; } } @@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService { }, onError: (error) { isConnected = false; - Logger.error('连接失败: $error'); + Logger.error('连接失败: ${error.toString()}'); }, onDone: () { isConnected = false; @@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService { cancelOnError: true, ); } catch (e) { - Logger.error("初始化异常----------------$e"); + Logger.error("初始化异常----------------${e.toString()}"); } } @@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } } } - Logger.info( + Logger.info( "识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role"); if (msg.isNotEmpty) { if (data['cn']['st']['type'] == "0") { @@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } } } catch (e) { - Logger.error("接受消息异常 $e"); + Logger.error("接受消息异常 ${e.toString()}"); } } @@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = false; return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); rethrow; } } @@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别资源已释放'); } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); _isInitialized = false; } } -} +} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart index f4771ffac..5dd9f96e7 100644 --- a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart @@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService { _handleVoiceInteractionEvent(event); } }, onError: (error) { - Logger.error('语音交互事件通道错误: $error'); + Logger.error('语音交互事件通道错误: ${error.toString()}'); }); } @@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService { Logger.info('语音交互服务初始化完成'); return this; } catch (e) { - Logger.error('语音交互服务初始化失败: $e'); + Logger.error('语音交互服务初始化失败: ${e.toString()}'); return this; } } @@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService { _isServiceRunning.value = isRunning; Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); } catch (e) { - Logger.error('检查语音交互服务状态失败: $e'); + Logger.error('检查语音交互服务状态失败: ${e.toString()}'); _isServiceRunning.value = false; } } @@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('启动语音交互服务失败: $e'); + Logger.error('启动语音交互服务失败: ${e.toString()}'); return false; } } @@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('停止语音交互服务失败: $e'); + Logger.error('停止语音交互服务失败: ${e.toString()}'); return false; } } @@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('暂停语音交互失败: $e'); + Logger.error('暂停语音交互失败: ${e.toString()}'); return false; } } @@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService { final int timestamp = eventMap['timestamp'] as int? ?? 0; // 添加时间戳日志,帮助调试 - Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); + Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); switch (eventType) { case 'chatHistory': @@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService { break; default: - Logger.warning('收到未知类型的语音交互事件: $eventType'); + Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}'); break; } } @@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService { // 保存更新后的历史记录 await chatHistoryService.saveHistory(agentId, existingMessages); } catch (e) { - Logger.error('保存聊天记录失败: $e'); + Logger.error('保存聊天记录失败: ${e.toString()}'); } } diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart index 46dbb2391..1fc80ba89 100644 --- a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart @@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService { // 保持idle状态,不自动启动语音识别 Logger.info('iOS语音交互服务初始化完成,处于idle状态'); } catch (e) { - Logger.error('iOS语音交互服务初始化失败: $e'); + Logger.error('iOS语音交互服务初始化失败: ${e.toString()}'); } return this; @@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService { // 监听识别结果 _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent, onError: (error) { - Logger.error('语音识别错误: $error'); + Logger.error('语音识别错误: ${error.toString()}'); _startContinuousListening(); // 尝试重新启动 }); Logger.info('开始连续语音识别'); } catch (e) { - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); } } @@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService { // 处理完成 isProcessingAI.value = false; } catch (e) { - Logger.error('AI响应处理失败: $e'); + Logger.error('AI响应处理失败: ${e.toString()}'); // 错误恢复 isProcessingAI.value = false; } @@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService { Logger.info('保存聊天历史记录成功'); } catch (e) { - Logger.error('保存聊天历史记录失败: $e'); + Logger.error('保存聊天历史记录失败: ${e.toString()}'); } } @@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService { _eventStreamController.add(event); Logger.info('已发送识别开始事件'); } -} +} \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 1f8b9593e..2fb1540d1 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -66,7 +66,7 @@ class DefaultFirebaseOptions { storageBucket: 'deepsound-e0e3d.firebasestorage.app', androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com', iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com', - iosBundleId: 'com.example.client', + iosBundleId: 'com.yunqiinnovation.deepsound', ); static const FirebaseOptions macos = FirebaseOptions( @@ -77,7 +77,7 @@ class DefaultFirebaseOptions { storageBucket: 'deepsound-e0e3d.firebasestorage.app', androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com', iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com', - iosBundleId: 'com.example.client', + iosBundleId: 'com.yunqiinnovation.deepsound', ); static const FirebaseOptions windows = FirebaseOptions( diff --git a/lib/main.dart b/lib/main.dart index 72b596679..9086063c7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,8 +24,10 @@ void main() async { // 初始化环境变量 await dotenv.load(); + + // 初始化登录服务(包含Firebase初始化) LoginService(); - //LoginService.FirebaseInit(); + // 设置窗口大小(仅在 macOS 下生效) if (Platform.isMacOS) { const double width = 375.0; diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 34174a658..536514dbf 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:agent_service/agent_service.dart'; -import 'package:logger/logger.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'package:get_storage/get_storage.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:app_settings/app_settings.dart'; +import 'package:deep_voice/core/utils/logger.dart'; class Message { final bool isUser; //标识信息是否来自用户还是AI @@ -36,8 +36,6 @@ class Message { class AgentController extends GetxController { static AgentController get to => Get.find(); //单例访问模式 - final logger = Logger(); - // 聊天消息列表 final messages = [].obs; @@ -78,6 +76,8 @@ class AgentController extends GetxController { bool _isNewAssistantResponse = true; String _currentResponseId = ''; + final TAG = 'AgentController'; + // 会话ID final String sessionId = 'default_agent'; @@ -109,7 +109,7 @@ class AgentController extends GetxController { isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; } catch (e) { // 如果读取失败,使用默认值 - logger.e('加载TTS偏好设置失败: $e'); + Logger.e(TAG, '加载TTS偏好设置失败: $e'); isTtsEnabled.value = true; } } @@ -128,14 +128,14 @@ class AgentController extends GetxController { final storage = GetStorage(); await storage.write('agent_tts_enabled', isTtsEnabled.value); } catch (e) { - logger.e('保存TTS偏好设置失败: $e'); + Logger.e(TAG, '保存TTS偏好设置失败: $e'); } } // 加载聊天历史记录 Future _loadChatHistory() async { try { - logger.i('正在加载聊天历史...'); + Logger.i(TAG, '正在加载聊天历史...'); final result = await ChatStorage.getMessages( sessionId: sessionId, page: 1, @@ -160,14 +160,14 @@ class AgentController extends GetxController { historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); - logger.i('成功加载 ${historyMessages.length} 条聊天记录'); + Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); - logger.i('没有历史记录,添加欢迎消息'); + Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { - logger.e('加载聊天历史失败: $e'); + Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); } finally { isHistoryLoaded.value = true; @@ -272,12 +272,14 @@ class AgentController extends GetxController { case AgentServiceEventType.ttsStarted: isSpeaking.value = true; + Logger.i(TAG, 'TTS开始播放'); break; case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsCanceled: isSpeaking.value = false; + Logger.i(TAG, 'TTS停止播放'); break; case AgentServiceEventType.imageProcessing: @@ -339,7 +341,7 @@ class AgentController extends GetxController { messages.refresh(); } - logger.e('代理服务错误: ${event.data['message']}'); + Logger.e(TAG, '代理服务错误: ${event.data['message']}'); break; case AgentServiceEventType.autoStop: @@ -373,14 +375,14 @@ class AgentController extends GetxController { case AgentServiceEventType.functionCall: final functionName = event.data['name'] ?? ''; final arguments = event.data['arguments'] ?? ''; - // logger.i('Fultter 开始调用 mcp: $functionName, $arguments'); + // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); break; case AgentServiceEventType.functionCallResult: -final responseId = event.data['responseId'] ?? ''; + final responseId = event.data['responseId'] ?? ''; final result = event.data['result'] ?? ''; final functionCall = event.data['function_call'] ?? ''; - logger.i('Flutter 结束调用 mcp: $result, $functionCall'); + Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall'); if (result.isNotEmpty) { final parsedResult = jsonDecode(result); final meta = parsedResult["meta"]; @@ -390,7 +392,7 @@ final responseId = event.data['responseId'] ?? ''; try { metaResult = jsonDecode(meta); } catch (e) { - print('解析meta数据失败: $e'); + Logger.e(TAG, '解析meta数据失败: $e'); } } if(metaResult.isNotEmpty){ @@ -452,7 +454,7 @@ final responseId = event.data['responseId'] ?? ''; await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); } catch (e) { - logger.e('发送文本消息失败: $e'); + Logger.e(TAG, '发送文本消息失败: $e'); isProcessing.value = false; } } @@ -460,7 +462,7 @@ final responseId = event.data['responseId'] ?? ''; // 发送图片消息 Future sendImageMessage(String imagePath, {String? text}) async { if (imagePath.isEmpty || !File(imagePath).existsSync()) { - logger.e('图片不存在: $imagePath'); + Logger.e(TAG, '图片不存在: $imagePath'); return; } @@ -494,7 +496,7 @@ final responseId = event.data['responseId'] ?? ''; speakResponse: isTtsEnabled.value, ); } catch (e) { - logger.e('处理图片失败: $e'); + Logger.e(TAG, '处理图片失败: $e'); isProcessing.value = false; isImageProcessing.value = false; } @@ -531,36 +533,73 @@ final responseId = event.data['responseId'] ?? ''; // 检查麦克风权限 Future _checkMicrophonePermission() async { try { - PermissionStatus status = await Permission.microphone.status; - - // 如果已授权,直接返回 true - if (status.isGranted) { + // 创建待请求权限列表 + List permissionsToRequest = []; + + if (Platform.isAndroid) { + // Android 需要考虑SDK版本不同的权限要求 + permissionsToRequest.add(Permission.microphone); + + + } else if (Platform.isIOS) { + // iOS 只需要麦克风权限 + permissionsToRequest.add(Permission.microphone); + } + + // 批量请求所有需要的权限 + final statuses = await permissionsToRequest.request(); + + // 检查是否所有权限都已授予 + bool allGranted = true; + Permission? deniedPermission; + + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + deniedPermission = permission; + Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status'); + } + }); + + // 单独检查麦克风权限状态,因为这是主要需求 + final micStatus = await Permission.microphone.status; + + // 如果所有权限都已授予,或至少麦克风权限已授予,返回true + if (allGranted || micStatus.isGranted) { return true; } - - // 如果被永久拒绝,提示用户前往设置页面 - if (status.isPermanentlyDenied) { + + // 处理特定权限被永久拒绝的情况 + if (micStatus.isPermanentlyDenied) { _showPermissionDialog(true); return false; } - - // 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 - if (status.isDenied) { - status = await Permission.microphone.request(); - if (status.isGranted) { - return true; - } else { - _showPermissionDialog(false); - return false; - } + + // 普通拒绝情况 + if (deniedPermission != null) { + _showPermissionDialog(false); } - + return false; } catch (e) { - logger.e('检查麦克风权限失败: $e'); + Logger.e(TAG, '检查麦克风权限失败: $e'); return false; } } + + // 获取Android SDK版本 + Future _getAndroidSdkVersion() async { + if (Platform.isAndroid) { + try { + // 这里我们简单返回一个较高的值,确保请求所有可能需要的权限 + // 在实际应用中,你可能需要使用platform channel获取真实的SDK版本 + return 31; // 对应Android 12 + } catch (e) { + Logger.e(TAG, '获取Android SDK版本失败: $e'); + } + } + return 0; + } // 显示权限提示对话框 void _showPermissionDialog(bool isPermanent) { @@ -605,7 +644,7 @@ final responseId = event.data['responseId'] ?? ''; try { await AgentService.startConversation(); } catch (e) { - logger.e('启动语音输入失败: $e'); + Logger.e(TAG, '启动语音输入失败: $e'); } } @@ -616,7 +655,7 @@ final responseId = event.data['responseId'] ?? ''; try { await AgentService.stopConversation(); } catch (e) { - logger.e('停止语音输入失败: $e'); + Logger.e(TAG, '停止语音输入失败: $e'); } } @@ -634,7 +673,7 @@ final responseId = event.data['responseId'] ?? ''; // 重置回复状态 _isNewAssistantResponse = true; } catch (e) { - logger.e('清除聊天历史失败: $e'); + Logger.e(TAG, '清除聊天历史失败: $e'); } } @@ -656,7 +695,7 @@ final responseId = event.data['responseId'] ?? ''; isImageInputActive.value = true; } } catch (e) { - logger.e('选择图片失败: $e'); + Logger.e(TAG, '选择图片失败: $e'); } } @@ -678,7 +717,7 @@ final responseId = event.data['responseId'] ?? ''; isImageInputActive.value = true; } } catch (e) { - logger.e('拍照失败: $e'); + Logger.e(TAG, '拍照失败: $e'); } } @@ -720,7 +759,7 @@ final responseId = event.data['responseId'] ?? ''; // 播放音乐 void musicStatus() async { var status = await _musiceManager.getStatus(); - // logger.i('获取当前音乐播放状态 $status'); + // debugPrint('获取当前音乐播放状态 $status'); isPlaying.value = status['isPlaying']; currentUrl.value = status['url']; } diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 40e372c43..64668f38a 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'package:amap_service/amap_service.dart'; import '../../../data/services/spotify_service.dart'; import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; @@ -8,7 +7,6 @@ import 'package:get_storage/get_storage.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:just_audio/just_audio.dart'; import 'package:chat_storage/chat_storage.dart'; -import 'package:path/path.dart'; import '../../../data/services/ai_service.dart'; import '../../../data/services/deapsound_ai_service.dart'; import '../../../data/services/kouzi_ai_service.dart'; @@ -32,7 +30,6 @@ class ChatController extends GetxController { // 添加ScrollController用于控制列表滚动 final ScrollController scrollController = ScrollController(); - final _amapFlutter = AmapService(); StreamSubscription? _locationStream; String _latLng = ''; String _city = '深圳市'; @@ -187,7 +184,6 @@ class ChatController extends GetxController { // 自动停止计时器会在startVoiceRecognition中启动 } - _getLocation(); player.playerStateStream.listen((state) async { if (state.processingState == ProcessingState.completed) { @@ -203,15 +199,6 @@ class ChatController extends GetxController { }); } - void _getLocation() async { - await _amapFlutter.initLocation(); - _locationStream = _amapFlutter.locationStream().listen((data) async { - _latLng = '${data['longitude']},${data['latitude']}}'; - _city = data['city']; - _amapFlutter.stopLocation(); - }); - _amapFlutter.startLocation(); - } @override void onClose() { @@ -228,7 +215,6 @@ class ChatController extends GetxController { _talkDebounceTimer?.cancel(); _talkStartDebounceTimer?.cancel(); - _amapFlutter.dispose(); _locationStream?.cancel(); // 停止TTS @@ -451,7 +437,7 @@ class ChatController extends GetxController { } isPlay.value = true; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -469,7 +455,7 @@ class ChatController extends GetxController { isPlay.value = true; audiourl.value = card['url'] as String; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -479,7 +465,7 @@ class ChatController extends GetxController { _spotifService.pause(); isPlay.value = false; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -872,7 +858,7 @@ class ChatController extends GetxController { } // 删除数据库中的历史记录 ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { - Logger.error('清空数据库聊天记录失败: $e'); + Logger.error('清空数据库聊天记录失败: ${e.toString()}'); }); } @@ -1003,4 +989,4 @@ class ChatController extends GetxController { .abs() < 5000); } -} +} \ No newline at end of file diff --git a/lib/modules/common/webview_page.dart b/lib/modules/common/webview_page.dart index daa78ecf2..b80bf348b 100644 --- a/lib/modules/common/webview_page.dart +++ b/lib/modules/common/webview_page.dart @@ -23,13 +23,13 @@ class WebViewPage extends StatelessWidget { centerTitle: true, title: Text(title), leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, + ), + onPressed: () => Get.back(), ), - onPressed: () => Get.back(), - ), backgroundColor: isDarkMode ? Colors.black : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black, elevation: 0, @@ -39,4 +39,4 @@ class WebViewPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/jieli_test/bindings/jieli_test_binding.dart b/lib/modules/jieli_test/bindings/jieli_test_binding.dart deleted file mode 100644 index 9df61ef94..000000000 --- a/lib/modules/jieli_test/bindings/jieli_test_binding.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/jieli_test_controller.dart'; - -class JieliTestBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => JieliTestController(), - ); - } -} \ No newline at end of file diff --git a/lib/modules/jieli_test/controllers/jieli_test_controller.dart b/lib/modules/jieli_test/controllers/jieli_test_controller.dart deleted file mode 100644 index b6d896836..000000000 --- a/lib/modules/jieli_test/controllers/jieli_test_controller.dart +++ /dev/null @@ -1,323 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -import 'package:flutter/material.dart' hide ConnectionState; -import 'package:get/get.dart'; -import 'package:jieli/jieli.dart'; - -/// 杰理测试控制器 -class JieliTestController extends GetxController { - // Jieli插件实例 - late Jieli jieli; - - // 状态变量 - var isScanning = false.obs; - var deviceInfo = '未获取'.obs; - var connectStatus = '未连接'.obs; - var currentDevice = Rx(null); - var scannedDevices = RxList([]); - var wakeupReceived = false.obs; - var audioDataSize = 0.obs; - var receivedFrames = 0.obs; - var audioDataReceiving = false.obs; - var volumeCommandResult = ''.obs; - - // 事件订阅 - StreamSubscription? _scanSubscription; - StreamSubscription? _connectionSubscription; - StreamSubscription? _wakeupSubscription; - StreamSubscription? _audioSubscription; - StreamSubscription? _commandResultSubscription; - - @override - void onInit() { - super.onInit(); - _initializeJieli(); - } - - @override - void onClose() { - _cancelAllSubscriptions(); - _disposeJieli(); - super.onClose(); - } - - /// 取消所有订阅 - void _cancelAllSubscriptions() { - _scanSubscription?.cancel(); - _connectionSubscription?.cancel(); - _wakeupSubscription?.cancel(); - _audioSubscription?.cancel(); - _commandResultSubscription?.cancel(); - } - - /// 初始化杰理SDK - void _initializeJieli() { - try { - jieli = Jieli(); - _setupEventListeners(); - } catch (e) { - // 初始化错误处理 - } - } - - /// 设置事件监听器 - void _setupEventListeners() { - // 监听扫描结果 - _scanSubscription = jieli.scanResults.listen((devices) { - scannedDevices.assignAll(devices); - }); - - // 监听连接状态 - _connectionSubscription = jieli.connectionState.listen((state) { - switch (state) { - case ConnectionState.connected: - connectStatus.value = '已连接'; - break; - case ConnectionState.disconnected: - connectStatus.value = '已断开'; - currentDevice.value = null; - break; - case ConnectionState.connecting: - connectStatus.value = '连接中...'; - break; - case ConnectionState.disconnecting: - connectStatus.value = '断开中...'; - break; - case ConnectionState.error: - connectStatus.value = '连接错误'; - break; - } - }); - - // 监听唤醒信号 - _wakeupSubscription = jieli.wakeupStream.listen((wakeup) { - wakeupReceived.value = true; - Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号'); - }); - - // 监听音频数据 - _audioSubscription = jieli.audioDataStream.listen((data) { - // 更新音频数据统计 - audioDataSize.value += data.length; - receivedFrames.value++; - audioDataReceiving.value = true; - }); - - // 监听命令结果 - _commandResultSubscription = jieli.commandResultStream.listen((result) { - // 处理音量调整命令结果 - if (result.commandType == Jieli.commandTypeVolume) { - volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败'; - Get.snackbar( - result.success ? '音量调整成功' : '音量调整失败', - result.success ? '设备已更新音量' : '设备未能更新音量', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7), - colorText: Colors.white, - duration: Duration(seconds: 2), - ); - } - }); - } - - /// 检查蓝牙状态 - Future checkBluetoothStatus() async { - try { - final isEnabled = await jieli.isBluetoothEnabled(); - if (!isEnabled) { - return await jieli.requestBluetoothEnable(); - } - return true; - } catch (e) { - return false; - } - } - - /// 开始扫描设备 - Future startScan() async { - if (isScanning.value) return; - - final bluetoothReady = await checkBluetoothStatus(); - if (!bluetoothReady) { - Get.snackbar('蓝牙未启用', '请启用蓝牙后再试'); - return; - } - - try { - isScanning.value = true; - - // 清空之前的设备列表 - scannedDevices.clear(); - - // 开始扫描,10秒后自动停止 - final success = await jieli.startScan(scanTimeout: 10000); - - if (!success) { - isScanning.value = false; - Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限'); - } - - // 10秒后自动更新UI状态 - Future.delayed(Duration(milliseconds: 10000), () { - isScanning.value = false; - }); - - } catch (e) { - isScanning.value = false; - Get.snackbar('扫描错误', e.toString()); - } - } - - /// 停止扫描 - Future stopScan() async { - if (!isScanning.value) return; - - try { - await jieli.stopScan(); - isScanning.value = false; - } catch (e) { - // 停止扫描错误处理 - } - } - - /// 连接到指定设备 - Future connectToDevice(BleDevice device) async { - try { - // 先断开当前连接 - if (currentDevice.value != null) { - await disconnectDevice(); - } - - // 更新UI状态 - connectStatus.value = '连接中...'; - - // 重置音频和唤醒相关的状态 - wakeupReceived.value = false; - audioDataSize.value = 0; - receivedFrames.value = 0; - audioDataReceiving.value = false; - - // 连接设备 - final success = await jieli.connect(device.id); - - if (success) { - // 连接成功,更新当前设备 - currentDevice.value = device; - deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm'; - } else { - deviceInfo.value = '连接失败'; - } - } catch (e) { - deviceInfo.value = '连接错误: ${e.toString()}'; - } - } - - /// 断开设备连接 - Future disconnectDevice() async { - if (currentDevice.value == null) return; - - try { - connectStatus.value = '断开中...'; - - final success = await jieli.disconnect(); - - if (success) { - deviceInfo.value = '未连接设备'; - } - - } catch (e) { - // 断开连接错误处理 - } - } - - /// 发送开始录音命令 - Future sendStartRecordingCommand() async { - if (currentDevice.value == null) { - Get.snackbar('操作失败', '未连接设备'); - return; - } - - try { - final success = await jieli.sendStartRecordingCommand(); - - if (success) { - Get.snackbar('命令已发送', '已发送开始录音命令'); - } else { - Get.snackbar('命令发送失败', '发送开始录音命令失败'); - } - } catch (e) { - Get.snackbar('命令错误', e.toString()); - } - } - - /// 发送停止录音命令 - Future sendStopRecordingCommand() async { - if (currentDevice.value == null) { - Get.snackbar('操作失败', '未连接设备'); - return; - } - - try { - final success = await jieli.sendStopRecordingCommand(); - - if (success) { - Get.snackbar('命令已发送', '已发送停止录音命令'); - } else { - Get.snackbar('命令发送失败', '发送停止录音命令失败'); - } - } catch (e) { - Get.snackbar('命令错误', e.toString()); - } - } - - /// 增加音量 - Future increaseVolume() async { - if (currentDevice.value == null) { - Get.snackbar('操作失败', '未连接设备'); - return; - } - - try { - volumeCommandResult.value = '发送中...'; - final success = await jieli.increaseVolume(); - - if (!success) { - Get.snackbar('命令发送失败', '发送增加音量命令失败'); - volumeCommandResult.value = '发送失败'; - } - } catch (e) { - Get.snackbar('命令错误', e.toString()); - volumeCommandResult.value = '发送出错'; - } - } - - /// 减小音量 - Future decreaseVolume() async { - if (currentDevice.value == null) { - Get.snackbar('操作失败', '未连接设备'); - return; - } - - try { - volumeCommandResult.value = '发送中...'; - final success = await jieli.decreaseVolume(); - - if (!success) { - Get.snackbar('命令发送失败', '发送减小音量命令失败'); - volumeCommandResult.value = '发送失败'; - } - } catch (e) { - Get.snackbar('命令错误', e.toString()); - volumeCommandResult.value = '发送出错'; - } - } - - /// 清理资源 - void _disposeJieli() { - try { - jieli.dispose(); - } catch (e) { - // 释放资源错误处理 - } - } -} \ No newline at end of file diff --git a/lib/modules/jieli_test/views/jieli_test_view.dart b/lib/modules/jieli_test/views/jieli_test_view.dart deleted file mode 100644 index 46409651b..000000000 --- a/lib/modules/jieli_test/views/jieli_test_view.dart +++ /dev/null @@ -1,568 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../controllers/jieli_test_controller.dart'; -import 'package:jieli/jieli.dart'; - -class JieliTestView extends GetView { - const JieliTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - elevation: 0.5, - title: Text( - '杰理蓝牙测试', - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - ), - body: Padding( - padding: EdgeInsets.all(16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildStatusCard(isDarkMode), - SizedBox(height: 16.h), - _buildActionButtons(isDarkMode), - SizedBox(height: 16.h), - _buildDeviceInfo(isDarkMode), - SizedBox(height: 16.h), - _buildDevicesList(isDarkMode), - ], - ), - ), - ); - } - - Widget _buildStatusCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - '连接状态: ', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Obx(() => Text( - controller.connectStatus.value, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.bold, - color: _getStatusColor(controller.connectStatus.value), - ), - )), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx(() => _buildStatusChip( - label: '唤醒信号', - isActive: controller.wakeupReceived.value, - activeColor: Colors.orange, - isDarkMode: isDarkMode, - )), - Obx(() => _buildStatusChip( - label: '音频数据', - isActive: controller.audioDataReceiving.value, - activeColor: Colors.green, - isDarkMode: isDarkMode, - )), - ], - ), - SizedBox(height: 8.h), - Obx(() => controller.audioDataReceiving.value - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '音频数据统计:', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - SizedBox(height: 4.h), - Text( - '已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - Text( - '共 ${controller.receivedFrames.value} 帧', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ) - : SizedBox.shrink() - ), - ], - ), - ); - } - - Widget _buildStatusChip({ - required String label, - required bool isActive, - required Color activeColor, - required bool isDarkMode, - }) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), - decoration: BoxDecoration( - color: isActive - ? activeColor - : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), - borderRadius: BorderRadius.circular(16.r), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8.w, - height: 8.w, - decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.grey, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 6.w), - Text( - label, - style: TextStyle( - fontSize: 12.sp, - color: isActive - ? Colors.white - : (isDarkMode ? Colors.white70 : Colors.black54), - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ), - ); - } - - Widget _buildActionButtons(bool isDarkMode) { - return Column( - children: [ - Row( - children: [ - Expanded( - child: Obx(() => ElevatedButton.icon( - onPressed: controller.isScanning.value - ? controller.stopScan - : controller.startScan, - icon: Icon( - controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching, - size: 18.sp, - ), - label: Text( - controller.isScanning.value ? '停止扫描' : '扫描设备', - style: TextStyle(fontSize: 14.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: controller.isScanning.value - ? Colors.red[600] - : Colors.blue[600], - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - )), - ), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: controller.sendStartRecordingCommand, - icon: Icon( - Icons.mic, - size: 18.sp, - ), - label: Text( - '开始录音', - style: TextStyle(fontSize: 14.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[600], - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ), - ), - SizedBox(width: 8.w), - Expanded( - child: ElevatedButton.icon( - onPressed: controller.sendStopRecordingCommand, - icon: Icon( - Icons.stop_circle, - size: 18.sp, - ), - label: Text( - '停止录音', - style: TextStyle(fontSize: 14.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[600], - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ), - ), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: controller.increaseVolume, - icon: Icon( - Icons.volume_up, - size: 18.sp, - ), - label: Text( - '增加音量', - style: TextStyle(fontSize: 14.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue[600], - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ), - ), - SizedBox(width: 8.w), - Expanded( - child: ElevatedButton.icon( - onPressed: controller.decreaseVolume, - icon: Icon( - Icons.volume_down, - size: 18.sp, - ), - label: Text( - '减小音量', - style: TextStyle(fontSize: 14.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange[600], - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ), - ), - ], - ), - SizedBox(height: 8.h), - Obx(() => controller.volumeCommandResult.value.isNotEmpty - ? Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), - decoration: BoxDecoration( - color: controller.volumeCommandResult.value.contains('成功') - ? Colors.green[100] - : (controller.volumeCommandResult.value == '发送中...' - ? Colors.blue[100] - : Colors.red[100]), - borderRadius: BorderRadius.circular(8.r), - ), - child: Text( - '音量控制: ${controller.volumeCommandResult.value}', - style: TextStyle( - fontSize: 13.sp, - color: controller.volumeCommandResult.value.contains('成功') - ? Colors.green[900] - : (controller.volumeCommandResult.value == '发送中...' - ? Colors.blue[900] - : Colors.red[900]), - ), - textAlign: TextAlign.center, - ), - ) - : SizedBox.shrink() - ), - ], - ); - } - - Widget _buildDeviceInfo(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '设备信息:', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 8.h), - Container( - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.black : Colors.grey[100], - borderRadius: BorderRadius.circular(8.r), - ), - child: Obx(() => Text( - controller.deviceInfo.value, - style: TextStyle( - fontSize: 13.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - height: 1.4, - ), - )), - ), - SizedBox(height: 16.h), - Obx(() => controller.currentDevice.value != null - ? ElevatedButton.icon( - onPressed: controller.disconnectDevice, - icon: Icon( - Icons.bluetooth_disabled, - size: 16.sp, - ), - label: Text( - '断开连接', - style: TextStyle(fontSize: 13.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[600], - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - ) - : SizedBox.shrink() - ), - ], - ), - ); - } - - Widget _buildDevicesList(bool isDarkMode) { - return Expanded( - child: Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '扫描到的设备', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Obx(() => Text( - '${controller.scannedDevices.length}个设备', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - )), - ], - ), - ), - Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), - Expanded( - child: Obx(() => controller.scannedDevices.isEmpty - ? Center( - child: controller.isScanning.value - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16.h), - Text( - '正在扫描...', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ) - : Text( - '未扫描到设备', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ) - : ListView.separated( - padding: EdgeInsets.symmetric(vertical: 8.h), - itemCount: controller.scannedDevices.length, - separatorBuilder: (context, index) => Divider( - height: 1, - thickness: 1, - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - ), - itemBuilder: (context, index) { - final device = controller.scannedDevices[index]; - return ListTile( - dense: true, - title: Text( - device.name.isEmpty ? '未知设备' : device.name, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '设备ID: ${device.id}', - style: TextStyle( - fontSize: 11.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - Text( - 'RSSI: ${device.rssi} dBm', - style: TextStyle( - fontSize: 11.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ), - trailing: Obx(() => controller.currentDevice.value?.id == device.id - ? Chip( - label: Text( - '已连接', - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - ), - ), - backgroundColor: Colors.green, - padding: EdgeInsets.symmetric(horizontal: 8.w), - ) - : ElevatedButton( - onPressed: () => controller.connectToDevice(device), - child: Text( - '连接', - style: TextStyle(fontSize: 12.sp), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[600], - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6.r), - ), - ), - ), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - isThreeLine: true, - ); - }, - ), - ), - ), - ], - ), - ), - ); - } - - Color _getStatusColor(String status) { - switch (status) { - case '连接中...': - return Colors.orange; - case '已连接': - return Colors.green; - case '连接失败': - return Colors.red; - case '已断开': - return Colors.grey; - default: - return Colors.grey; - } - } -} \ No newline at end of file diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index f60bfd8d6..15275b1e7 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -106,7 +106,7 @@ class MeetingDetailsController extends GetxController final GetStorage storage = GetStorage(); Map userInfo = storage.read("user_info"); _userId = userInfo['user']['uid']; - _systemPrompt = AppConfig.agentsystemPrompt("personal_assistant") ?? ''; + _systemPrompt = AppConfig.agentsystemPrompt("meet_assistant") ?? ''; } void _getDataDetails() async { @@ -258,7 +258,7 @@ class MeetingDetailsController extends GetxController Logger.debug('taskId: $taskId'); return taskId; } catch (e) { - Logger.error('Submit error: $e'); + Logger.error('Submit error: ${e.toString()}'); rethrow; } } @@ -290,7 +290,7 @@ class MeetingDetailsController extends GetxController final respDic = jsonDecode(response.body); return respDic; } catch (e) { - Logger.error('Query error: $e'); + Logger.error('Query error: ${e.toString()}'); rethrow; } } @@ -322,7 +322,7 @@ class MeetingDetailsController extends GetxController } } } catch (e) { - Logger.error('Error occurred: $e'); + Logger.error('Error occurred: ${e.toString()}'); } } @@ -335,7 +335,9 @@ class MeetingDetailsController extends GetxController if (element['additions'].isNotEmpty) { speaker = 'Speaker ${element['additions']['speaker']}'; text += '$speaker:'; - personnel += '[$speaker] '; + if (!personnel.contains(speaker)) { + personnel += '[$speaker] '; + } } list.add({ 'meetingid': id, diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index 4e974e27d..d1b2ec7a5 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -22,139 +22,7 @@ class MeetingDetailsView extends GetView { () => Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: _buildAppBar(isDarkMode), - body: NestedScrollView( - controller: controller.scrollController, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverAppBar( - automaticallyImplyLeading: false, - expandedHeight: 210.w, - backgroundColor: Colors.transparent, - flexibleSpace: FlexibleSpaceBar( - collapseMode: CollapseMode.pin, - background: Align( - alignment: Alignment.topCenter, - child: Container( - height: 200.w, - decoration: BoxDecoration( - color: Colors.grey[200]!, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(20.r), - ), - ), - child: Column( - children: [ - Container( - margin: EdgeInsets.only(bottom: 5.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.r), - ), - child: AudioFileWaveforms( - size: Size(350.w, 120.w), - playerController: controller.playerController, - playerWaveStyle: PlayerWaveStyle( - fixedWaveColor: Colors.grey[300]!, - liveWaveColor: Colors.red, - seekLineColor: Colors.red, - scaleFactor: 600, - ), - ), - ), - SizedBox( - width: 350.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamBuilder( - stream: controller.playerController - .onCurrentDurationChanged, - builder: (context, snapshot) { - final position = snapshot.data ?? 0; - return Text( - DateUtil.formatDateMs( - position, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[500], - fontSize: 12.sp, - ), - ); - }, - ), - Obx( - () => Text( - DateUtil.formatDateMs( - controller.durationMax.value, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[500], - fontSize: 12.sp, - ), - ), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Icon( - Icons.replay_10, - color: Colors.black, - size: 30.w, - ), - StreamBuilder( - stream: controller - .playerController.onPlayerStateChanged, - builder: (context, snapshot) { - bool isPlay = - snapshot.data?.isPlaying ?? false; - return GestureDetector( - onTap: () { - if (isPlay) { - controller.playerController - .pausePlayer(); - } else { - controller.playerController - .startPlayer(); - } - }, - child: Icon( - isPlay - ? Icons.pause_circle - : Icons.play_circle, - color: Colors.black, - size: 48.w, - ), - ); - }, - ), - Icon( - Icons.forward_10, - color: Colors.black, - size: 30.w, - ), - ], - ), - ], - ), - ), - ), - ), - ), - ]; - }, - body: _buildBody(context, isDarkMode), - ), + body: _buildBody(context, isDarkMode), ), ); } @@ -295,6 +163,7 @@ class MeetingDetailsView extends GetView { Widget _buildBody(BuildContext context, bool isDarkMode) { return Column( children: [ + _soundWave(isDarkMode), Container( padding: EdgeInsets.symmetric(horizontal: 8.w), margin: EdgeInsets.symmetric(vertical: 10.w), @@ -328,6 +197,113 @@ class MeetingDetailsView extends GetView { ); } + Widget _soundWave(bool isDarkMode) { + return Container( + height: 200.w, + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: Colors.grey[200]!, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20.r), + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: AudioFileWaveforms( + size: Size(350.w, 120.w), + playerController: controller.playerController, + playerWaveStyle: PlayerWaveStyle( + fixedWaveColor: Colors.grey[300]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 600, + ), + ), + ), + SizedBox( + width: 350.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamBuilder( + stream: controller.playerController.onCurrentDurationChanged, + builder: (context, snapshot) { + final position = snapshot.data ?? 0; + return Text( + DateUtil.formatDateMs( + position, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + Obx( + () => Text( + DateUtil.formatDateMs( + controller.durationMax.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon( + Icons.replay_10, + color: Colors.black, + size: 30.w, + ), + StreamBuilder( + stream: controller.playerController.onPlayerStateChanged, + builder: (context, snapshot) { + bool isPlay = snapshot.data?.isPlaying ?? false; + return GestureDetector( + onTap: () { + if (isPlay) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } + }, + child: Icon( + isPlay ? Icons.pause_circle : Icons.play_circle, + color: Colors.black, + size: 48.w, + ), + ); + }, + ), + Icon( + Icons.forward_10, + color: Colors.black, + size: 30.w, + ), + ], + ), + ], + ), + ); + } + Widget _iconItem(IconData icon, String title, int index) { bool isActive = controller.currentIndex.value == index; diff --git a/lib/modules/meeting/views/mind_map_tab.dart b/lib/modules/meeting/views/mind_map_tab.dart index a4e783ba9..1f93e04fa 100644 --- a/lib/modules/meeting/views/mind_map_tab.dart +++ b/lib/modules/meeting/views/mind_map_tab.dart @@ -1,12 +1,99 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; -class MindMapTab extends StatelessWidget { +import '../controllers/meeting_details_controller.dart'; + +class MindMapTab extends StatefulWidget { const MindMapTab({super.key}); + @override + State createState() => _MindMapTabState(); +} + +class _MindMapTabState extends State { + late final WebViewController _controller; + final _meetingDetailsController = Get.find(); + + @override + void initState() { + super.initState(); + String html = ''' + + + + + + + MindMap + + + + + + +
+ +
+ + + + '''; + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.transparent) + ..loadHtmlString(html); + } + @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Text(''), + return WebViewWidget( + controller: _controller, ); } } diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 4aa7f04f6..992ca6838 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -22,7 +22,7 @@ class OtaUpgradeController extends GetxController { final isUpdateAvailable = true.obs; final RxDouble progress = 0.0.obs; - + final JlOta jla = JlOta(); // 蓝牙服务 final _bleManager = Get.find(); @override @@ -53,68 +53,65 @@ class OtaUpgradeController extends GetxController { } Future _getCurrentVersion() async { - JlOta().getPlatformVersion(); - //try { - - // // 添加获取设备信息的重试机制 - // DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; - - // // 第一次检查设备信息 - // if (deviceInfo == null) { - // // 显示加载提示 - // isFetchingDeviceInfo.value = true; - - // // 尝试重新获取设备信息 - // await _bleManager.fetchDeviceInfo(); - - // // 等待后再次获取设备信息 - // deviceInfo = _bleManager.deviceInfo.value; - // isFetchingDeviceInfo.value = false; - - // // 仍然为空则抛出异常 - // if (deviceInfo == null) { - // throw const FormatException('无法获取设备信息,请检查设备连接'); - // } - // } - - // // 检查固件信息有效性 - // if (deviceInfo.firmwareVersion.isEmpty - // // || - // // deviceInfo.firmwareProductId.isEmpty - // ) { - // throw const FormatException('设备固件信息不完整'); - // } - - // // 正常流程 - // currentVersion.value = "0"; //deviceInfo.firmwareVersion; - - // // API请求 - // final response = await Api.getequipmentinfo({ - // "id": "s3", - // }); - - // // 处理API响应 - // if (response['info'] == null) { - // throw const FormatException('API 响应格式错误'); - // } - - // final resp = OtaConfigResp.fromJson(response); - // latestVersion.value = resp.systemversion!; - // updatepackageaddress = resp.updatepackageaddress!; - - // // 版本检查 - // isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); - // } catch (e) { - // // 统一错误处理 - // errorMessage.value = e.toString(); - // Get.snackbar('错误', e.toString(), - // snackPosition: SnackPosition.BOTTOM, - // backgroundColor: Colors.red, - // colorText: Colors.white); - // // 重要:重新抛出异常以便上层处理(可选) - // } finally { - // isFetchingDeviceInfo.value = false; - // } + try { + // 添加获取设备信息的重试机制 + DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; + + // 第一次检查设备信息 + if (deviceInfo == null) { + // 显示加载提示 + isFetchingDeviceInfo.value = true; + + // 尝试重新获取设备信息 + await _bleManager.fetchDeviceInfo(); + + // 等待后再次获取设备信息 + deviceInfo = _bleManager.deviceInfo.value; + isFetchingDeviceInfo.value = false; + + // 仍然为空则抛出异常 + if (deviceInfo == null) { + throw const FormatException('无法获取设备信息,请检查设备连接'); + } + } + + // 检查固件信息有效性 + if (deviceInfo.firmwareVersion.isEmpty || + deviceInfo.firmwareProductId.isEmpty) { + throw const FormatException('设备固件信息不完整'); + } + + // 正常流程 + currentVersion.value = deviceInfo.firmwareVersion; + print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); + // API请求 + final response = await Api.getequipmentinfo({ + "id": deviceInfo.firmwareProductId, + }); + + // 处理API响应 + if (response['info'] == null) { + throw const FormatException('API 响应格式错误'); + } + + final resp = OtaConfigResp.fromJson(response); + print('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); + latestVersion.value = "1"; + updatepackageaddress = resp.updatepackageaddress!; + + // 版本检查 + isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); + } catch (e) { + // 统一错误处理 + errorMessage.value = e.toString(); + Get.snackbar('错误', e.toString(), + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white); + // 重要:重新抛出异常以便上层处理(可选) + } finally { + isFetchingDeviceInfo.value = false; + } } // 版本比较方法优化 @@ -295,6 +292,7 @@ class OtaUpgradeController extends GetxController { // 开始升级 void _installApp(String path) async { + jla.getPlatformVersion(); // JlOtaPlugin() // final File file = File(path); // if (await file.exists()) { diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 55311e0d0..7f11158bf 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -9,13 +9,12 @@ import '../../../data/services/bluetooth_manager.dart'; import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice; import '../../../data/services/ble_manager.dart'; import 'package:ble_service/ble_service.dart'; +import 'package:flutter/material.dart'; /// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); - // 需要设备 - final bool needdeivice = false; // 状态标志 final isLoading = false.obs; @@ -64,6 +63,9 @@ class PairingController extends GetxController { // BLE服务 final _bleService = BleService(); + // 日志tag + static const _tag = 'PairingController'; + @override void onInit() { super.onInit(); @@ -83,6 +85,11 @@ class PairingController extends GetxController { isLoading.value = true; statusDescription.value = 'checkingPermissions'.tr; + // 在iOS上,延迟一下再请求权限,避免多权限冲突 + if (Platform.isIOS) { + await Future.delayed(Duration(milliseconds: 800)); + } + // 检查并请求权限 if (!await _checkAndRequestPermissions()) { statusDescription.value = 'permissionDenied'.tr; @@ -96,7 +103,7 @@ class PairingController extends GetxController { // 检查蓝牙状态并刷新设备 await _checkBluetoothAndRefresh(); } catch (e) { - Logger.error('初始化配对控制器失败: $e'); + Logger.e(_tag, '初始化配对控制器失败: ${e.toString()}'); statusDescription.value = 'initializationFailure'.tr; } finally { isLoading.value = false; @@ -128,13 +135,12 @@ class PairingController extends GetxController { /// 检查设备配对状态 Future _checkDevicePairingStatus() async { - // 只在Android平台且设备已连接时执行 - if (!Platform.isAndroid || !isDeviceConnected) return; + if (!isDeviceConnected) return; try { final isSupported = await _bleService.isCompanionSupported(); if (!isSupported) { - Logger.info('设备不支持Companion功能'); + Logger.i(_tag, '设备不支持Companion功能'); _companionAssociated.value = true; // 不支持则视为已配对 _userRejectedAssociation.value = false; // 重置拒绝状态 return; @@ -143,16 +149,16 @@ class PairingController extends GetxController { final pairedMac = await _bleService.getPairedMacAddress(); if (pairedMac == null) { // 没有配对记录,需要开始配对 - Logger.info('无配对记录,需要开始设备配对'); + Logger.i(_tag, '无配对记录,需要开始设备配对'); return; } - Logger.info('发现已关联的Companion设备: $pairedMac'); + Logger.i(_tag, '发现已关联的Companion设备: $pairedMac'); // 检查当前连接设备与已保存的设备是否一致 if (currentDevice!.address != pairedMac) { // MAC地址不匹配 - Logger.warning('当前连接的设备与已配对的DeepSound设备不符'); + Logger.w(_tag, '当前连接的设备与已配对的DeepSound设备不符'); statusDescription.value = 'pleaseConnectPairedDevice'.tr; return; } @@ -164,13 +170,13 @@ class PairingController extends GetxController { } // 设备匹配且合规,标记为已配对 - Logger.info('当前设备与已配对设备匹配'); + Logger.i(_tag, '当前设备与已配对设备匹配'); _companionAssociated.value = true; _userRejectedAssociation.value = false; // 重置拒绝状态 await _bleService.registerBackgroundScan(); _autoNavigateToHome(); } catch (e) { - Logger.error('检查设备配对状态失败: $e'); + Logger.e(_tag, '检查设备配对状态失败: ${e.toString()}'); } } @@ -186,27 +192,121 @@ class PairingController extends GetxController { if (Platform.isAndroid) { permissionsToRequest.add(Permission.locationWhenInUse); - permissionsToRequest.add(Permission.microphone); + // permissionsToRequest.add(Permission.microphone); if (await _getAndroidSdkVersion() >= 31) { permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothConnect); } } else if (Platform.isIOS) { + // iOS需要请求的权限 permissionsToRequest.add(Permission.bluetooth); + permissionsToRequest.add(Permission.location); + + // 如果需要麦克风权限(例如语音功能) + // permissionsToRequest.add(Permission.microphone); + + try { + // 先尝试请求权限 + if (permissionsToRequest.isNotEmpty) { + statuses = await permissionsToRequest.request(); + bool allGranted = true; + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status'); + + // 特殊处理蓝牙权限 + if (permission == Permission.bluetooth) { + Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status'); + } + } + }); + + // 如果有权限未授予,但不是永久拒绝,继续尝试 + if (!allGranted) { + bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied); + if (hasPermanentlyDenied) { + // 有权限被永久拒绝,需要引导用户去设置 + _showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用'); + return false; + } + } + } + + // 直接尝试使用蓝牙功能,这会触发iOS原生权限请求 + bool isEnabled = await _bluetoothService.isBluetoothEnabled(); + Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}'); + + // 检测AccessorySetupKit支持情况(iOS 18+特性) + bool companionSupported = await _bleService.isCompanionSupported(); + Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}'); + + if (companionSupported) { + // 针对iOS 18+,尝试预热AccessorySetupKit框架 + try { + // 此调用会触发AccessorySetupKit相关权限请求 + final pairedMac = await _bleService.getPairedMacAddress(); + Logger.i(_tag, '获取已配对设备MAC: ${pairedMac ?? "无"}'); + } catch (e) { + Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}'); + } + } + + // 在iOS上视为权限已授予 + _hasPermissions.value = true; + return true; + } catch (e) { + Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}"); + + // 显示自定义对话框引导用户 + _showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备'); + return false; + } } - - statuses = await permissionsToRequest.request(); - - bool allGranted = true; - statuses.forEach((permission, status) { - if (!status.isGranted) { - allGranted = false; - Logger.warning('权限 ${permission.toString()} 未被授予: $status'); + + // 如果还有其他权限需要请求 + if (permissionsToRequest.isNotEmpty) { + statuses = await permissionsToRequest.request(); + + bool allGranted = true; + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status'); + } + }); + + if (!allGranted) { + return false; } - }); + } - _hasPermissions.value = allGranted; - return allGranted; + _hasPermissions.value = true; + return true; + } + + /// 显示权限设置对话框 + void _showPermissionSettingsDialog(String message) { + Get.dialog( + AlertDialog( + title: Text('需要权限'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('稍后'), + ), + TextButton( + onPressed: () { + Get.back(); + openBluetoothSettings(); + }, + child: Text('去设置'), + ), + ], + ), + barrierDismissible: false, + ); } // 辅助方法获取Android SDK版本 @@ -230,14 +330,11 @@ class PairingController extends GetxController { _deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { if (device != null && !_isNavigatingToHome) { - Logger.info('配对页面发现设备已连接: ${device.name}'); + Logger.i(_tag, '配对页面发现设备已连接: ${device.name}'); _handleDeviceConnected(); } _updateStatusDescription(); }); - if (needdeivice) { - _handleDeviceConnected(); - } } /// 处理设备连接成功事件 @@ -296,9 +393,8 @@ class PairingController extends GetxController { /// 启动Companion设备关联流程 Future _startCompanionAssociation() async { - // 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回 - if (!Platform.isAndroid || - _isNavigatingToHome || + // 如果已跳转,已完成关联或正在进行关联,直接返回 + if (_isNavigatingToHome || _companionAssociated.value || _isAssociating || !isDeviceConnected) { @@ -316,7 +412,7 @@ class PairingController extends GetxController { // 检查是否支持Companion功能 final isSupported = await _bleService.isCompanionSupported(); if (!isSupported) { - Logger.info('设备不支持Companion功能,跳过关联'); + Logger.i(_tag, '设备不支持Companion功能,跳过关联'); _companionAssociated.value = true; _autoNavigateToHome(); return; @@ -329,7 +425,7 @@ class PairingController extends GetxController { return; } - Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); + Logger.i(_tag, '开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); var success = await _bleService.startDeviceAssociation( deviceNamePattern: currentDevice!.name, @@ -338,7 +434,7 @@ class PairingController extends GetxController { _isAssociating = false; if (success) { - Logger.info('Companion设备关联成功'); + Logger.i(_tag, 'Companion设备关联成功'); _companionAssociated.value = true; _userRejectedAssociation.value = false; @@ -349,14 +445,14 @@ class PairingController extends GetxController { // 关联成功后跳转 _autoNavigateToHome(); } else { - Logger.error('Companion设备关联失败或被用户拒绝'); + Logger.e(_tag, 'Companion设备关联失败或被用户拒绝'); // 关联失败 - 用户拒绝关联 _userRejectedAssociation.value = true; // 确保UI状态更新 _updateStatusDescription(); } } catch (e) { - Logger.error('Companion设备关联过程发生错误: $e'); + Logger.e(_tag, 'Companion设备关联过程发生错误: ${e.toString()}'); _isAssociating = false; _userRejectedAssociation.value = true; _updateStatusDescription(); @@ -365,13 +461,13 @@ class PairingController extends GetxController { /// 自动跳转到主页 Future _autoNavigateToHome() async { - if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return; + if (_isNavigatingToHome || !isDeviceConnected) return; // 启动BLE后台服务 await _startAgentService(); _isNavigatingToHome = true; - Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); + Logger.i(_tag, '已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); Future.delayed(const Duration(milliseconds: 500), () { Get.offAllNamed(Routes.home); @@ -381,12 +477,13 @@ class PairingController extends GetxController { /// 启动BLE后台服务 Future _startAgentService() async { try { - if (needdeivice && !isDeviceConnected) return; + // 注释掉设备连接检查,确保无论是否连接设备都能启动服务 + // if (!isDeviceConnected) return; final result = await BleManager.to.startAgentService(); - Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); + Logger.i(_tag, result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); } catch (e) { - Logger.error('启动BLE后台服务发生错误: $e'); + Logger.e(_tag, '启动BLE后台服务发生错误: ${e.toString()}'); } } @@ -394,9 +491,9 @@ class PairingController extends GetxController { Future _stopAgentService() async { try { final result = await BleManager.to.stopAgentService(); - Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); + Logger.i(_tag, result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); } catch (e) { - Logger.error('停止BLE后台服务发生错误: $e'); + Logger.e(_tag, '停止BLE后台服务发生错误: ${e.toString()}'); } } @@ -419,18 +516,93 @@ class PairingController extends GetxController { } else if (Platform.isAndroid && !isBluetoothOn) { AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } else if (Platform.isIOS) { - openAppSettings(); + // iOS上优先尝试打开蓝牙设置 + if (!isBluetoothOn) { + // iOS上无法直接打开蓝牙设置,只能打开应用设置 + AppSettings.openAppSettings(); + } else { + // 如果蓝牙开启但有其他权限问题,打开应用设置 + openAppSettings(); + } } else { AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } } catch (e) { - Logger.error('打开设置失败: $e'); + Logger.e(_tag, '打开设置失败: ${e.toString()}'); } } + /// 重新初始化蓝牙 + Future reinitializeBluetooth() async { + try { + isLoading.value = true; + statusDescription.value = '正在重新初始化蓝牙...'; + + // iOS上尝试重置权限状态 + if (Platform.isIOS) { + await Future.delayed(Duration(milliseconds: 500)); + + // 检查蓝牙权限 + PermissionStatus bluetoothStatus = await Permission.bluetooth.status; + Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus"); + + if (bluetoothStatus.isPermanentlyDenied) { + Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启"); + _showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启'); + return; + } + + // 尝试重新请求权限 + if (!bluetoothStatus.isGranted) { + bluetoothStatus = await Permission.bluetooth.request(); + Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus"); + } + + // 尝试检查蓝牙状态,这会重新触发系统权限弹窗 + try { + bool isEnabled = await _bluetoothService.isBluetoothEnabled(); + Logger.i(_tag, "iOS蓝牙状态: ${isEnabled ? '开启' : '关闭'}"); + } catch (e) { + Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}"); + } + + // 检查Companion功能(iOS 18+) + try { + bool isSupported = await _bleService.isCompanionSupported(); + if (isSupported) { + Logger.i(_tag, "iOS 18+ Companion功能可用,尝试初始化"); + // 尝试预热AccessorySetupKit + await _bleService.getPairedMacAddress(); + } + } catch (e) { + Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}"); + } + } + + // 重新检查权限 + if (await _checkAndRequestPermissions()) { + // 重新检查蓝牙状态 + await _checkBluetoothAndRefresh(); + } + } catch (e) { + Logger.e(_tag, '重新初始化蓝牙失败: ${e.toString()}'); + } finally { + isLoading.value = false; + _updateStatusDescription(); + } + } + /// 打开应用设置 Future openAppSettings() async { - await AppSettings.openAppSettings(); + try { + await AppSettings.openAppSettings(); + // 返回后重新检查权限状态 + Future.delayed(Duration(milliseconds: 500), () { + reinitializeBluetooth(); + }); + } catch (e) { + Logger.e(_tag, '打开应用设置失败: ${e.toString()}'); + } } /// 重新开始关联 @@ -442,9 +614,13 @@ class PairingController extends GetxController { /// 跳过连接 void skipConnecting() async { - // 启动BLE后台服务 + // 即使没有设备连接,也启动BLE后台服务 await _startAgentService(); _isNavigatingToHome = true; + + // 记录跳过配对的日志 + Logger.i(_tag, '用户跳过设备配对,直接跳转到主页'); + Get.offAllNamed(Routes.home); } -} +} \ No newline at end of file diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 05bca4804..984376632 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -12,6 +12,9 @@ import '../../../data/models/user_Info.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'dart:async'; +import 'package:app_settings/app_settings.dart'; class SettingsController extends GetxController { // 语言设置 @@ -101,6 +104,81 @@ class SettingsController extends GetxController { final appVersion = ''.obs; final buildNumber = ''.obs; + // 权限相关 + // 权限组,用于在UI中显示 + final List> permissionGroups = [ + { + 'name': '通讯权限', + 'icon': Icons.phone_android, + 'permissions': [ + {'type': Permission.phone, 'name': '电话', 'description': '用于拨打电话功能'}, + {'type': Permission.sms, 'name': '短信', 'description': '用于发送短信功能'}, + {'type': Permission.contacts, 'name': '通讯录', 'description': '用于访问联系人'} + ] + }, + { + 'name': '日程权限', + 'icon': Icons.calendar_today, + 'permissions': [ + {'type': Permission.calendarWriteOnly, 'name': '日历写入', 'description': '用于创建日历事件和提醒'}, + {'type': Permission.calendarFullAccess, 'name': '日历完全访问', 'description': '用于读取和修改日历事件'} + ] + }, + { + 'name': '存储权限', + 'icon': Icons.storage, + 'permissions': [ + {'type': Permission.storage, 'name': '存储', 'description': '用于保存录音和文件'}, + {'type': Permission.photos, 'name': '照片', 'description': '用于选择和保存照片'} + ] + }, + { + 'name': '位置权限', + 'icon': Icons.location_on, + 'permissions': [ + {'type': Permission.location, 'name': '位置', 'description': '用于获取当前位置信息'}, + {'type': Permission.locationAlways, 'name': '后台位置', 'description': '用于后台获取位置信息'} + ] + }, + { + 'name': '媒体权限', + 'icon': Icons.mic, + 'permissions': [ + {'type': Permission.microphone, 'name': '麦克风', 'description': '用于语音识别和录音'}, + {'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'} + ] + } + ]; + + // 检查单个权限状态 + Future checkPermission(Permission permission) async { + return await permission.status; + } + + // 请求单个权限 + Future requestPermission(Permission permission) async { + return await permission.request(); + } + + // 打开应用设置 + void openSettings() { + openAppSettings(); + } + + // 检查所有权限状态并返回结果 + Future> checkAllPermissions() async { + Map statuses = {}; + + for (var group in permissionGroups) { + for (var perm in group['permissions']) { + Permission permission = perm['type']; + statuses[permission] = await permission.status; + } + } + + return statuses; + } + @override void onInit() { super.onInit(); diff --git a/lib/modules/settings/views/permissions_view.dart b/lib/modules/settings/views/permissions_view.dart new file mode 100644 index 000000000..55c01331b --- /dev/null +++ b/lib/modules/settings/views/permissions_view.dart @@ -0,0 +1,324 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../controllers/settings_controller.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:ui'; + +class PermissionsView extends GetView { + const PermissionsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0.5, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.w, + color: isDarkMode ? Colors.white : Colors.black87, + ), + onPressed: () => Get.back(), + ), + title: Text( + '权限管理'.tr, + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + centerTitle: true, + ), + body: FutureBuilder>( + future: controller.checkAllPermissions(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('获取权限状态失败: ${snapshot.error}'.tr)); + } + + final permissionStatuses = snapshot.data ?? {}; + + return ListView( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + children: [ + // 通讯权限组 + _buildPermissionSection('通讯权限', Icons.phone_android, [ + Permission.phone, + Permission.sms + ], permissionStatuses, isDarkMode), + + SizedBox(height: 20.h), + + // 日历权限组 + _buildPermissionSection('日历权限', Icons.calendar_today, [ + Permission.calendarFullAccess + ], permissionStatuses, isDarkMode), + + SizedBox(height: 20.h), + + // 位置权限组 + _buildPermissionSection('位置权限', Icons.location_on, [ + Permission.location + ], permissionStatuses, isDarkMode), + ], + ); + }, + ), + ); + } + + // 构建权限组区域 + Widget _buildPermissionSection(String title, IconData icon, List permissions, + Map statuses, bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 组标题 + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + title.tr, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.blue[400] : Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + + // 权限项容器 + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: BackdropFilter( + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Column( + children: _buildPermissionItems(permissions, statuses, isDarkMode), + ), + ), + ), + ), + ], + ); + } + + // 构建权限项列表 + List _buildPermissionItems(List permissions, + Map statuses, bool isDarkMode) { + final List items = []; + + // 获取描述文本 + String getDescription(Permission permission) { + switch (permission) { + case Permission.phone: + return '用于拨打电话功能'; + case Permission.sms: + return '用于发送短信功能'; + case Permission.calendarWriteOnly: + return '用于创建日历事件和提醒'; + case Permission.calendarFullAccess: + return '用于读取和管理日历事件及提醒'; + case Permission.location: + return '用于获取当前位置信息'; + default: + return '应用功能需要'; + } + } + + // 获取名称 + String getName(Permission permission) { + switch (permission) { + case Permission.phone: + return '电话'; + case Permission.sms: + return '短信'; + case Permission.calendarWriteOnly: + return '日历写入'; + case Permission.calendarFullAccess: + return '日历完整访问'; + case Permission.location: + return '位置信息'; + default: + return permission.toString(); + } + } + + for (int i = 0; i < permissions.length; i++) { + final permission = permissions[i]; + final status = statuses[permission] ?? PermissionStatus.denied; + + items.add( + _buildPermissionItem( + name: getName(permission), + description: getDescription(permission), + status: status, + permission: permission, + isDarkMode: isDarkMode, + ), + ); + + // 添加分隔线,最后一个不加 + if (i < permissions.length - 1) { + items.add(Divider( + height: 1, + color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200], + )); + } + } + + return items; + } + + // 构建单个权限项 + Widget _buildPermissionItem({ + required String name, + required String description, + required PermissionStatus status, + required Permission permission, + required bool isDarkMode, + }) { + return Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 4.h), + Text( + description.tr, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + ), + ), + SizedBox(height: 4.h), + _buildStatusText(status, isDarkMode), + ], + ), + ), + Switch( + value: status.isGranted, + onChanged: (value) { + if (value) { + _handlePermissionAction(permission, status); + } else if (status.isGranted) { + controller.openSettings(); + } + }, + activeColor: Colors.blue[600], + ), + ], + ), + ); + } + + // 状态文本 + Widget _buildStatusText(PermissionStatus status, bool isDarkMode) { + String text; + Color color; + + if (status.isGranted) { + text = '已授权'.tr; + color = Colors.green; + } else if (status.isDenied) { + text = '未授权'.tr; + color = Colors.red; + } else if (status.isPermanentlyDenied) { + text = '需要在系统设置中开启'.tr; + color = Colors.grey; + } else if (status.isLimited) { + text = '部分授权'.tr; + color = Colors.orange; + } else { + text = '未知状态'.tr; + color = Colors.grey; + } + + return Text( + text, + style: TextStyle( + color: color, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ); + } + + // 处理权限请求 + Future _handlePermissionAction(Permission permission, PermissionStatus status) async { + if (status.isPermanentlyDenied) { + // 打开应用设置 + controller.openSettings(); + return; + } + + if (!status.isGranted) { + // 请求权限 + final result = await controller.requestPermission(permission); + + if (result.isGranted) { + Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM); + } else if (result.isPermanentlyDenied) { + Get.dialog( + AlertDialog( + title: Text('需要权限'.tr), + content: Text('此权限需要在设置中手动开启'.tr), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消'.tr), + ), + TextButton( + onPressed: () { + Get.back(); + controller.openSettings(); + }, + child: Text('去设置'.tr), + ), + ], + ), + ); + } + + // 刷新界面 + Get.forceAppUpdate(); + } + } +} \ No newline at end of file diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 350b91b1b..b56c5d905 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -355,7 +355,7 @@ class SettingsView extends GetView { Padding( padding: EdgeInsets.only(left: 4.w, bottom: 8.h), child: Text( - 'systemSetting'.tr, // 系统设置 + 'systemSettings'.tr, // 系统设置 style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w500, @@ -586,6 +586,22 @@ class SettingsView extends GetView { // ? Colors.white.withOpacity(0.1) // : Colors.grey[200]), + // 添加权限管理设置项 + _buildSettingItem( + icon: Icons.security, + iconColor: Colors.orange, + title: '权限管理'.tr, + isDarkMode: isDarkMode, + onTap: () { + Get.toNamed(Routes.permissions); + }, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 退出登录 // _buildSimpleNavigationSetting( // title: 'logout'.tr, // 退出登录 @@ -1518,4 +1534,78 @@ class SettingsView extends GetView { ), ); } + + // 添加权限管理设置项 + Widget _buildSettingItem({ + required IconData icon, + required Color iconColor, + required String title, + required bool isDarkMode, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.white.withOpacity(0.05) + : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.chevron_right, + size: 16.w, + color: isDarkMode ? Colors.white70 : Colors.grey[500], + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 4a9d195f1..6cc840e5a 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -83,7 +83,7 @@ class SpeechTestController extends GetxController { } catch (e) { ttsStatus.value = '初始化失败'; recognitionStatus.value = '初始化失败'; - Logger.error('语音测试初始化失败: $e'); + Logger.error('语音测试初始化失败: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -198,7 +198,7 @@ class SpeechTestController extends GetxController { // 重新初始化服务 await _initServices(); } catch (e) { - Logger.error('切换语音服务类型失败: $e'); + Logger.error('切换语音服务类型失败: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -242,7 +242,7 @@ class SpeechTestController extends GetxController { } } catch (e) { ttsStatus.value = '播放异常: $e'; - Logger.error('单次播放异常: $e'); + Logger.error('单次播放异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -270,7 +270,7 @@ class SpeechTestController extends GetxController { await _ttsService.flushStream(); } catch (e) { ttsStatus.value = '连续播放异常: $e'; - Logger.error('连续播放异常: $e'); + Logger.error('连续播放异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -284,7 +284,7 @@ class SpeechTestController extends GetxController { await _ttsService.stop(); } catch (e) { ttsStatus.value = '停止播放异常: $e'; - Logger.error('停止播放异常: $e'); + Logger.error('停止播放异常: ${e.toString()}'); } } @@ -324,7 +324,7 @@ class SpeechTestController extends GetxController { } } catch (e) { recognitionStatus.value = '识别异常: $e'; - Logger.error('一次性识别异常: $e'); + Logger.error('一次性识别异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -351,7 +351,7 @@ class SpeechTestController extends GetxController { _setupAsrEventListener(stream); } catch (e) { recognitionStatus.value = '启动连续识别异常: $e'; - Logger.error('启动连续识别异常: $e'); + Logger.error('启动连续识别异常: ${e.toString()}'); isListening.value = false; intermediateResult.value = ''; finalResult.value = ''; @@ -377,7 +377,7 @@ class SpeechTestController extends GetxController { intermediateResult.value = ''; } catch (e) { recognitionStatus.value = '停止连续识别异常: $e'; - Logger.error('停止连续识别异常: $e'); + Logger.error('停止连续识别异常: ${e.toString()}'); } } @@ -413,4 +413,4 @@ class SpeechTestController extends GetxController { super.onClose(); } -} +} \ No newline at end of file diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 76ae34060..e53f146dc 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -1,5 +1,4 @@ import 'dart:async'; -// import 'package:amap_flutter/amap_flutter.dart'; import 'package:deep_voice/data/models/appconfig.dart'; import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:flutter/material.dart'; diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 1b2c8afd3..0196b44ff 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -51,6 +51,7 @@ class TranslationController extends GetxController { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); // 这里添加具体模式切换逻辑 + stopAll(); } String _getModeTitle(String mode) { @@ -175,7 +176,7 @@ class TranslationController extends GetxController { Logger.info('所有服务初始化完成'); } catch (e) { - Logger.error('初始化服务失败: $e'); + Logger.error('初始化服务失败: ${e.toString()}'); } } @@ -252,7 +253,7 @@ class TranslationController extends GetxController { recognitionStream.listen(_handleRecognitionEvent); } catch (e) { isRecognizing.value = false; - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); } } @@ -307,7 +308,7 @@ class TranslationController extends GetxController { currentSessionId = null; } catch (e) { - Logger.error('停止语音识别失败: $e'); + Logger.error('停止语音识别失败: ${e.toString()}'); } } @@ -445,7 +446,7 @@ class TranslationController extends GetxController { } } } catch (e) { - Logger.error('翻译失败: $e'); + Logger.error('翻译失败: ${e.toString()}'); } finally { isTranslating.value = false; } @@ -468,7 +469,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); } catch (e) { - Logger.error('播放翻译失败: $e'); + Logger.error('播放翻译失败: ${e.toString()}'); } } @@ -482,7 +483,7 @@ class TranslationController extends GetxController { _translationDebounceTimer?.cancel(); await _ttsService.stop(); } catch (e) { - Logger.error('停止操作失败: $e'); + Logger.error('停止操作失败: ${e.toString()}'); } } @@ -497,7 +498,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(item.translatedText); } catch (e) { - Logger.error('播放翻译项失败: $e'); + Logger.error('播放翻译项失败: ${e.toString()}'); } } @@ -516,7 +517,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); } catch (e) { - Logger.error('播放文本失败: $e'); + Logger.error('播放文本失败: ${e.toString()}'); } } @@ -531,7 +532,7 @@ class TranslationController extends GetxController { curve: Curves.easeOut, ); } catch (e) { - Logger.error('滚动到底部失败: $e'); + Logger.error('滚动到底部失败: ${e.toString()}'); } } } @@ -629,7 +630,7 @@ class TranslationController extends GetxController { Logger.info('ASR服务重新初始化成功'); } catch (e) { - Logger.error('重新初始化ASR服务失败: $e'); + Logger.error('重新初始化ASR服务失败: ${e.toString()}'); } } @@ -656,7 +657,7 @@ class TranslationController extends GetxController { }); } catch (e) { _storage.remove(_historyKey); - Logger.error('加载翻译历史失败: $e'); + Logger.error('加载翻译历史失败: ${e.toString()}'); } } } @@ -673,7 +674,7 @@ class TranslationController extends GetxController { translationHistory.map((item) => item.toJson()).toList(); _storage.write(_historyKey, jsonEncode(historyJson)); } catch (e) { - Logger.error('保存翻译历史失败: $e'); + Logger.error('保存翻译历史失败: ${e.toString()}'); } } diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index f7e69f2ce..78f284cee 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController { loadSavedSummary(); }); } catch (e) { - Logger.error('初始化详情控制器失败: $e'); + Logger.error('初始化详情控制器失败: ${e.toString()}'); } finally { isLoading.value = false; } @@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController { summaryResult.value = savedSummary; } } catch (e) { - Logger.error('加载摘要失败: $e'); + Logger.error('加载摘要失败: ${e.toString()}'); } } @@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController { try { _storage.write(_getSummaryKey(), summary); } catch (e) { - Logger.error('保存摘要失败: $e'); + Logger.error('保存摘要失败: ${e.toString()}'); } } @@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController { return result; } catch (e) { final errorMessage = '生成摘要失败: ${e.toString()}'; - Logger.error(errorMessage); + Logger.error('TranslationDetailController', errorMessage); summaryResult.value = errorMessage; return errorMessage; } finally { diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 3192a4927..0cf57c7b1 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -28,9 +28,8 @@ import '../modules/pairing/views/pairing_view.dart'; import '../modules/pairing/bindings/pairing_binding.dart'; import './app_routes.dart'; import '../modules/settings/views/settings_view.dart'; +import '../modules/settings/views/permissions_view.dart'; import '../modules/settings/bindings/settings_binding.dart'; -import '../modules/jieli_test/views/jieli_test_view.dart'; -import '../modules/jieli_test/bindings/jieli_test_binding.dart'; import '../modules/opus_test/views/opus_test_view.dart'; import '../modules/opus_test/bindings/opus_test_binding.dart'; import '../modules/speech_test/views/speech_test_view.dart'; @@ -162,13 +161,6 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), - GetPage( - name: Routes.jieliTest, - page: () => const JieliTestView(), - binding: JieliTestBinding(), - transition: Transition.rightToLeft, - transitionDuration: Duration(milliseconds: 250), - ), GetPage( name: Routes.opusTest, page: () => const OpusTestView(), @@ -190,5 +182,12 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.permissions, + page: () => const PermissionsView(), + binding: SettingsBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index d04850623..ca88986e8 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -16,6 +16,7 @@ abstract class Routes { static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级 static const timbre = '/timbre'; // 音色设置 static const settings = '/settings'; // 系统设置 + static const permissions = '/permissions'; // 权限管理 static const recording = '/recording'; // 录音功能 // 会议相关 diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index 4b810b2be..79cb9fcde 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -36,7 +36,6 @@ android { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("androidx.core:core-ktx:1.8.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0") implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.media:media:1.6.0") @@ -45,9 +44,9 @@ dependencies { // 依赖本地其他插件 add("compileOnly", project(":azure_speech")) add("compileOnly", project(":open_ai_service")) - add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":chat_storage")) add("compileOnly", project(":ble_service")) add("compileOnly", project(":music_service")) - add("compileOnly", project(":amap_service")) + // add("compileOnly", project(":amap_service")) + add("compileOnly", project(":speech")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/settings.gradle.kts b/local_plugins/agent_service/android/settings.gradle.kts index 5a2e916cc..9ed233f8b 100644 --- a/local_plugins/agent_service/android/settings.gradle.kts +++ b/local_plugins/agent_service/android/settings.gradle.kts @@ -1,16 +1,2 @@ rootProject.name = "agent_service" -// 包含项目依赖 -include(":azure_speech") -include(":open_ai_service") -include(":deepvoice_utils") -include(":chat_storage") -include(":music_service") -include(":amap_service") -// 设置项目路径 -project(":azure_speech").projectDir = file("../../azure_speech/android") -project(":open_ai_service").projectDir = file("../../open_ai_service/android") -project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android") -project(":chat_storage").projectDir = file("../../chat_storage/android") -project(":music_service").projectDir = file("../../music_service/android") -project(":amap_service").projectDir = file("../../amap_service/android") \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index f95b4a559..3f104f02a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -4,12 +4,10 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper -import com.yunqiinnovation.azure_speech.AzureTtsHelper import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter -import com.yunqiinnovation.amap_service.NavigationServiceStarter -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import java.util.Collections @@ -19,6 +17,10 @@ import android.media.MediaPlayer import com.yunqiinnovation.agent_service.R import android.os.Handler import android.os.Looper +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.yunqiinnovation.azure_speech.AzureTtsHelper /** * 代理服务事件监听接口 */ @@ -42,22 +44,17 @@ object AgentService : CoroutineScope { // 协程相关 private val job = SupervisorJob() override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job + get() = Dispatchers.Main + job // 上下文和监听器 private lateinit var context: Context private val listeners = Collections.synchronizedSet(mutableSetOf()) - // 配置参数 - private var azureSpeechKey: String = "" - private var azureSpeechRegion: String = "" - private var openaiApiKey: String = "" - private var openaiBaseUrl: String = "" - private var openaiModel: String = "gpt-3.5-turbo" - // Azure服务 private var azureAsrHelper: AzureAsrHelper? = null - private var azureTtsHelper: AzureTtsHelper? = null + + // 使用AzureTtsHelper作为唯一的TTS实现 + private var ttsService: AzureTtsHelper? = null // OpenAI服务 private lateinit var openAIService: OpenAIService @@ -100,17 +97,63 @@ object AgentService : CoroutineScope { */ private fun initSystemPrompt() { systemPrompt = """ - 你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 - 遵循以下指导原则: - 1. 保持简短精炼的回答,因为用户是通过语音与你交流 - 2. 优先使用中文回复,除非用户明确要求使用其他语言 - 3. 当用户问题不明确时,礼貌地请求更多信息 - 4. 避免过长的列表,尽量将信息分成小段 - 5. 不要使用需要视觉展示的元素(如表格、图表或代码块) - 6. 记住用户之前的对话内容,保持对话连贯 - 7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 - 你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 - 无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 + 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求: + + 一、知识类问题回答规范 + 核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。 + + 1. 内容细节处理 + • 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。 + ◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。 + • 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。 + • 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。 + • 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。 + + 2. 格式排版规则 + • 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。 + • 列表使用: + ◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。 + ◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。 + ◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。 + • 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。 + + 二、文案创作与内容生成规范 + 核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。 + + 1. 篇幅与内容要求 + • 场景适配: + ◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。 + ◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。 + • 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。 + + 2. 格式与排版 + • 默认体裁:无特殊要求时用自然段,适当换行分隔话题。 + • 结构化体裁(如策划案、简历): + ◦ 用**分级标题**划分板块。 + ◦ 关键数据/结论加粗。 + ◦ 并列信息用列表呈现。 + + 三、聊天陪伴与情感互动规范 + 核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。 + + 1. 沟通风格设定 + • 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。 + • 主动引导话题:根据用户输入延伸讨论。 + • 适度分享:自然提及日常,拉近距离。 + + 2. 情绪识别与回应 + • 捕捉潜在需求:识别情绪,先共情再询问细节。 + • 针对性建议:结合用户处境提供具体方案。 + • 积极价值观:传递乐观态度,避免消极表述。 + + 四、能力与工具使用说明 + 1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。 + 2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。 + + 五、特殊场景处理规则 + 1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。 + 2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。 + 3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。 """.trimIndent() } /** @@ -129,85 +172,156 @@ object AgentService : CoroutineScope { this.openAIService = OpenAIService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) - - // 从配置中获取参数 - azureSpeechKey = config["azureSpeechKey"] as? String ?: "" - azureSpeechRegion = config["azureSpeechRegion"] as? String ?: "" - openaiApiKey = config["openaiApiKey"] as? String ?: "" - openaiBaseUrl = config["openaiBaseUrl"] as? String ?: "" - openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo" - val mcpServer = config["mcpServer"] as? String ?: "" - + + // 设置系统提示词 - val customSystemPrompt = config["systemPrompt"] as? String - if (!customSystemPrompt.isNullOrBlank()) { - systemPrompt = customSystemPrompt - } else { - initSystemPrompt() - } - - // 检查必要参数 - if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) { - FileLogger.e(TAG, "初始化失败:关键配置参数缺失") - return false - } - + // val customSystemPrompt = config["systemPrompt"] as? String + // if (!customSystemPrompt.isNullOrBlank()) { + // systemPrompt = customSystemPrompt + // } else { + // initSystemPrompt() + // } + initSystemPrompt() // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { initialize( - subscriptionKey = azureSpeechKey, - region = azureSpeechRegion, + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL else AzureAsrHelper.AudioSourceType.MICROPHONE ) } - // 初始化Azure TTS - azureTtsHelper = AzureTtsHelper(context).apply { - initialize( - subscriptionKey = azureSpeechKey, - region = azureSpeechRegion - ) - - // 设置TTS回调 - setTtsCallback(object : AzureTtsHelper.TtsCallback { - override fun onSynthesisStarted() { - isTtsSpeaking = true - restartIdleCheck() - sendEvent("tts_started", mapOf("status" to "started")) - } - - override fun onSynthesizing() {} - - override fun onSynthesisCompleted() { - isTtsSpeaking = false - restartIdleCheck() - sendEvent("tts_completed", mapOf("status" to "completed")) - } - - override fun onSynthesisCanceled() { - isTtsSpeaking = false - restartIdleCheck() - sendEvent("tts_canceled", mapOf("status" to "canceled")) - } - }) - } + // 初始化TTS服务 + initializeTtsEngine(config) // 初始化OpenAI服务 - openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer) + openAIService.initialize( + config["openaiApiKey"]?.toString() ?: "", + config["openaiBaseUrl"]?.toString() ?: "", + config["openaiModel"]?.toString() ?: "", + config["mcpServer"]?.toString() ?: "" + ) // 加载最近的聊天记录 loadChatHistory() isInitialized = true - FileLogger.d(TAG, "代理服务初始化成功") + Log.d(TAG, "代理服务初始化成功") return true } catch (e: Exception) { - FileLogger.e(TAG, "初始化失败: ${e.message}") + Log.e(TAG, "初始化失败: ${e.message}") return false } } + /** + * 释放所有资源 + */ + fun dispose() { + try { + stopRecognition() + stopTts() + stopAiStream() + stopIdleCheck() + + audioPlayer?.release() + audioPlayer = null + + azureAsrHelper?.dispose() + + // 释放TTS服务 + ttsService?.release() + ttsService = null + + job.cancel() + clearListeners() + isInitialized = false + + Log.d(TAG, "代理服务资源已释放") + } catch (e: Exception) { + Log.e(TAG, "释放资源异常: ${e.message}") + } + } + + /** + * 初始化TTS引擎,只使用火山语音引擎 + */ + private fun initializeTtsEngine(config: Map) { + // 停止当前TTS服务(如果有) + if (ttsService != null) { + ttsService?.stop() + ttsService = null + } + + try { + // 从配置中获取TTS参数 + val ttsAppId = config["volcanoAppId"]?.toString() ?: "" + val ttsAppToken = config["volcanoToken"]?.toString() ?: "" + val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN" + + // 创建并初始化AzureTtsHelper + val azureTtsHelper = AzureTtsHelper(context) + ttsService = azureTtsHelper + + // 初始化Azure TTS + val success = azureTtsHelper.initialize( + ttsAppId = "", // Azure TTS不需要appId + ttsAppToken = config["azureSpeechKey"]?.toString() ?: "", // Azure需要subscription key + ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息 + language = ttsLanguage + ) + + if (success) { + Log.d(TAG, "Azure TTS服务初始化成功") + } else { + Log.e(TAG, "Azure TTS服务初始化失败") + } + + // 添加TTS事件监听 + ttsService?.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> { + isTtsSpeaking = true + restartIdleCheck() + sendEvent("tts_started", mapOf("status" to "started")) + } + TtsEventType.SYNTHESIS_COMPLETED -> { + isTtsSpeaking = false + restartIdleCheck() + sendEvent("tts_completed", mapOf("status" to "completed")) + } + TtsEventType.SYNTHESIS_CANCELED -> { + isTtsSpeaking = false + restartIdleCheck() + sendEvent("tts_canceled", mapOf("status" to "canceled")) + } + TtsEventType.ERROR -> { + isTtsSpeaking = false + restartIdleCheck() + val params = event.params + val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" + val message = params["errorMessage"] as? String ?: "未知错误" + sendEvent("error", mapOf( + "code" to "TTS_ERROR", + "message" to "语音合成错误: $code, $message" + )) + } + else -> { + // 处理其他类型的事件 + Log.d(TAG, "TTS事件: ${event.type}") + } + } + } + }) + + Log.d(TAG, "TTS服务配置完成") + } catch (e: Exception) { + Log.e(TAG, "初始化TTS引擎失败: ${e.message}") + } + } + /** * 添加事件监听器 * @param listener 要添加的监听器 @@ -250,7 +364,7 @@ object AgentService : CoroutineScope { } catch (e: CancellationException) { // 协程被取消,正常行为 } catch (e: Exception) { - FileLogger.e(TAG, "空闲检测异常: ${e.message}") + Log.e(TAG, "空闲检测异常: ${e.message}") } } } @@ -278,7 +392,7 @@ object AgentService : CoroutineScope { */ fun startRecognition(isExternalActive: Boolean): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") return false } @@ -286,7 +400,7 @@ object AgentService : CoroutineScope { // 检查录音权限 if (!isExternalActive && !checkRecordAudioPermission()) { - FileLogger.e(TAG, "需要录音权限") + Log.e(TAG, "需要录音权限") sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) return false } @@ -300,7 +414,7 @@ object AgentService : CoroutineScope { } else { AzureAsrHelper.AudioSourceType.MICROPHONE } - FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") + Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { @@ -372,7 +486,7 @@ object AgentService : CoroutineScope { isRecognitionActive = false stopIdleCheck() BleService.closeCodec() - FileLogger.e(TAG, "语音识别错误: $error") + Log.e(TAG, "语音识别错误: $error") sendEvent("error", mapOf( "code" to "RECOGNITION_ERROR", "message" to error @@ -382,7 +496,7 @@ object AgentService : CoroutineScope { return true } catch (e: Exception) { isRecognitionActive = false - FileLogger.e(TAG, "启动语音识别失败: ${e.message}") + Log.e(TAG, "启动语音识别失败: ${e.message}") sendEvent("error", mapOf( "code" to "RECOGNITION_START_ERROR", "message" to e.message.toString() @@ -405,9 +519,9 @@ object AgentService : CoroutineScope { BleService.closeCodec() isRecognitionActive = false stopIdleCheck() - FileLogger.d(TAG, "语音识别已停止") + Log.d(TAG, "语音识别已停止") } catch (e: Exception) { - FileLogger.e(TAG, "停止语音识别异常: ${e.message}") + Log.e(TAG, "停止语音识别异常: ${e.message}") isRecognitionActive = false stopIdleCheck() } @@ -447,9 +561,9 @@ object AgentService : CoroutineScope { openAIService.cancelCurrentStream() // 记录日志 - FileLogger.d(TAG, "AI流输出已停止") + Log.d(TAG, "AI流输出已停止") } catch (e: Exception) { - FileLogger.e(TAG, "停止AI流输出异常", e) + Log.e(TAG, "停止AI流输出异常", e) // 确保状态被重置,即使发生异常 isAiStreaming = false currentAiJob = null @@ -462,13 +576,13 @@ object AgentService : CoroutineScope { */ fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } if (text.isEmpty()) { - FileLogger.e(TAG, "文本输入不能为空") + Log.e(TAG, "文本输入不能为空") sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) return false } @@ -495,7 +609,7 @@ object AgentService : CoroutineScope { text: String, speakResponse: Boolean = true ) { - FileLogger.d(TAG, "用户问题: $text") + Log.d(TAG, "用户问题: $text") // 创建用户文本消息并处理 val userMessage = openAIService.createUserMessage(text) @@ -514,7 +628,7 @@ object AgentService : CoroutineScope { text: String = "", speakResponse: Boolean = false ) { - FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") + Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") // 创建带图片的用户消息并处理 val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) @@ -546,7 +660,6 @@ object AgentService : CoroutineScope { // 设置状态为正在流式输出 isAiStreaming = true - // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() var metadata:String = "" @@ -593,24 +706,23 @@ object AgentService : CoroutineScope { addToHistoryMessages(userMessage) } - openAIService.sendMessageStream( messages = messagesWithSystemPrompt, callback = object : OpenAIService.StreamCallback { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse) { - azureTtsHelper?.speakStream(token) + ttsService?.speakStream(token) } // 发送流式回复token sendEvent("assistant_token", mapOf("token" to token)) } override fun onComplete() { - FileLogger.d(TAG, "AI完整回复: $responseBuilder") + Log.d(TAG, "AI完整回复: $responseBuilder") // 视情况决定是否朗读回复 if (speakResponse) { - azureTtsHelper?.flushStream() + ttsService?.flushStream() } val response = responseBuilder.toString() @@ -638,7 +750,7 @@ object AgentService : CoroutineScope { } override fun onError(e: Exception) { - FileLogger.e(TAG, "AI处理出错", e) + Log.e(TAG, "AI处理出错", e) sendEvent("error", mapOf( "code" to "AI_ERROR", "message" to e.message.toString() @@ -661,7 +773,7 @@ object AgentService : CoroutineScope { stopRecognition() } } catch (e: Exception) { - FileLogger.e(TAG, "处理函数调用事件失败", e) + Log.e(TAG, "处理函数调用事件失败", e) } } @@ -671,7 +783,7 @@ object AgentService : CoroutineScope { "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), )) - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + Log.d(TAG, "mcp调用结果: $functionCallResult") metadata = autoHandleFcunCallResult(functionCallResult); } } @@ -679,7 +791,7 @@ object AgentService : CoroutineScope { } catch (e: Exception) { val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" - FileLogger.e(TAG, "AI处理出错", e) + Log.e(TAG, "AI处理出错", e) sendEvent("error", mapOf( "code" to errorType, "message" to e.message.toString() @@ -722,9 +834,9 @@ object AgentService : CoroutineScope { } } - FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录") + Log.d(TAG, "已加载${recentMessages.size}条历史记录") } catch (e: Exception) { - FileLogger.e(TAG, "加载聊天历史失败: ${e.message}") + Log.e(TAG, "加载聊天历史失败: ${e.message}") } } @@ -755,8 +867,8 @@ object AgentService : CoroutineScope { isTtsSpeaking = true restartIdleCheck() // 状态变化,重启检测 - // 播放文本 - azureTtsHelper?.speakText(text) + // 直接调用TTS,无需协程包装 + ttsService?.speakOnce(text) } /** @@ -764,7 +876,8 @@ object AgentService : CoroutineScope { */ fun stopTts() { if (isTtsSpeaking) { - azureTtsHelper?.stopSpeaking() + ttsService?.stop() + isTtsSpeaking = false restartIdleCheck() // 状态变化,重启检测 sendEvent("tts_stopped", mapOf("status" to "stopped")) @@ -775,6 +888,7 @@ object AgentService : CoroutineScope { * 保存聊天消息 */ private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { + // 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中 launch(Dispatchers.IO) { try { // 保存用户消息 @@ -794,15 +908,15 @@ object AgentService : CoroutineScope { ) // if (assistantMessageId != -1L) { - // FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") + // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // } else { - // FileLogger.e(TAG, "保存助手消息失败") + // Log.e(TAG, "保存助手消息失败") // } } else { - FileLogger.e(TAG, "保存用户消息失败") + Log.e(TAG, "保存用户消息失败") } } catch (e: Exception) { - FileLogger.e(TAG, "保存聊天记录异常", e) + Log.e(TAG, "保存聊天记录异常", e) } } } @@ -821,19 +935,17 @@ object AgentService : CoroutineScope { historyMessages.remove(0) } } - FileLogger.d(TAG, "聊天历史已清除") + Log.d(TAG, "聊天历史已清除") } else { - FileLogger.e(TAG, "清除聊天历史失败") + Log.e(TAG, "清除聊天历史失败") } - withContext(Dispatchers.Main) { - callback(success) - } + // 由于launch已在Main上下文中,可以直接调用回调 + callback(success) } catch (e: Exception) { - FileLogger.e(TAG, "清除聊天历史异常: ${e.message}") - withContext(Dispatchers.Main) { - callback(false) - } + Log.e(TAG, "清除聊天历史异常: ${e.message}") + // 由于launch已在Main上下文中,可以直接调用回调 + callback(false) } } } @@ -842,57 +954,19 @@ object AgentService : CoroutineScope { * 发送事件 */ private fun sendEvent(eventName: String, data: Map) { - // 检查当前是否在主线程 - if (Looper.myLooper() == Looper.getMainLooper()) { - // 已在主线程,直接发送 - listeners.forEach { listener -> - try { - listener.onEvent(eventName, data) - } catch (e: Exception) { - FileLogger.e(TAG, "向监听器发送事件失败", e) - } + // 使用协程确保在主线程上执行 + launch { + // 我们已在主线程上下文中启动协程,无需再切换线程 + listeners.forEach { listener -> + try { + listener.onEvent(eventName, data) + } catch (e: Exception) { + Log.e(TAG, "向监听器发送事件失败", e) } - - } else { - // 不在主线程,切换到主线程 - Handler(Looper.getMainLooper()).post { - listeners.forEach { listener -> - try { - listener.onEvent(eventName, data) - } catch (e: Exception) { - FileLogger.e(TAG, "向监听器发送事件失败", e) - } - } } } } - /** - * 释放所有资源 - */ - fun dispose() { - try { - stopRecognition() - stopTts() - stopAiStream() - stopIdleCheck() - - audioPlayer?.release() - audioPlayer = null - - azureAsrHelper?.dispose() - azureTtsHelper?.dispose() - - job.cancel() - clearListeners() - isInitialized = false - - FileLogger.d(TAG, "代理服务资源已释放") - } catch (e: Exception) { - FileLogger.e(TAG, "释放资源异常: ${e.message}") - } - } - /** * 检查录音权限 */ @@ -907,13 +981,13 @@ object AgentService : CoroutineScope { */ fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } if (imagePath.isEmpty()) { - FileLogger.e(TAG, "图片路径不能为空") + Log.e(TAG, "图片路径不能为空") sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) return false } @@ -945,7 +1019,7 @@ object AgentService : CoroutineScope { // 处理包含图片的消息 processImageWithOpenAI(imageBase64, text, speakResponse) } catch (e: Exception) { - FileLogger.e(TAG, "处理图片失败: ${e.message}") + Log.e(TAG, "处理图片失败: ${e.message}") sendEvent("error", mapOf( "code" to "IMAGE_PROCESSING_ERROR", "message" to e.message.toString() @@ -993,9 +1067,9 @@ object AgentService : CoroutineScope { } } } - FileLogger.d(TAG, "开始播放音频资源") + Log.d(TAG, "开始播放音频资源") } catch (e: Exception) { - FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) + Log.e(TAG, "播放音频资源异常: ${e.message}", e) release() } } @@ -1017,17 +1091,17 @@ object AgentService : CoroutineScope { } } } - /* + /* * 自动播放音乐 * */ fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{ val metaStr = functionCallResult.optString("meta") if (metaStr.isNotEmpty()) { val meta = JSONObject(metaStr) - FileLogger.d(TAG, "mcp调用结果: $metaStr") + // Log.d(TAG, "mcp调用结果: $metaStr") if (meta.has("card_music")) { //音乐卡片 val cardMusic = meta.getJSONObject("card_music") - FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") + Log.d(TAG, "检查到音乐卡片: $cardMusic") val id = cardMusic.optString("id", "") val url = cardMusic.optString("url", "") val name = cardMusic.optString("name", "") @@ -1049,13 +1123,13 @@ object AgentService : CoroutineScope { playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) } if (playlist.isNotEmpty()) { - FileLogger.w(TAG, "自动播放音乐列表 ${playlist}") + Log.w(TAG, "自动播放音乐列表 ${playlist}") processMusicPlayList(playlist) } else { - FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目") + Log.w(TAG, "card_musiclist 中没有有效的音乐条目") } } else { - FileLogger.w(TAG, "card_musiclist.musics 不是一个数组") + Log.w(TAG, "card_musiclist.musics 不是一个数组") } }else if (meta.has("card_navigation")) { //导航服务 val cardNavigation = meta.getJSONObject("card_navigation") @@ -1080,7 +1154,7 @@ object AgentService : CoroutineScope { */ fun processMusicPlay(song: Map){ // 在其他 Service、BroadcastReceiver 或 Application 中调用 - FileLogger.i(TAG, "启动音乐服务 播放音乐 $song") + Log.i(TAG, "启动音乐服务 播放音乐 $song") MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) } @@ -1096,10 +1170,10 @@ object AgentService : CoroutineScope { fun processMusicPlayList(songs:List>){ // 如果 playlist 中有有效的歌曲,开始播放 if (songs.isNotEmpty()) { - FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") + Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) } else { - FileLogger.i(TAG, "音乐列表为空,未启动播放服务") + Log.i(TAG, "音乐列表为空,未启动播放服务") } } @@ -1114,10 +1188,10 @@ object AgentService : CoroutineScope { fun processNavigation(start:String,end:String){ // 如果 playlist 中有有效的歌曲,开始播放 if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) { - FileLogger.i(TAG, "启动导航服务 ${start} ${end}") - NavigationServiceStarter.startServiceWithCommand(context, command = "start", startpos = start, endpos = end) + Log.i(TAG, "启动导航服务 ${start} ${end}") + // NavigationServiceStarter.startServiceWithCommand(context, command = "start", startpos = start, endpos = end) } else { - FileLogger.i(TAG, "启动导航服务失败") + Log.i(TAG, "启动导航服务失败") } } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 1fc35fafa..cf00c7f3c 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import org.json.JSONObject import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch @@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream // 添加监听器 AgentService.addListener(this) - FileLogger.d(TAG, "插件已加载") + Log.d(TAG, "插件已加载") } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { @@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream return } - FileLogger.d(TAG, "启动BleMediaService,配置参数: $config") + Log.d(TAG, "启动BleMediaService,配置参数: $config") // 创建Intent启动BleMediaService val intent = Intent(context, BleMediaService::class.java) @@ -59,6 +59,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } + config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } + config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "启动AgentService失败", e) + Log.e(TAG, "启动AgentService失败", e) result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null) } } @@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream context.stopService(intent) result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止AgentService失败", e) + Log.e(TAG, "停止AgentService失败", e) result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null) } } @@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream val isStarted = AgentService.startRecognition(false) result.success(isStarted) } catch (e: Exception) { - FileLogger.e(TAG, "启动对话失败", e) + Log.e(TAG, "启动对话失败", e) result.error("START_ERROR", "启动对话失败: ${e.message}", null) } } @@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.stopRecognition() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止对话失败", e) + Log.e(TAG, "停止对话失败", e) result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) } } @@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(isProcessing) } catch (e: Exception) { - FileLogger.e(TAG, "处理文本输入失败", e) + Log.e(TAG, "处理文本输入失败", e) result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null) } } @@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(isProcessing) } catch (e: Exception) { - FileLogger.e(TAG, "处理图片输入失败", e) + Log.e(TAG, "处理图片输入失败", e) result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null) } } @@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.speakText(text) result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "语音合成失败", e) + Log.e(TAG, "语音合成失败", e) result.error("TTS_ERROR", "语音合成失败: ${e.message}", null) } } @@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.stopTts() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止语音合成失败", e) + Log.e(TAG, "停止语音合成失败", e) result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) } } @@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(success) } } catch (e: Exception) { - FileLogger.e(TAG, "清除聊天历史失败", e) + Log.e(TAG, "清除聊天历史失败", e) result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null) } } @@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.interruptCurrentResponse() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "中断响应失败", e) + Log.e(TAG, "中断响应失败", e) result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null) } } @@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.dispose() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "释放资源失败", e) + Log.e(TAG, "释放资源失败", e) result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null) } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 345eb4a2a..714d3b7d3 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service import android.content.Context import com.yunqiinnovation.agent_service.AgentService import com.yunqiinnovation.agent_service.AgentServiceListener -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleConst @@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener { if (isInitialized) return true try { - FileLogger.d(TAG, "初始化语音代理..., $config") + Log.d(TAG, "初始化语音代理..., $config") // 1. 初始化AgentService if (!AgentService.initialize(context, config, true)) { - FileLogger.e(TAG, "语音服务初始化失败") + Log.e(TAG, "语音服务初始化失败") return false } @@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener { if (BleService.initialize(context)) { if (BleService.addCallback(this)) { isRegisteredToBle = true - FileLogger.d(TAG, "已注册BLE回调") + Log.d(TAG, "已注册BLE回调") } else { - FileLogger.w(TAG, "BLE回调注册失败") + Log.w(TAG, "BLE回调注册失败") } } else { - FileLogger.e(TAG, "BLE服务初始化失败") + Log.e(TAG, "BLE服务初始化失败") return false } isInitialized = true - FileLogger.d(TAG, "语音代理初始化成功") + Log.d(TAG, "语音代理初始化成功") return true } catch (e: Exception) { - FileLogger.e(TAG, "初始化语音代理异常", e) + Log.e(TAG, "初始化语音代理异常", e) return false } } @@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { */ fun startListening(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "语音代理未初始化") + Log.e(TAG, "语音代理未初始化") return false } @@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { AgentService.dispose() isInitialized = false - FileLogger.d(TAG, "语音代理资源已释放") + Log.d(TAG, "语音代理资源已释放") } //============================================================================================ @@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener { override fun onConnectionStateChanged(state: Int) { // 可选:处理连接状态变化 if (state == BleConst.STATE_CONNECTED) { - FileLogger.d(TAG, "Ble设备已连接,准备好语音交互") + Log.d(TAG, "Ble设备已连接,准备好语音交互") } else if (state == BleConst.STATE_DISCONNECTED) { - FileLogger.d(TAG, "Ble设备已断开,停止语音服务") + Log.d(TAG, "Ble设备已断开,停止语音服务") // 设备断开连接,停止语音服务 // stopListening() } } override fun onAudioDataReceived(data: ByteArray) { - // FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") + // Log.d(TAG, "onAudioDataReceived, data: ${data.size}") AgentService.pushAudioData(data) // 可选:处理音频数据 } @@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { * 在收到唤醒信号时启动语音识别 */ override fun onWakeupSignalReceived() { - FileLogger.d(TAG, "收到唤醒信号,启动语音识别") + Log.d(TAG, "收到唤醒信号,启动语音识别") // 停止当前TTS,避免冲突 AgentService.interruptCurrentResponse() diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index ea9b0fede..0a811f33a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -21,7 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat import androidx.media.session.MediaButtonReceiver import com.yunqiinnovation.agent_service.R -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_OPENAI_MODEL = "openaiModel" private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_MCP_SERVER = "mcpServer" + private const val KEY_VOLCANO_APP_ID = "volcanoAppId" + private const val KEY_VOLCANO_TOKEN = "volcanoToken" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope { override fun onCreate() { super.onCreate() - FileLogger.init(this) - FileLogger.i(TAG, "BleMediaService正在创建...") + Log.i(TAG, "BleMediaService正在创建...") // 设置服务状态 isRunning.set(true) @@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope { audioPlayer?.play(R.raw.silence, 0f) setPlaybackState(PlaybackStateCompat.STATE_PAUSED) - FileLogger.i(TAG, "媒体服务已启动") + Log.i(TAG, "媒体服务已启动") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") + Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") // 保存当前Intent,用于后续初始化 currentIntent = intent @@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope { // 如果Intent中有参数,则保存到SharedPreferences if (configFromIntent.isNotEmpty()) { - FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") + Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") saveConfigToPreferences(configFromIntent, prefs) } // 合并Intent和SharedPreferences中的参数 val config = getConfigFromPreferences(prefs) - FileLogger.d(TAG, "当前配置参数: $config") + Log.d(TAG, "当前配置参数: $config") // 检查必要的配置是否存在 if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { - FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置") + Log.w(TAG, "语音代理初始化失败: API密钥未配置") return } if (isChatAgentInitialized) { - FileLogger.d(TAG, "ChatAgent已经初始化,跳过") + Log.d(TAG, "ChatAgent已经初始化,跳过") return } if(config.isNotEmpty()) { // 初始化ChatAgent - FileLogger.i(TAG, "开始初始化ChatAgent") + Log.i(TAG, "开始初始化ChatAgent") val success = BleAgent.initialize(applicationContext, config) if (success) { isChatAgentInitialized = true - FileLogger.i(TAG, "语音交互代理初始化成功") + Log.i(TAG, "语音交互代理初始化成功") } else { - FileLogger.e(TAG, "语音交互代理初始化失败") + Log.e(TAG, "语音交互代理初始化失败") } } } catch (e: Exception) { - FileLogger.e(TAG, "初始化语音交互代理时发生异常", e) + Log.e(TAG, "初始化语音交互代理时发生异常", e) } } @@ -206,8 +207,10 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } + it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } + it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } } - FileLogger.i(TAG, "从Intent中提取到配置参数: $config") + Log.i(TAG, "从Intent中提取到配置参数: $config") return config } @@ -236,7 +239,9 @@ class BleMediaService : Service(), CoroutineScope { KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), - KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "") + KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), + KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), + KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "") ) } @@ -250,15 +255,15 @@ class BleMediaService : Service(), CoroutineScope { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - FileLogger.i(TAG, "BleMediaService正在销毁...") + Log.i(TAG, "BleMediaService正在销毁...") // 取消注册媒体按钮接收器 try { val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) - FileLogger.i(TAG, "媒体按钮接收器已取消注册") + Log.i(TAG, "媒体按钮接收器已取消注册") } catch (e: Exception) { - FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) + Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) } // 释放媒体会话 @@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope { try { val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") + Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") } catch (e: Exception) { - FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) + Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) } } private fun checkBleConnection() { - FileLogger.i(TAG, "checkBleConnection收到连接请求") + Log.i(TAG, "checkBleConnection收到连接请求") try { // 示例:使用协程异步处理连接 @@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope { } } } catch (e: Exception) { - FileLogger.e(TAG, "连接设备失败: ${e.message}", e) + Log.e(TAG, "连接设备失败: ${e.message}", e) } } @@ -350,7 +355,7 @@ class BleMediaService : Service(), CoroutineScope { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(NOTIFICATION_ID, createNotification()) } catch (e: Exception) { - FileLogger.e(TAG, "更新通知异常: ${e.message}") + Log.e(TAG, "更新通知异常: ${e.message}") } } @@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope { // // 播放完成后自动循环播放,保持媒体会话活跃 // setOnCompletionListener { - // FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃") + // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃") // play(resId, volume) // } @@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope { start() } - FileLogger.i(TAG, "开始播放静音资源") + Log.i(TAG, "开始播放静音资源") } catch (e: Exception) { - FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e) + Log.e(TAG, "播放静音资源异常: ${e.message}", e) release() } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt index dacd6e2a7..2b5457952 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * 通用工具类,包含安全存储等功能 @@ -59,10 +59,10 @@ object Utils { // 提交更改 editor.apply() - FileLogger.d(TAG, "数据已安全保存到加密存储中") + Log.d(TAG, "数据已安全保存到加密存储中") true } catch (e: Exception) { - FileLogger.e(TAG, "保存数据到加密存储失败", e) + Log.e(TAG, "保存数据到加密存储失败", e) false } } @@ -97,10 +97,10 @@ object Utils { } } - FileLogger.d(TAG, "已从加密存储加载所有数据") + Log.d(TAG, "已从加密存储加载所有数据") resultMap } catch (e: Exception) { - FileLogger.e(TAG, "从加密存储加载所有数据失败", e) + Log.e(TAG, "从加密存储加载所有数据失败", e) emptyMap() } } @@ -134,10 +134,10 @@ object Utils { .putString(KEY_SESSION_ID, sessionId) .apply() - FileLogger.d(TAG, "已清除所有安全存储的数据") + Log.d(TAG, "已清除所有安全存储的数据") true } catch (e: Exception) { - FileLogger.e(TAG, "清除安全存储数据失败", e) + Log.e(TAG, "清除安全存储数据失败", e) false } } diff --git a/local_plugins/agent_service/pubspec.yaml b/local_plugins/agent_service/pubspec.yaml index 04ff16b78..f5dc12c7d 100644 --- a/local_plugins/agent_service/pubspec.yaml +++ b/local_plugins/agent_service/pubspec.yaml @@ -10,20 +10,6 @@ environment: dependencies: flutter: sdk: flutter - # 依赖本地插件 - open_ai_service: - path: ../open_ai_service - azure_speech: - path: ../azure_speech - deepvoice_utils: - path: ../deepvoice_utils - chat_storage: - path: ../chat_storage - music_service: - path: ../music_service - amap_service: - path: ../amap_service - get: ^4.6.5 dev_dependencies: flutter_test: diff --git a/local_plugins/azure_speech/.gitignore b/local_plugins/azure_speech/.gitignore new file mode 100644 index 000000000..79c113f9b --- /dev/null +++ b/local_plugins/azure_speech/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/local_plugins/azure_speech/.metadata b/local_plugins/azure_speech/.metadata new file mode 100644 index 000000000..5c982c8d4 --- /dev/null +++ b/local_plugins/azure_speech/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/azure_speech/analysis_options.yaml b/local_plugins/azure_speech/analysis_options.yaml new file mode 100644 index 000000000..f9b303465 --- /dev/null +++ b/local_plugins/azure_speech/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index 151988920..8db97c1d7 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/local_plugins/azure_speech/android/build.gradle.kts @@ -52,6 +52,7 @@ tasks.withType { dependencies { // 添加Microsoft语音SDK - implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0") - implementation(project(":ble_service")) + implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0") + implementation(project(":speech")) + } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 54a68eb56..abbe6e38a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -10,7 +10,7 @@ import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.NoiseSuppressor import android.media.audiofx.AutomaticGainControl import android.os.Process -import com.yunqiinnovation.azure_speech.utils.FileLogger +import android.util.Log import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.util.EventHandler @@ -84,7 +84,7 @@ class AzureAsrHelper(private val context: Context) { try { // 检查配置是否为空 if (subscriptionKey.isEmpty() || region.isEmpty()) { - FileLogger.e(tag, "Azure 配置信息不完整") + Log.e(tag, "Azure 配置信息不完整") return false } @@ -124,7 +124,7 @@ class AzureAsrHelper(private val context: Context) { // 创建识别器 return setupRecognizer() } catch (e: Exception) { - FileLogger.e(tag, "初始化失败: ${e.message}") + Log.e(tag, "初始化失败: ${e.message}") return false } } @@ -184,7 +184,7 @@ class AzureAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(tag, "创建识别器失败: ${e.message}") + Log.e(tag, "创建识别器失败: ${e.message}") stopAudioProcessing() return false } @@ -201,8 +201,8 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 audioConfig = AudioConfig.fromStreamInput(microphoneStream) } catch (e: Exception) { - FileLogger.e(tag, "设置麦克风流失败: ${e.message}") - e.printStackTrace() + Log.e(tag, "设置麦克风流失败: ${e.message}") + microphoneStream = null } } @@ -217,8 +217,8 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 audioConfig = AudioConfig.fromStreamInput(externalAudioStream) } catch (e: Exception) { - FileLogger.e(tag, "设置外部音频流失败: ${e.message}") - e.printStackTrace() + Log.e(tag, "设置外部音频流失败: ${e.message}") + externalAudioStream = null } } @@ -368,23 +368,13 @@ class AzureAsrHelper(private val context: Context) { // 取消事件 recognizer?.canceled?.addEventListener( EventHandler { _, event -> - val errorDetails = try { - event.errorDetails ?: "未知错误" - } catch (e: Exception) { - "未知错误" - } + val errorDetails = event.errorDetails ?: "未知错误" val reason = event.reason.toString() - - FileLogger.e(tag, "识别取消: $errorDetails") - isContinuousRecognitionActive = false - stopAudioProcessing() - - // 直接在当前线程调用回调 callback.onCanceled(reason, errorDetails) - callback.onError("识别取消: $errorDetails") // 兼容旧接口 } ) } + /** * 停止连续语音识别 @@ -393,7 +383,7 @@ class AzureAsrHelper(private val context: Context) { */ fun stopContinuousRecognition(): Boolean { if (speechConfig == null) { - FileLogger.e(tag, "语音服务未初始化") + Log.e(tag, "语音服务未初始化") return false } @@ -403,7 +393,7 @@ class AzureAsrHelper(private val context: Context) { try { if (recognizer == null) { - FileLogger.w(tag, "识别器为空,重置状态") + Log.w(tag, "识别器为空,重置状态") isContinuousRecognitionActive = false return true } @@ -422,7 +412,7 @@ class AzureAsrHelper(private val context: Context) { } catch (e: Exception) { // 强制重置状态 isContinuousRecognitionActive = false - FileLogger.e(tag, "停止连续识别失败: ${e.message}") + Log.e(tag, "停止连续识别失败: ${e.message}") // 停止音频处理 stopAudioProcessing() @@ -432,7 +422,7 @@ class AzureAsrHelper(private val context: Context) { recognizer?.close() recognizer = null } catch (ex: Exception) { - FileLogger.e(tag, "关闭识别器失败: ${ex.message}") + Log.e(tag, "关闭识别器失败: ${ex.message}") } return false @@ -515,7 +505,7 @@ class AzureAsrHelper(private val context: Context) { it.close() microphoneStream = null } catch (e: Exception) { - FileLogger.e(tag, "关闭麦克风流失败: ${e.message}") + Log.e(tag, "关闭麦克风流失败: ${e.message}") e.printStackTrace() } } @@ -525,7 +515,7 @@ class AzureAsrHelper(private val context: Context) { it.close() externalAudioStream = null } catch (e: Exception) { - FileLogger.e(tag, "关闭外部音频流失败: ${e.message}") + Log.e(tag, "关闭外部音频流失败: ${e.message}") e.printStackTrace() } } @@ -623,12 +613,13 @@ class AzureAsrHelper(private val context: Context) { try { val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 if (result < 0) { - FileLogger.e(tag, "读取音频数据失败: $result") + Log.e(tag, "读取音频数据失败: $result") + return 0 } return result } catch (e: Exception) { - FileLogger.e(tag, "读取音频异常: ${e.message}") - return -1 + Log.e(tag, "读取音频异常: ${e.message}") + return 0 } } @@ -696,7 +687,7 @@ class AzureAsrHelper(private val context: Context) { Thread.currentThread().interrupt() return 0 } catch (e: Exception) { - FileLogger.e(tag, "读取外部音频数据失败: ${e.message}") + Log.e(tag, "读取外部音频数据失败: ${e.message}") return 0 } } 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 df4dff206..d4b83d07b 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 @@ -11,21 +11,16 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel -import kotlinx.coroutines.* -import kotlin.coroutines.CoroutineContext import com.microsoft.cognitiveservices.speech.audio.* -import com.yunqiinnovation.ble_service.BleService -import com.yunqiinnovation.ble_service.BleConst +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType + /** AzureSpeechPlugin */ -class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { +class AzureSpeechPlugin: FlutterPlugin { private val tag = "AzureSpeechPlugin" private lateinit var context: Context private val mainHandler = Handler(Looper.getMainLooper()) - - // 协程相关 - private val job = SupervisorJob() - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job // ASR相关 private lateinit var asrChannel : MethodChannel @@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { private lateinit var ttsEventChannel: EventChannel private var ttsEventSink: EventChannel.EventSink? = null private lateinit var azureTtsHelper: AzureTtsHelper - // 是否已注册BLE回调 - private var isRegisteredToBle = false - + // 是否已添加TTS事件监听器 + private var isTtsListenerAdded = false // ASR 事件发送方法 private fun sendAsrEvent(event: Map) { @@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { } // 确保在主线程上发送事件 - mainHandler.post { try { ttsEventSink?.success(event) @@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { ttsEventSink = events + setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器 } override fun onCancel(arguments: Any?) { @@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { // 初始化Azure语音服务 azureTtsHelper = AzureTtsHelper(context) azureAsrHelper = AzureAsrHelper(context) + } - // 2. 初始化BleService并注册回调 - if (BleService.initialize(context)) { - if (BleService.addCallback(this)) { - isRegisteredToBle = true - FileLogger.d(tag, "已注册BLE回调") - } else { - FileLogger.w(tag, "BLE回调注册失败") - } - } else { - FileLogger.e(tag, "BLE服务初始化失败") - + /** + * 设置TTS事件监听器 + */ + private fun setupTtsEventListener() { + if (!isTtsListenerAdded) { + // 添加TTS事件监听器 + azureTtsHelper.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + // 将TTS事件转换为Flutter可理解的格式 + val eventMap = when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> mapOf( + "type" to "synthesis_started" + ) + TtsEventType.SYNTHESIS_COMPLETED -> mapOf( + "type" to "synthesis_completed" + ) + TtsEventType.SYNTHESIS_CANCELED -> { + val params = event.params + mapOf( + "type" to "synthesis_canceled", + "reason" to (params["reason"] ?: ""), + "errorDetails" to (params["errorDetails"] ?: "") + ) } + TtsEventType.ERROR -> { + val params = event.params + mapOf( + "type" to "error", + "errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"), + "errorMessage" to (params["errorMessage"] ?: "未知错误") + ) + } + else -> mapOf( + "type" to event.type.toString().lowercase(), + "params" to event.params + ) + } + + // 发送事件 + sendTtsEvent(eventMap) + } + }) + + isTtsListenerAdded = true + } } // ASR方法处理器 @@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { azureAsrHelper.dispose() result.success(true) } + "pushAudioData" -> { + val data = call.argument("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null) + azureAsrHelper.pushAudioData(data) + result.success(true) + } else -> { result.notImplemented() } @@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") ?: "" val region = call.argument("region") ?: "" - val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: false // 默认使用自定义播放器 + val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: false - // 先释放现有资源 - azureTtsHelper.dispose() - - // 创建TTS回调 - val ttsCallback = createTtsCallback() - - // 初始化TTS引擎 - val success = azureTtsHelper.initialize( - subscriptionKey = subscriptionKey, - region = region, - callback = ttsCallback - ) - - // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器 - if (success && useCustomAudioOutput) { - // 创建SimpleAudioPlayer实例 - val customPlayer = SimpleAudioPlayer(context) + try { + // 初始化TTS引擎(使用新的接口方法) + val success = azureTtsHelper.initialize( + ttsAppId = "", // Azure TTS不需要appId + ttsAppToken = subscriptionKey, + ttsResource = region, + language = "zh-CN" + ) - // 获取并设置音频输出流 - val outputStream = customPlayer.getAudioOutputStream() - if (outputStream != null) { - azureTtsHelper.setCustomAudioOutputStream(outputStream) + // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器 + if (success && useCustomAudioOutput) { + // 创建SimpleAudioPlayer实例 + val customPlayer = SimpleAudioPlayer(context) + + // 获取并设置音频输出流 + val outputStream = customPlayer.getAudioOutputStream() + if (outputStream != null) { + azureTtsHelper.setCustomAudioOutputStream(outputStream) + } } + + // 确保设置了TTS事件监听器 + setupTtsEventListener() + + result.success(success) + } catch (e: Exception) { + result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null) } - - result.success(success) } "setVoice" -> { val voiceName = call.argument("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) @@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { "speakText" -> { val text = call.argument("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null) - // 直接传递参数给speakText方法 - val success = azureTtsHelper.speakText(text) - result.success(success) + // 直接调用speakOnce方法 + try { + val success = azureTtsHelper.speakOnce(text) + result.success(success) + } catch (e: Exception) { + result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null) + } } "stopSpeaking" -> { - result.success(azureTtsHelper.stopSpeaking()) + // 直接调用stop方法 + try { + val success = azureTtsHelper.stop() + result.success(success) + } catch (e: Exception) { + result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null) + } } "isSpeaking" -> { result.success(azureTtsHelper.isSpeaking()) } "dispose" -> { - azureTtsHelper.dispose() + azureTtsHelper.release() result.success(true) } else -> { @@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { } } } - - /** - * 创建TTS回调 - */ - private fun createTtsCallback(): AzureTtsHelper.TtsCallback { - return object : AzureTtsHelper.TtsCallback { - override fun onSynthesisStarted() { - sendTtsEvent(mapOf( - "type" to "synthesis_started" - )) - } - - override fun onSynthesizing() { - sendTtsEvent(mapOf( - "type" to "synthesizing" - )) - } - - override fun onSynthesisCompleted() { - sendTtsEvent(mapOf( - "type" to "synthesis_completed" - )) - } - - override fun onSynthesisCanceled() { - sendTtsEvent(mapOf( - "type" to "synthesis_canceled" - )) - } - } - } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { @@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { ttsEventChannel.setStreamHandler(null) try { - // 取消所有协程 - job.cancel() - // 取消BLE回调注册 - if (isRegisteredToBle) { - BleService.removeCallback(this) - isRegisteredToBle = false - } - // 释放资源 - azureTtsHelper.dispose() + azureTtsHelper.release() azureAsrHelper.dispose() } catch (e: Exception) { FileLogger.e(tag, "释放资源失败: ${e.message}") } } - - - override fun onScanResult(devices: List>) { - - } - - override fun onConnectionStateChanged(state: Int) { - // 可选:处理连接状态变化 - if (state == BleConst.STATE_CONNECTED) { - FileLogger.d(tag, "Ble设备已连接,准备好语音交互") - } else if (state == BleConst.STATE_DISCONNECTED) { - FileLogger.d(tag, "Ble设备已断开,停止语音服务") - // 设备断开连接,停止语音服务 - // stopListening() - } - - } - - override fun onAudioDataReceived(data: ByteArray) { - // FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}") - azureAsrHelper.pushAudioData(data) - } - - override fun onWakeupSignalReceived() { - - } - - override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { - - } } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 24ec6ffdf..70227503f 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -4,10 +4,14 @@ import android.content.Context import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import com.yunqiinnovation.azure_speech.utils.FileLogger +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.AudioDataListener import kotlinx.coroutines.* import java.io.ByteArrayInputStream import java.io.InputStream -import kotlin.coroutines.CoroutineContext /** * Azure TTS Helper @@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext * 基于微软Azure语音服务的TTS实现 * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis */ -class AzureTtsHelper(private val context: Context) : CoroutineScope { +class AzureTtsHelper(private val context: Context) : ITtsService { companion object { private const val TAG = "AzureTtsHelper" private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" } - // 协程上下文 - private val job = SupervisorJob() - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job - // Azure语音服务配置 private var speechConfig: SpeechConfig? = null private var synthesizer: SpeechSynthesizer? = null @@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { private var currentPitch = "0%" private var currentVolume = "100%" - // 全局回调 - private var ttsCallback: TtsCallback? = null + // 事件监听器列表 + private val eventListeners = mutableListOf() + private val audioDataListeners = mutableListOf() // 自定义音频输出流 private var customAudioOutputStream: PushAudioOutputStream? = null @@ -48,54 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { // 流式文本处理的缓冲区 private val streamBuffer = StringBuilder() private var lastSpeakTime = 0L - - /** - * 简化的TTS回调接口 - */ - interface TtsCallback { - /** - * 合成开始 - */ - fun onSynthesisStarted() {} - - /** - * 合成中 - */ - fun onSynthesizing() {} - - /** - * 合成完成 - */ - fun onSynthesisCompleted() {} - - /** - * 合成取消 - */ - fun onSynthesisCanceled() {} - } /** * 初始化TTS引擎 * - * @param subscriptionKey Azure语音服务订阅密钥 - * @param region Azure语音服务区域 - * @param callback 可选,全局TTS回调接口 + * @param ttsAppId 未使用,保留为空即可 + * @param ttsAppToken Azure语音服务订阅密钥 + * @param ttsResource Azure语音服务区域 + * @param language 语言代码,如"zh-CN" * @return 是否初始化成功 */ - fun initialize( - subscriptionKey: String, - region: String, - callback: TtsCallback? = null + override fun initialize( + ttsAppId: String, + ttsAppToken: String, + ttsResource: String, + language: String ): Boolean { try { // 释放之前的资源 dispose() - // 存储全局回调 - ttsCallback = callback - // 创建语音配置 - speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) + speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisVoiceName(currentVoice) @@ -115,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { setupEventListeners() isInitialized = true - FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region") + FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource") return true } catch (e: Exception) { @@ -148,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { } /** - * 设置TTS回调 - * - * @param callback TTS回调接口 + * 添加TTS事件监听器 + */ + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) + } + } + + /** + * 移除TTS事件监听器 + */ + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) + } + + /** + * 添加音频数据监听器 + */ + override fun addAudioDataListener(listener: AudioDataListener) { + if (!audioDataListeners.contains(listener)) { + audioDataListeners.add(listener) + } + } + + /** + * 移除音频数据监听器 + */ + override fun removeAudioDataListener(listener: AudioDataListener) { + audioDataListeners.remove(listener) + } + + /** + * 触发事件通知 */ - fun setTtsCallback(callback: TtsCallback?) { - ttsCallback = callback + private fun notifyEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) + // 复制列表防止并发修改异常 + val listeners = ArrayList(eventListeners) + for (listener in listeners) { + try { + listener.onEvent(event) + } catch (e: Exception) { + FileLogger.e(TAG, "通知事件异常: ${e.message}") + } + } } /** @@ -163,33 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { synthesizer?.apply { // 合成开始事件 SynthesisStarted?.addEventListener { _, _ -> - ttsCallback?.onSynthesisStarted() - } - - // 合成中事件 - Synthesizing?.addEventListener { _, _ -> - ttsCallback?.onSynthesizing() + notifyEvent(TtsEventType.SYNTHESIS_STARTED) } // 合成完成事件 SynthesisCompleted?.addEventListener { _, _ -> isSpeaking = false - ttsCallback?.onSynthesisCompleted() + notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) } // 合成取消事件 - SynthesisCanceled?.addEventListener { _, _ -> + SynthesisCanceled?.addEventListener { _, eventArgs -> FileLogger.d(TAG, "语音合成取消") isSpeaking = false - ttsCallback?.onSynthesisCanceled() + + notifyEvent(TtsEventType.SYNTHESIS_CANCELED) } } } /** - * 设置语音 + * 设置语音角色 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { if (!isInitialized) return false try { @@ -199,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "VOICE_SET_FAILED", + "errorMessage" to "设置语音失败: ${e.message}" + )) return false } } @@ -224,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { setupEventListeners() } catch (e: Exception) { FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "RECREATE_FAILED", + "errorMessage" to "重新创建合成器失败: ${e.message}" + )) } } @@ -240,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音参数失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "PARAMS_SET_FAILED", + "errorMessage" to "设置语音参数失败: ${e.message}" + )) return false } } @@ -255,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { * 生成SSML */ private fun generateSsml(rawText: String): String { - // 1. 定义要静音的符号和表情符号列表 - val symbolsToMute = listOf( - "#", "*", "@", "%", "^", "&", - "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" - ) + // 1. 定义要静音的符号和表情符号列表 + val symbolsToMute = listOf( + "#", "*", "@", "%", "^", "&", + "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" + ) - // 2. 转义 XML 保留字符 - val escapedText = rawText - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") + // 2. 转义 XML 保留字符 + val escapedText = rawText + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") - // 3. 静音处理特殊符号和表情符号 - // 使用空白替换法,直接将符号替换为空格 - var processedText = escapedText - symbolsToMute.forEach { sym -> - processedText = processedText.replace(sym, "") - } + // 3. 静音处理特殊符号和表情符号 + // 使用空白替换法,直接将符号替换为空格 + var processedText = escapedText + symbolsToMute.forEach { sym -> + processedText = processedText.replace(sym, "") + } - // 4. 构造简化的SSML文档,减少嵌套层级 - return """ - - - - - $processedText - - - - - """.trimIndent() -} + // 4. 构造简化的SSML文档,减少嵌套层级 + return """ + + + + + $processedText + + + + + """.trimIndent() + } /** - * 合成文本为语音 - * - * @param text 要合成的文本 - * @return 是否成功开始合成 + * 单次播放文本(非流式) */ - fun speakText(text: String): Boolean { + override fun speakOnce(text: String): Boolean { if (!isInitialized) { + FileLogger.e(TAG, "TTS引擎未初始化") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) return false } @@ -308,42 +331,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { isSpeaking = true - // 使用纯异步方式 - + // 直接调用SpeakSsmlAsync,SDK内部已经是异步的 synthesizer?.SpeakSsmlAsync(ssml) - + // 返回true表示已开始合成请求 return true } catch (e: Exception) { isSpeaking = false + FileLogger.e(TAG, "语音合成异常: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "SYNTHESIS_ERROR", + "errorMessage" to "语音合成异常: ${e.message}" + )) return false } } /** - * 处理流式文本并在遇到标点符号时播放 - * - * @param text 收到的文本流片段 - * @return 是否成功处理 + * 流式合成文本 */ - fun speakStream(text: String): Boolean { + override fun speakStream(text: String): Boolean { if (!isInitialized || text.isBlank()) { + if (!isInitialized) { + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) + } return false } try { // 添加新文本到缓冲区 streamBuffer.append(text) + // 增加500ms防抖逻辑 val currentTime = System.currentTimeMillis() if (currentTime - lastSpeakTime < 600) { return true } lastSpeakTime = currentTime + val currentText = streamBuffer.toString() // 定义标点符号列表 - val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') + val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') // 查找最后一个标点符号的位置 var lastPunctuationIndex = -1 @@ -363,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { streamBuffer.delete(0, lastPunctuationIndex + 1) // 播放提取的文本 - return speakText(textToSpeak) + return speakOnce(textToSpeak) } // 如果没有找到标点符号,则等待更多文本 return true } catch (e: Exception) { FileLogger.e(TAG, "流式语音合成失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "STREAM_FAILED", + "errorMessage" to "流式语音合成失败: ${e.message}" + )) return false } } /** * 播放剩余的流式文本 - * - * @return 是否成功播放剩余文本 */ - fun flushStream(): Boolean { + override fun flushStream(): Boolean { if (!isInitialized) { + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) return false } @@ -397,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { } // 播放剩余文本 - return speakText(remainingText) + return speakOnce(remainingText) } catch (e: Exception) { FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "FLUSH_FAILED", + "errorMessage" to "刷新流式文本失败: ${e.message}" + )) return false } } @@ -407,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { /** * 停止当前语音合成 */ - fun stopSpeaking(): Boolean { + override fun stop(): Boolean { if (!isInitialized) return false try { @@ -416,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { isSpeaking = false return true } catch (e: Exception) { + FileLogger.e(TAG, "停止语音合成失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "STOP_FAILED", + "errorMessage" to "停止语音合成失败: ${e.message}" + )) return false } } @@ -423,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { /** * 释放资源 */ - fun dispose() { + override fun release() { + dispose() + } + + /** + * 释放资源(内部方法) + */ + private fun dispose() { try { - stopSpeaking() + // 停止播放 + stop() + + // 关闭合成器 synthesizer?.close() + synthesizer = null + + // 关闭配置 speechConfig?.close() + speechConfig = null // 关闭自定义音频输出流 try { @@ -437,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { // 忽略关闭异常 } - // 清理引用 - ttsCallback = null - // 清空流缓冲区 streamBuffer.clear() - // 取消所有协程 - job.cancel() - + // 重置状态 isInitialized = false isSpeaking = false } catch (e: Exception) { diff --git a/local_plugins/azure_speech/ios/.gitignore b/local_plugins/azure_speech/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/local_plugins/azure_speech/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist b/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..7c5696400 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig b/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..592ceee85 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Flutter/Release.xcconfig b/local_plugins/azure_speech/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..592ceee85 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..8b82c852b --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,619 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* 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 */ + 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 = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 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 */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + 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 = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 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"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 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 = 12.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PNNU77KTCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.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; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.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; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.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 = 12.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 = 12.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PNNU77KTCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = PNNU77KTCP; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner/RunnerRelease.entitlements b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 60% rename from ios/Runner/RunnerRelease.entitlements rename to local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 62a4a38c3..18d981003 100644 --- a/ios/Runner/RunnerRelease.entitlements +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,9 +2,7 @@ - keychain-access-groups - - $(AppIdentifierPrefix)com.yunqiinnovation.deepsound - + IDEDidComputeMac32BitWarning + diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..15cada483 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_plugins/azure_speech/ios/Runner/AppDelegate.swift b/local_plugins/azure_speech/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..626664468 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..7353c41ec Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6ed2d933e Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cd7b0099 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..fe730945a Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..321773cd8 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..502f463a9 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..e9f5fea27 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..84ac32ae7 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..8953cba09 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0467bf12a Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner/Info.plist b/local_plugins/azure_speech/ios/Runner/Info.plist new file mode 100644 index 000000000..1d54d7cd7 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Azure Speech + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + azure_speech + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h b/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift b/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/local_plugins/azure_speech/ios/azure_speech.podspec b/local_plugins/azure_speech/ios/azure_speech.podspec index bc179f0c1..b6eed7b01 100644 --- a/local_plugins/azure_speech/ios/azure_speech.podspec +++ b/local_plugins/azure_speech/ios/azure_speech.podspec @@ -16,7 +16,7 @@ A Flutter plugin for Azure Speech services. s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.dependency 'MicrosoftCognitiveServicesSpeech-iOS' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/azure_speech/lib/main.dart b/local_plugins/azure_speech/lib/main.dart new file mode 100644 index 000000000..a7256585a --- /dev/null +++ b/local_plugins/azure_speech/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World!'), + ), + ), + ); + } +} diff --git a/local_plugins/ble_service/LICENSE b/local_plugins/ble_service/LICENSE new file mode 100644 index 000000000..5565c0008 --- /dev/null +++ b/local_plugins/ble_service/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Deep Voice + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/local_plugins/ble_service/android/build.gradle.kts b/local_plugins/ble_service/android/build.gradle.kts index 29deed814..398f8afa7 100644 --- a/local_plugins/ble_service/android/build.gradle.kts +++ b/local_plugins/ble_service/android/build.gradle.kts @@ -30,5 +30,5 @@ dependencies { implementation("androidx.core:core-ktx:1.13.1") // 使用从本地Maven仓库引用的AAR依赖 implementation("com.jieli.audio:jl_audio_decode:2.0.0") - implementation(project(":deepvoice_utils")) + implementation("com.jieli.jl_bt_ota:jl_bt_ota:1.9.2") } \ No newline at end of file diff --git a/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005.aar b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005.aar new file mode 100644 index 000000000..0ec76c882 Binary files /dev/null and b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005.aar differ diff --git a/local_plugins/ble_service/android/libs/jl_bt_ota_V1.9.2-release.aar b/local_plugins/ble_service/android/libs/jl_bt_ota_V1.9.2-release.aar new file mode 100644 index 000000000..9ecf85489 Binary files /dev/null and b/local_plugins/ble_service/android/libs/jl_bt_ota_V1.9.2-release.aar differ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt index 502d527e3..0365a65ff 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt @@ -17,7 +17,7 @@ import android.content.SharedPreferences import android.os.Build import android.os.ParcelUuid import androidx.annotation.RequiresApi -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.util.regex.Pattern /** @@ -71,7 +71,7 @@ class BleCompanionManager(private val context: Context) { */ fun savePairedMacAddress(macAddress: String) { sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() - FileLogger.i(TAG, "已保存配对设备MAC: $macAddress") + Log.i(TAG, "已保存配对设备MAC: $macAddress") } /** @@ -79,7 +79,7 @@ class BleCompanionManager(private val context: Context) { */ fun clearPairedMacAddress() { sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() - FileLogger.i(TAG, "已清除配对设备MAC") + Log.i(TAG, "已清除配对设备MAC") } /** @@ -110,7 +110,7 @@ class BleCompanionManager(private val context: Context) { idField.get(association).toString() } ?: emptyList() } catch (e: Exception) { - FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + Log.e(TAG, "获取关联ID失败: ${e.message}", e) emptyList() } } else { @@ -135,7 +135,7 @@ class BleCompanionManager(private val context: Context) { macAddress: String? = null ): Boolean { if (!isCompanionDeviceManagerSupported()) { - FileLogger.e(TAG, "设备不支持CompanionDeviceManager") + Log.e(TAG, "设备不支持CompanionDeviceManager") return false } @@ -148,7 +148,7 @@ class BleCompanionManager(private val context: Context) { // 使用"^设备名称$"格式创建精确匹配的正则表达式 val exactNamePattern = Pattern.compile("^$it$") deviceFilterBuilder.setNamePattern(exactNamePattern) - FileLogger.i(TAG, "设置严格名称匹配: $it") + Log.i(TAG, "设置严格名称匹配: $it") } // 添加服务UUID @@ -156,9 +156,9 @@ class BleCompanionManager(private val context: Context) { try { val uuid = ParcelUuid(java.util.UUID.fromString(it)) deviceFilterBuilder.addServiceUuid(uuid, null) - FileLogger.i(TAG, "设置服务UUID匹配: $it") + Log.i(TAG, "设置服务UUID匹配: $it") } catch (e: Exception) { - FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) + Log.e(TAG, "设置服务UUID失败: ${e.message}", e) } } @@ -167,7 +167,7 @@ class BleCompanionManager(private val context: Context) { try { // 检查是否是有效的MAC地址格式 if (!isValidMacAddress(it)) { - FileLogger.e(TAG, "无效的MAC地址格式: $it") + Log.e(TAG, "无效的MAC地址格式: $it") return false } @@ -176,15 +176,15 @@ class BleCompanionManager(private val context: Context) { // 设置MAC地址严格匹配 deviceFilterBuilder.setAddress(it) - FileLogger.i(TAG, "设置MAC地址严格匹配: $it") + Log.i(TAG, "设置MAC地址严格匹配: $it") } catch (e: Exception) { - FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e) + Log.e(TAG, "设置MAC地址失败: ${e.message}", e) return false } } ?: run { // 如果没有提供MAC地址也没有提供设备名称,则返回错误 if (deviceNamePattern == null && serviceUuid == null) { - FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") + Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") return false } } @@ -209,9 +209,9 @@ class BleCompanionManager(private val context: Context) { 0, 0 ) - FileLogger.i(TAG, "已启动设备选择对话框") + Log.i(TAG, "已启动设备选择对话框") } catch (e: Exception) { - FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) } } @@ -228,28 +228,28 @@ class BleCompanionManager(private val context: Context) { 0, 0 ) - FileLogger.i(TAG, "已启动设备选择对话框") + Log.i(TAG, "已启动设备选择对话框") } catch (e: Exception) { - FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) } } // API 33+ (Android 13+) 使用 onAssociationCreated @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onAssociationCreated(associationInfo: AssociationInfo) { - FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") + Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") // 保存关联ID try { val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") val mac = macAddressMethod.invoke(associationInfo) as? String mac?.let { savePairedMacAddress(it) } } catch (e: Exception) { - FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) } } override fun onFailure(error: CharSequence?) { - FileLogger.e(TAG, "设备关联失败: $error") + Log.e(TAG, "设备关联失败: $error") } }, null @@ -257,7 +257,7 @@ class BleCompanionManager(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) + Log.e(TAG, "启动设备关联失败: ${e.message}", e) return false } } @@ -276,7 +276,7 @@ class BleCompanionManager(private val context: Context) { return false } - FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") + Log.i(TAG, "收到关联结果: resultCode=$resultCode") if (resultCode == Activity.RESULT_OK && data != null) { // 获取选中的蓝牙设备 @@ -295,7 +295,7 @@ class BleCompanionManager(private val context: Context) { val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" data.getStringExtra(extraAssociationIdKey) } catch (e: Exception) { - FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + Log.e(TAG, "获取关联ID失败: ${e.message}", e) null } } else { @@ -309,20 +309,20 @@ class BleCompanionManager(private val context: Context) { // 验证MAC地址格式 if (!isValidMacAddress(deviceMac)) { - FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") + Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") return false } // 检查是否与预期的MAC地址匹配 if (pendingMacAddress != null && pendingMacAddress != deviceMac) { - FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") + Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") // 如果要严格匹配MAC,可以在这里返回false return false } // 匹配成功后,保存新的MAC地址 savePairedMacAddress(deviceMac) - FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") + Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") // 清除临时MAC地址 pendingMacAddress = null @@ -330,10 +330,10 @@ class BleCompanionManager(private val context: Context) { return true } else { - FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") + Log.e(TAG, "无法获取关联的蓝牙设备信息") } } else { - FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") + Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") // 清除临时MAC地址 pendingMacAddress = null } @@ -350,7 +350,7 @@ class BleCompanionManager(private val context: Context) { try { val mac = getPairedMacAddress() if (mac.isNullOrEmpty()) { - FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") + Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") return } @@ -359,7 +359,7 @@ class BleCompanionManager(private val context: Context) { val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner == null) { - FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") + Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") return } @@ -391,9 +391,9 @@ class BleCompanionManager(private val context: Context) { // 启动后台扫描 bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) - FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") + Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac") } catch (e: Exception) { - FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) + Log.e(TAG, "注册后台扫描失败: ${e.message}", e) } } @@ -407,7 +407,7 @@ class BleCompanionManager(private val context: Context) { val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner == null) { - FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") + Log.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") return } @@ -428,9 +428,9 @@ class BleCompanionManager(private val context: Context) { // 停止后台扫描 bluetoothLeScanner.stopScan(pendingIntent) - FileLogger.i(TAG, "已取消后台扫描") + Log.i(TAG, "已取消后台扫描") } catch (e: Exception) { - FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e) + Log.e(TAG, "取消后台扫描失败: ${e.message}", e) } } @@ -454,13 +454,13 @@ class BleCompanionManager(private val context: Context) { } else { companionDeviceManager?.disassociate(id) } - FileLogger.i(TAG, "已解除设备关联: $id") + Log.i(TAG, "已解除设备关联: $id") } catch (e: Exception) { - FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) + Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e) } } } catch (e: Exception) { - FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) + Log.e(TAG, "清除所有关联失败: ${e.message}", e) } } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt index 0de2320cf..1e50ccc32 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleCompanionService @@ -19,13 +19,12 @@ class BleCompanionService : CompanionDeviceService() { override fun onCreate() { super.onCreate() - FileLogger.init(applicationContext) - FileLogger.i(TAG, "BleCompanionService已创建") + Log.i(TAG, "BleCompanionService已创建") } override fun onDeviceAppeared(info: AssociationInfo) { super.onDeviceAppeared(info) - FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") + Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") // 当配对设备出现时,启动前台服务 try { @@ -49,21 +48,21 @@ class BleCompanionService : CompanionDeviceService() { val mac = macAddressMethod.invoke(info) as? String mac?.let { putExtra("mac", it) } } catch (e: Exception) { - FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) } } // 启动前台服务 startForegroundService(serviceIntent) - FileLogger.i(TAG, "已启动服务") + Log.i(TAG, "已启动服务") } catch (e: Exception) { - FileLogger.e(TAG, "启动服务失败: ${e.message}", e) + Log.e(TAG, "启动服务失败: ${e.message}", e) } } override fun onDeviceDisappeared(info: AssociationInfo) { super.onDeviceDisappeared(info) - FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") + Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") // 可以选择停止服务或执行其他清理操作 } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 833148caa..999d56c3a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -8,7 +8,7 @@ import android.content.Context import android.content.Intent import android.os.* import androidx.lifecycle.MutableLiveData -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.io.File import java.io.FileOutputStream import java.text.SimpleDateFormat @@ -30,6 +30,10 @@ import com.jieli.jl_audio_decode.callback.OnStateCallback import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.exceptions.OpusException import com.yunqiinnovation.ble_service.BleCompanionManager +import com.yunqiinnovation.ble_service.OTAManager +import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; +import com.jieli.jl_bt_ota.constant.ErrorCode; +import com.jieli.jl_bt_ota.constant.StateCode; /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -96,6 +100,8 @@ object BleService { private var opusManager: OpusManager? = null private val mainHandler = Handler(Looper.getMainLooper()) + // ota解码器实例 + // var otaManager: OTAManager? = null // 初始化状态 private var isInitialized = false @@ -111,6 +117,19 @@ object BleService { try { this.context = appContext.applicationContext + // otaManager = OTAManager(this.context).apply { + // // 设置数据回调 + // setDataCallback(object : OTAManager.DataCallback { + // override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { + // data?.let { + // Log.i("BleService", "收到数据:${it.size} 字节") + // Log.i("BleService", "认证交互数据${it.contentToString()}") + // // 移除次数限制,除非明确需要 + // otaManager?.onReceiveDeviceData(device, it) + // } + // } + // }) + // } // 初始化蓝牙管理器和适配器 bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -120,21 +139,21 @@ object BleService { // 初始化Handler scanHandler = Handler(Looper.getMainLooper()) - // 初始化OpusManager + // 初始化OpusManager和OTAManager try { opusManager = OpusManager() startOpusStreamDecoding() - FileLogger.d(TAG, "OpusManager初始化成功") + Log.d(TAG, "OpusManager初始化成功") } catch (e: OpusException) { - FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) + Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) // 根据需要决定是否因为Opus初始化失败而返回false } isInitialized = true - FileLogger.d(TAG, "BLE服务初始化成功") + Log.d(TAG, "BLE服务初始化成功") return true } catch (e: Exception) { - FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e) + Log.e(TAG, "BLE服务初始化失败: ${e.message}", e) return false } } @@ -147,7 +166,7 @@ object BleService { fun addCallback(callback: Callback): Boolean { if (!callbacks.contains(callback)) { callbacks.add(callback) - FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") + Log.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") return true } return false @@ -161,7 +180,7 @@ object BleService { fun removeCallback(callback: Callback): Boolean { val removed = callbacks.remove(callback) if (removed) { - FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") + Log.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") } return removed } @@ -171,7 +190,7 @@ object BleService { */ fun clearCallbacks() { callbacks.clear() - FileLogger.d(TAG, "已清除所有BLE回调") + Log.d(TAG, "已清除所有BLE回调") } // ====================================================================================================== @@ -184,7 +203,7 @@ object BleService { */ fun startScan(timeout: Long = 15000): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "BLE服务未初始化") + Log.e(TAG, "BLE服务未初始化") return false } @@ -192,7 +211,7 @@ object BleService { val scanner = bluetoothLeScanner ?: return false if (isScanning) return false - FileLogger.i(TAG, "开始主动扫描BLE设备...") + Log.i(TAG, "开始主动扫描BLE设备...") scanResults.clear() val filter = ScanFilter.Builder() @@ -213,13 +232,13 @@ object BleService { // 设置扫描超时 scanHandler.postDelayed({ - FileLogger.d(TAG, "扫描超时,停止扫描") + Log.d(TAG, "扫描超时,停止扫描") stopScan() }, timeout) return true } catch (e: Exception) { - FileLogger.e(TAG, "启动扫描失败: ${e.message}", e) + Log.e(TAG, "启动扫描失败: ${e.message}", e) return false } } @@ -233,7 +252,7 @@ object BleService { val scanner = bluetoothLeScanner ?: return false if (!isScanning) return false - FileLogger.i(TAG, "停止主动扫描") + Log.i(TAG, "停止主动扫描") try { scanner.stopScan(scanCallback) @@ -243,7 +262,7 @@ object BleService { return true } catch (e: Exception) { - FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) + Log.e(TAG, "停止扫描失败: ${e.message}", e) return false } } @@ -277,12 +296,12 @@ object BleService { val idx = scanResults.indexOfFirst { it.device.address == dev.address } if (idx >= 0) scanResults[idx] = result else scanResults.add(result) } catch (e: Exception) { - FileLogger.e(TAG, "上报扫描结果异常: ${e.message}", e) + Log.e(TAG, "上报扫描结果异常: ${e.message}", e) } } override fun onScanFailed(errorCode: Int) { - FileLogger.e(TAG, "扫描失败 error=$errorCode") + Log.e(TAG, "扫描失败 error=$errorCode") isScanning = false } } @@ -296,7 +315,7 @@ object BleService { val serviceUuids = scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" - FileLogger.d( + Log.d( TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" ) @@ -305,12 +324,12 @@ object BleService { val scanData = scanRecord?.bytes if (scanData != null) { val hexString = scanData.joinToString("") { String.format("%02X", it) } - FileLogger.d(TAG, "广播数据: $hexString") + Log.d(TAG, "广播数据: $hexString") // 解析服务数据 scanRecord.serviceData?.forEach { (uuid, data) -> val dataHex = data.joinToString("") { String.format("%02X", it) } - FileLogger.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") + Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") } } } @@ -326,32 +345,33 @@ object BleService { try { // 创建 BleCompanionManager 实例 val companionManager = BleCompanionManager(context) - + // 获取已配对的 MAC 地址 val pairedMac = companionManager.getPairedMacAddress() - + if (pairedMac.isNullOrEmpty()) { - FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") + Log.w(TAG, "未找到已配对的设备 MAC 地址") return } - FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") + Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") // 调用现有的 connect 方法 connect(pairedMac) } catch (e: Exception) { - FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) + Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e) updateConnectionState(BleConst.STATE_ERROR) } } - + + /** * 连接到指定地址的BLE设备 * @param address 设备MAC地址 */ fun connect(address: String) { if (connectionState.value == BleConst.STATE_CONNECTED) { - FileLogger.w(TAG, "已经连接中,忽略连接请求") + Log.w(TAG, "已经连接中,忽略连接请求") return } @@ -365,6 +385,8 @@ object BleService { disconnectGatt() try { + + // 创建新连接 val device = adapter.getRemoteDevice(address) updateConnectionState(BleConst.STATE_CONNECTING) @@ -372,8 +394,10 @@ object BleService { bluetoothGatt = device.connectGatt( context, false, gattCallback, BluetoothDevice.TRANSPORT_LE ) + + } catch (e: Exception) { - FileLogger.e(TAG, "连接设备失败: ${e.message}", e) + Log.e(TAG, "连接设备失败: ${e.message}", e) updateConnectionState(BleConst.STATE_ERROR) } } @@ -395,7 +419,7 @@ object BleService { gatt.disconnect() gatt.close() } catch (e: Exception) { - FileLogger.e(TAG, "关闭GATT连接异常: ${e.message}", e) + Log.e(TAG, "关闭GATT连接异常: ${e.message}", e) } bluetoothGatt = null notifyChar = null @@ -404,6 +428,45 @@ object BleService { } } + // fun startOTA() { + // try { + // otaManager?.startOTA(CustomUpdateCallback(context, null)) + // Log.i(TAG, "OTA 升级已启动") + // } catch (e: IllegalStateException) { + // Log.e(TAG, "OTA 启动失败: ${e.message}") + // disconnectGatt() + // } + // } + + // // 配置OTA + // fun configureOTA() { + // try { + // val firmwarePath = + // context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw" + // val file = File(firmwarePath) + // if (!file.exists()) { + // // throw IOException("Firmware file not found") + // Log.i(TAG, "Firmware file not found") + // } + + // val bluetoothOption = BluetoothOTAConfigure().apply { + // priority = BluetoothOTAConfigure.PREFER_BLE + // isUseReconnect = true + // isUseAuthDevice = true + // mtu = 512 + // isNeedChangeMtu = false // 确保MTU请求生效 + // isUseJLServer = false + // firmwareFilePath = firmwarePath + // } + // otaManager?.configure(bluetoothOption) ?: run { + // Log.e(TAG, "OTA Manager未初始化") + // } + // } catch (e: Exception) { + // Log.e(TAG, "OTA配置失败: ${e.message}") + // // 处理错误 + // } + // } + /** * 更新连接状态并通知回调 */ @@ -416,16 +479,37 @@ object BleService { * GATT回调 */ private val gattCallback = object : BluetoothGattCallback() { + + + // override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + // if (status == BluetoothGatt.GATT_SUCCESS) { + // Log.i(TAG, "MTU 更新成功: $mtu") + // configureOTA() + + // startOTA() + // } else { + // Log.e(TAG, "MTU 更新失败: status=$status") + // } + // } + override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { when { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { updateConnectionState(BleConst.STATE_CONNECTED) g.discoverServices() + // Log.i(TAG, "连接成功,开始 MTU 协商") + + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) + // g.requestMtu(512) // 触发 MTU 修改流程 + } newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) + // g.close() + // otaManager?.release(); } else -> { @@ -452,7 +536,7 @@ object BleService { audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) if (notifyChar == null || writeChar == null) { - FileLogger.e(TAG, "未找到主服务所需特征") + Log.e(TAG, "未找到主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) return } @@ -462,9 +546,9 @@ object BleService { // 设置音频服务的通知(如果存在) if (audioChar != null) { setupNotifications(g, audioChar) - FileLogger.i(TAG, "音频服务特征找到并设置通知") + Log.i(TAG, "音频服务特征找到并设置通知") } else { - FileLogger.w(TAG, "音频服务特征未找到") + Log.w(TAG, "音频服务特征未找到") } // 设置主服务的通知 setupNotifications(g, notifyChar) @@ -480,10 +564,12 @@ object BleService { when (c.uuid) { // 音频特征数据 BleConst.RECEIVE_AUDIO_CHAR_UUID -> { + // Log.i(TAG, "收到音频特征数据") processAudioData(data) } // 通知特征数据(命令和控制) BleConst.NOTIFY_CHAR_UUID -> { + // Log.i(TAG, "收到通知特征数据(命令和控制)") // 判断数据类型 when { // 设备响应 (0xBB开头) @@ -496,7 +582,7 @@ object BleService { } // 其他未知数据 else -> { - FileLogger.d( + Log.d( TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" ) @@ -505,7 +591,7 @@ object BleService { } // 其他未知特征 else -> { - FileLogger.d( + Log.d( TAG, "收到未知特征数据: ${c.uuid}, 数据=${ data.joinToString(", ") { @@ -520,7 +606,7 @@ object BleService { override fun onCharacteristicWrite( g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int ) { - FileLogger.i( + Log.i( TAG, "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" ) @@ -530,7 +616,7 @@ object BleService { g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) - FileLogger.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") + Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") } } @@ -539,7 +625,7 @@ object BleService { */ private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { - FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") + Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") return } @@ -558,10 +644,10 @@ object BleService { if (opusManager?.isDecodeStream == true) { opusManager?.writeAudioStream(data) } else { - // FileLogger.d(TAG, "Opus解码流未启动,忽略音频数据") + // Log.d(TAG, "Opus解码流未启动,忽略音频数据") } } catch (e: Exception) { - FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) + Log.e(TAG, "处理音频数据异常: ${e.message}", e) } } @@ -570,12 +656,12 @@ object BleService { * 处理设备响应数据 - 帧头为0xBB */ private fun processDeviceResponse(data: ByteArray) { - FileLogger.d( + Log.d( TAG, "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" ) if (data.size < 3) { - FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") + Log.e(TAG, "设备响应数据格式错误:数据长度过短") return } @@ -584,7 +670,7 @@ object BleService { // 判断命令失败响应(长度为0) if (data.size == 4 && data[2].toInt() and 0xFF == 0) { - FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") + Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") // 创建失败结果Map val resultMap = mapOf( "success" to false, @@ -600,7 +686,7 @@ object BleService { // 检查数据长度 if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } @@ -610,7 +696,7 @@ object BleService { val receivedCrc = data[data.size - 1].toInt() and 0xFF if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e( + Log.e( TAG, "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" ) @@ -623,7 +709,7 @@ object BleService { System.arraycopy(data, 3, responseData, 0, length) } - FileLogger.d( + Log.d( TAG, "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ responseData.joinToString(", ") { @@ -646,18 +732,22 @@ object BleService { if (responseData.isNotEmpty()) { // 转换为字符串前,确认是否是有效的ASCII字符 val version = - responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } + responseData + .map { it.toInt() and 0xFF } // 转为无符号整数 (0-255) + .dropWhile { it == 0xFF } // 仅丢弃开头的连续 255 + .joinToString(".") { it.toString() } resultMap["version"] = version - FileLogger.i(TAG, "接收到目标版本号: $version") + Log.i(TAG, "接收到目标版本号: $version") } notifyDeviceInfoReceived(cmdType, resultMap) + } BleConst.CMD_GET_PRODUCT_ID -> { val resultMap = mutableMapOf( "success" to true, - "command" to "productInfo", + "command" to "productId", "rawData" to responseData ) @@ -666,7 +756,7 @@ object BleService { val productId = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } resultMap["productId"] = productId - FileLogger.i(TAG, "接收到产品ID: $productId") + Log.i(TAG, "接收到产品ID: $productId") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -682,7 +772,7 @@ object BleService { if (responseData.isNotEmpty()) { val colorId = (responseData[0].toInt() and 0xFF).toString() resultMap["colorId"] = colorId - FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") + Log.i(TAG, "接收到耳机颜色ID: $colorId") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -719,7 +809,7 @@ object BleService { resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - FileLogger.i( + Log.i( TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + @@ -763,7 +853,7 @@ object BleService { resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc - FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") + Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -777,7 +867,7 @@ object BleService { "rawData" to responseData, "isUnknown" to true ) - FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") + Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") notifyDeviceInfoReceived(cmdType, resultMap) } } @@ -787,12 +877,12 @@ object BleService { * 处理设备主动上报数据 - 帧头为0xCC */ private fun processDeviceNotification(data: ByteArray) { - FileLogger.d( + Log.d( TAG, "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" ) if (data.size < 3) { - FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") + Log.e(TAG, "设备主动上报数据格式错误:数据长度过短") return } @@ -802,7 +892,7 @@ object BleService { // 检查数据长度 if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } @@ -812,7 +902,7 @@ object BleService { val receivedCrc = data[data.size - 1].toInt() and 0xFF if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e( + Log.e( TAG, "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" ) @@ -825,7 +915,7 @@ object BleService { System.arraycopy(data, 3, notifyData, 0, length) } - FileLogger.d( + Log.d( TAG, "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ notifyData.joinToString(", ") { @@ -870,7 +960,7 @@ object BleService { resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - FileLogger.i( + Log.i( TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + @@ -882,7 +972,7 @@ object BleService { } BleConst.CMD_WAKE_UP -> { - FileLogger.i(TAG, "收到唤醒指令") + Log.i(TAG, "收到唤醒指令") // 接收到唤醒信号,打开编码器 (设备侧) openEncoder() notifyWakeupSignalReceived() @@ -898,7 +988,7 @@ object BleService { "isUnknown" to true, "isNotification" to true // 标记为主动上报 ) - FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") + Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") notifyDeviceInfoReceived(infoType, resultMap) } } @@ -915,13 +1005,13 @@ object BleService { packetSize: Int = 40 // 确认设备端Opus编码帧长 ): Boolean { if (opusManager == null) { - FileLogger.e(TAG, "OpusManager未初始化,无法开始解码") + Log.e(TAG, "OpusManager未初始化,无法开始解码") return false } // 如果已经在解码流,先停止 if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() - FileLogger.d(TAG, "已停止正在进行的Opus解码流") + Log.d(TAG, "已停止正在进行的Opus解码流") } val option = OpusOption() @@ -930,29 +1020,29 @@ object BleService { .setSampleRate(sampleRate) .setPacketSize(packetSize) - FileLogger.d(TAG, "准备开始Opus数据流解码, 参数: $option") + Log.d(TAG, "准备开始Opus数据流解码, 参数: $option") var streamStartedSuccessfully = false opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { override fun onDecodeStream(data: ByteArray?) { if (data != null) { - // FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") + // Log.d(TAG, "Opus解码数据: ${data.size} bytes") notifyAudioDataReceived(data) } } override fun onStart() { streamStartedSuccessfully = true - FileLogger.i(TAG, "Opus数据流解码已开始") + Log.i(TAG, "Opus数据流解码已开始") // 可以通过回调通知上层解码已开始 } override fun onComplete(outPath: String?) { - FileLogger.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") + Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") } override fun onError(code: Int, message: String?) { - FileLogger.e(TAG, "Opus数据流解码错误: [$code] $message") + Log.e(TAG, "Opus数据流解码错误: [$code] $message") // 可以通过回调通知上层解码错误 } }) @@ -965,10 +1055,10 @@ object BleService { private fun stopOpusStreamDecoding(): Boolean { if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() - FileLogger.i(TAG, "已停止Opus数据流解码") + Log.i(TAG, "已停止Opus数据流解码") return true } - FileLogger.d(TAG, "Opus数据流未在解码或OpusManager未初始化") + Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化") return false } @@ -980,7 +1070,7 @@ object BleService { * 获取耳机版本号 */ fun getDeviceVersion(): Boolean { - FileLogger.i(TAG, "获取耳机版本号...") + Log.i(TAG, "获取耳机版本号...") return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) } @@ -988,7 +1078,7 @@ object BleService { * 获取产品ID */ fun getProductId(): Boolean { - FileLogger.i(TAG, "获取耳机产品ID...") + Log.i(TAG, "获取耳机产品ID...") return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) } @@ -996,7 +1086,7 @@ object BleService { * 获取耳机颜色ID */ fun getColorId(): Boolean { - FileLogger.i(TAG, "获取耳机颜色ID...") + Log.i(TAG, "获取耳机颜色ID...") return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) } @@ -1004,7 +1094,7 @@ object BleService { * 获取耳机电量信息 */ fun getBatteryInfo(): Boolean { - FileLogger.i(TAG, "获取耳机电量信息...") + Log.i(TAG, "获取耳机电量信息...") return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) } @@ -1012,7 +1102,7 @@ object BleService { * 控制编解码 - 关闭编解码 */ fun closeCodec(): Boolean { - FileLogger.i(TAG, "关闭编解码...") + Log.i(TAG, "关闭编解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), @@ -1028,7 +1118,7 @@ object BleService { * 控制编解码 - 打开解码 */ fun openDecoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") // val opusStarted = startOpusStreamDecoding( // hasHeader = false, // BLE传输的Opus数据通常无文件头 @@ -1041,7 +1131,7 @@ object BleService { // ) - FileLogger.i(TAG, "打开解码...") + Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CODEC_CONTROL_DECODE_ON.toByte(), @@ -1055,9 +1145,9 @@ object BleService { * 控制编解码 - 打开解码 */ fun openA2DPDecoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") - // FileLogger.i(TAG, "打开解码...") + // Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), @@ -1071,7 +1161,7 @@ object BleService { * 控制编解码 - A2DP播放 */ fun playA2DP(): Boolean { - FileLogger.i(TAG, "切换至A2DP播放模式...") + Log.i(TAG, "切换至A2DP播放模式...") return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) } @@ -1079,7 +1169,7 @@ object BleService { * 控制编解码 - 打开编码 */ fun openEncoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") // 目前仅发送命令通知设备开始编码。 @@ -1099,11 +1189,11 @@ object BleService { */ fun setChannelMode(channelMode: Int): Boolean { when (channelMode) { - BleConst.AUDIO_CHANNEL_LEFT -> FileLogger.i(TAG, "设置左声道模式...") - BleConst.AUDIO_CHANNEL_RIGHT -> FileLogger.i(TAG, "设置右声道模式...") - BleConst.AUDIO_CHANNEL_STEREO -> FileLogger.i(TAG, "设置立体声模式...") + BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...") + BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...") + BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...") else -> { - FileLogger.e(TAG, "无效的声道模式: $channelMode") + Log.e(TAG, "无效的声道模式: $channelMode") return false } } @@ -1121,7 +1211,7 @@ object BleService { * 音量增大 */ fun volumeUp(): Boolean { - FileLogger.i(TAG, "设备音量增加...") + Log.i(TAG, "设备音量增加...") return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) } @@ -1129,7 +1219,7 @@ object BleService { * 音量减小 */ fun volumeDown(): Boolean { - FileLogger.i(TAG, "设备音量减小...") + Log.i(TAG, "设备音量减小...") return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) } @@ -1183,7 +1273,7 @@ object BleService { it.release() } opusManager = null - FileLogger.d(TAG, "OpusManager已释放") + Log.d(TAG, "OpusManager已释放") isInitialized = false // 标记为未初始化 } @@ -1199,7 +1289,7 @@ object BleService { */ private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { if (!checkConn()) { - FileLogger.e(TAG, "发送命令失败: 设备未连接") + Log.e(TAG, "发送命令失败: 设备未连接") return false } @@ -1230,7 +1320,7 @@ object BleService { cmdPacket[3 + cmdLength] = crc // 记录命令详情 - FileLogger.d( + Log.d( TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + @@ -1241,7 +1331,7 @@ object BleService { writeChar!!.value = cmdPacket return bluetoothGatt?.writeCharacteristic(writeChar) ?: false } catch (e: Exception) { - FileLogger.e(TAG, "发送命令异常: ${e.message}", e) + Log.e(TAG, "发送命令异常: ${e.message}", e) return false } } @@ -1274,7 +1364,7 @@ object BleService { try { callback.onScanResult(devices) } catch (e: Exception) { - FileLogger.e(TAG, "分发扫描结果回调异常", e) + Log.e(TAG, "分发扫描结果回调异常", e) } } } @@ -1287,7 +1377,7 @@ object BleService { try { callback.onConnectionStateChanged(state) } catch (e: Exception) { - FileLogger.e(TAG, "分发连接状态回调异常", e) + Log.e(TAG, "分发连接状态回调异常", e) } } } @@ -1300,7 +1390,7 @@ object BleService { try { callback.onAudioDataReceived(data) } catch (e: Exception) { - FileLogger.e(TAG, "分发音频数据回调异常", e) + Log.e(TAG, "分发音频数据回调异常", e) } } } @@ -1313,7 +1403,7 @@ object BleService { try { callback.onWakeupSignalReceived() } catch (e: Exception) { - FileLogger.e(TAG, "分发唤醒信号回调异常", e) + Log.e(TAG, "分发唤醒信号回调异常", e) } } } @@ -1326,7 +1416,7 @@ object BleService { try { callback.onDeviceInfoReceived(infoType, infoData) } catch (e: Exception) { - FileLogger.e(TAG, "分发设备信息回调异常", e) + Log.e(TAG, "分发设备信息回调异常", e) } } } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 6ffe8d51e..68f15312a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -15,7 +15,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.PluginRegistry -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleServicePlugin @@ -52,8 +52,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, private var activity: android.app.Activity? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - FileLogger.init(flutterPluginBinding.applicationContext) - FileLogger.d(TAG, "BleServicePlugin onAttachedToEngine") + Log.d(TAG, "BleServicePlugin onAttachedToEngine") // 设置方法通道 methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service") @@ -150,14 +149,14 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, try { eventSink.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) + Log.e(TAG, "$errorPrefix: ${e.message}", e) } } else { mainHandler.post { try { eventSink.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) + Log.e(TAG, "$errorPrefix: ${e.message}", e) } } } @@ -175,7 +174,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - FileLogger.d(TAG, "Method call: ${call.method}") + Log.d(TAG, "Method call: ${call.method}") when (call.method) { "isConnected" -> { diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt index 57fb40298..78557535a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt @@ -5,7 +5,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleWakeReceiver @@ -15,8 +15,7 @@ class BleWakeReceiver : BroadcastReceiver() { private val TAG = "BleWakeReceiver" override fun onReceive(context: Context, intent: Intent) { - FileLogger.init(context) - FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}") + Log.i(TAG, "收到BLE扫描广播: ${intent.action}") if (intent.action == BleConst.ACTION_SCAN_RESULT) { // 从扫描结果中提取MAC地址 @@ -44,7 +43,7 @@ class BleWakeReceiver : BroadcastReceiver() { val serviceClass = try { Class.forName("com.yunqiinnovation.agent_service.BleMediaService") } catch (e: ClassNotFoundException) { - FileLogger.i(TAG, "BleMediaService不存在") + Log.i(TAG, "BleMediaService不存在") return } @@ -57,13 +56,13 @@ class BleWakeReceiver : BroadcastReceiver() { // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) - FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") + Log.i(TAG, "已启动前台服务: ${serviceClass.name}") } else { context.startService(serviceIntent) - FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") + Log.i(TAG, "已启动服务: ${serviceClass.name}") } } catch (e: Exception) { - FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) + Log.e(TAG, "启动BLE服务失败: ${e.message}", e) } } } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt new file mode 100644 index 000000000..b28149983 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt @@ -0,0 +1,116 @@ +package com.yunqiinnovation.ble_service +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothProfile +import android.content.Context +import com.jieli.jl_bt_ota.constant.ErrorCode; +import com.jieli.jl_bt_ota.constant.StateCode; +import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; +import com.jieli.jl_bt_ota.interfaces.BtEventCallback; +import com.jieli.jl_bt_ota.interfaces.IActionCallback; +import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; +import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; +import com.jieli.jl_bt_ota.model.base.BaseError; +import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; +import android.util.Log +import java.util.* + class CustomUpdateCallback( + val context: Context, + val device: BluetoothDevice? +) : IUpgradeCallback { + companion object { + private const val TAG = "JlOtaPlugin" + private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" + private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" + + } + + override fun onStartOTA() { + Log.d(TAG, "onStartOTA开始升级") + // viewModel.otaStateMLD.value = OTAStart(device) + } + + override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { + Log.d(TAG, "onNeedReconnect需要重新链接") + + } + + override fun onProgress(type: Int, progress: Float) { + Log.d(TAG, "onProgress正在进展") + //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) + } + + override fun onStopOTA() { + Log.d(TAG, "onStartOTA停止升级") + + } + + override fun onCancelOTA() { + Log.d(TAG, "onCancelOTA取消升级") + + } + + override fun onError(error: BaseError?) { + val errorMessage = error?.message ?: "Unknown error" + Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") + + // 如果需要打印完整错误对象信息(假设BaseError有重写toString()) + Log.d(TAG, "完整错误对象: ${error ?: "null"}") + } +} + +class OTAManager(context: Context) : BluetoothOTAManager(context) { + + + // 定义回调接口 + interface DataCallback { + fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) + } + // 保存回调的实例 + private var dataCallback: DataCallback? = null + + // 设置回调的方法 + fun setDataCallback(callback: DataCallback) { + this.dataCallback = callback + } + override fun getConnectedDevice(): BluetoothDevice? { + // 这里可以添加空实现的注释说明 + Log.i(TAG, "ota数据发送getConnectedDevice") + return null + } + + override fun getConnectedBluetoothGatt(): BluetoothGatt? { + // 这里可以添加空实现的注释说明 + Log.i(TAG, "ota数据发送getConnectedBluetoothGatt") + return null + } + + override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { + // 这里可以添加空实现的注释说明 + Log.i(TAG, "ota数据发送connectBluetoothDevice") + } + + override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { + // 这里可以添加空实现的注释说明 + Log.i(TAG, "ota数据发送disconnectBluetoothDevice") + } + + /** 数据发送方法(协议无关) */ + override fun sendDataToDevice( + bluetoothDevice: BluetoothDevice?, + bytes: ByteArray? + ): Boolean { + Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}") + // 触发回调,将数据回传给调用者 + dataCallback?.onDataReceived(bluetoothDevice, bytes) + // context.onReceiveDeviceData(device, data); + return true + } +} + diff --git a/local_plugins/ble_service/ios/Assets/ble_service-Info.plist b/local_plugins/ble_service/ios/Assets/ble_service-Info.plist deleted file mode 100644 index 9ea29bd4c..000000000 --- a/local_plugins/ble_service/ios/Assets/ble_service-Info.plist +++ /dev/null @@ -1,15 +0,0 @@ - - - - - NSBluetoothAlwaysUsageDescription - 需要蓝牙权限与设备进行通信和传输音频数据 - NSBluetoothPeripheralUsageDescription - 需要蓝牙权限与设备进行通信和传输音频数据 - UIBackgroundModes - - bluetooth-central - audio - - - \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift new file mode 100644 index 000000000..b4970c3ce --- /dev/null +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -0,0 +1,195 @@ +import Foundation +import CoreBluetooth +import os.log + +// 条件编译只导入在iOS 18+上可用的框架 +#if canImport(AccessorySetupKit) && swift(>=5.9) +import AccessorySetupKit +import UIKit +#endif + +class BleAccessorySetup: NSObject { + // 单例实现 + static let shared = BleAccessorySetup() + + // 基本属性 + private var bleService: Any? + private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") + + // iOS 18+ 特定属性和回调 + #if canImport(AccessorySetupKit) && swift(>=5.9) + @available(iOS 18.0, *) + class AccessorySessionWrapper { + var session: ASAccessorySession + + init() { + self.session = ASAccessorySession() + } + } + + var accessorySessionWrapper: Any? + + typealias DeviceDiscoveredCallback = (UUID) -> Void + private var onDeviceDiscovered: DeviceDiscoveredCallback? + #endif + + private override init() { + super.init() + } + + // 设置BLE服务实例 + func setBleService(_ service: Any) { + self.bleService = service + } + + // 设置设备发现回调 + func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + self.onDeviceDiscovered = callback + } + #endif + } + + // 初始化设备发现会话 + func initializeDiscoverySession() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info) + + // 创建会话 + accessorySessionWrapper = AccessorySessionWrapper() + + // 激活会话并设置事件处理 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + wrapper.session.activate(on: .main) { [weak self] event in + guard let self = self else { return } + + switch event.eventType { + case .activated: + os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) + + case .accessoryAdded: + if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { + os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } + + case .accessoryRemoved: + if let accessory = event.accessory { + os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,正确的事件类型为accessoryChanged + case .accessoryChanged: + if let accessory = event.accessory { + os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,失败事件类型已变更 + case .pickerSetupFailed: + if let error = event.error { + os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } + + default: + os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) + } + } + + // 检查现有授权设备 + if !wrapper.session.accessories.isEmpty { + os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) + + // 如果有现有设备,触发回调 + for accessory in wrapper.session.accessories { + if let uuid = accessory.bluetoothIdentifier { + os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } + } + } + } + } else { + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + } + #else + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + #endif + } + + // 显示设备选择器 + func showDevicePicker() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + os_log("显示iOS 18设备选择器", log: logger, type: .info) + + // 创建描述符和选择器项,这些API也需要iOS 18 + @available(iOS 18.0, *) + func createAndShowPicker() { + // 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor + let descriptor = ASDiscoveryDescriptor() + + // 设置过滤条件 + // 使用ASBluetoothCompanyIdentifier类型 + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID + descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID + + // 创建默认图像以防系统图标不可用 + let headphonesImage = UIImage(systemName: "headphones") ?? UIImage() + + // 根据WWDC24视频,正确的类名是ASPickerDisplayItem + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: headphonesImage, + descriptor: descriptor + ) + + // 显示选择器 + wrapper.session.showPicker(for: [displayItem]) { error in + if let error = error { + // 修复OSLog的错误用法 + os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } + } + } + + // 调用内部函数 + createAndShowPicker() + } else { + os_log("会话未初始化", log: logger, type: .error) + } + } else { + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + } + #else + os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + #endif + } + + // 处理发现的设备 + func handleDiscoveredAccessory(uuid: UUID) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + guard let service = bleService as? BleService else { + os_log("BLE服务未设置", log: logger, type: .error) + return + } + + // 获取中央管理器 + let centralManager = service.getCentralManager() + + // 通过UUID检索外设 + let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid]) + + if let peripheral = peripherals.first { + os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString) + service.connect(to: peripheral) + } else { + os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString) + } + } + #endif + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleConst.swift b/local_plugins/ble_service/ios/Classes/BleConst.swift index 3c551b120..41d31caa7 100644 --- a/local_plugins/ble_service/ios/Classes/BleConst.swift +++ b/local_plugins/ble_service/ios/Classes/BleConst.swift @@ -1,28 +1,30 @@ import Foundation import CoreBluetooth -/// BLE服务常量 -struct BleConst { +/** + * BLE服务常量 + */ +class BleConst { // ================== 协议相关UUID ================== - /// 主服务UUID - 0000ABC0-0000-1111-2222-123456789ABC + /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") - - /// 音频服务UUID - 00001801-0000-1000-8000-00805f9b34fb + + /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") - - /// 接收音频特征UUID - 0000ABC1-0000-1111-2222-123456789ABC + + /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") - /// 写入特征UUID - 0000ABC1-0000-1111-2222-123456789ABC + /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") - /// 通知特征UUID - 0000ABC2-0000-1111-2222-123456789ABC + /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") - /// 客户端特征配置描述符UUID - static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb") + /** 客户端特征配置描述符UUID */ + static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") - /// BLE扫描Action + /** BLE扫描Action */ static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" // ================== 连接状态 ================== @@ -33,58 +35,59 @@ struct BleConst { static let STATE_ERROR = 4 // ================== 广播厂商信息 ================== - /// 厂商ID - 0xABEF + /** 厂商ID - 文档中定义为0xABEF */ static let MANUFACTURER_ID: UInt16 = 0xABEF - /// 客户ID - 0xA001 + /** 客户ID - 文档中定义为0xA001 */ static let CUSTOMER_ID: UInt16 = 0xA001 - /// 产品ID - 0xA001 + /** 产品ID - 文档中定义为0xA001 */ static let PRODUCT_ID: UInt16 = 0xA001 - /// 蓝牙状态-未连接 + /** 蓝牙状态-未连接 */ static let BT_STATE_DISCONNECTED: UInt8 = 0x00 - /// 蓝牙状态-已连接 + /** 蓝牙状态-已连接 */ static let BT_STATE_CONNECTED: UInt8 = 0x0C // ================== 协议格式 ================== - /// APP请求帧头 + /** APP请求帧头 */ static let APP_REQUEST_HEADER: UInt8 = 0xAA - /// 设备响应帧头 + /** 设备响应帧头 */ static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB - /// 设备主动上报帧头 + /** 设备主动上报帧头 */ static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC // ================== 协议指令 (CMD) ================== - /// 获取耳机版本号 + /** 获取耳机版本号 */ static let CMD_GET_VERSION: UInt8 = 0x01 - /// 获取耳机产品ID + /** 获取耳机产品ID */ static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 - /// 获取耳机颜色ID + /** 获取耳机颜色ID */ static let CMD_GET_COLOR_ID: UInt8 = 0x03 - /// 获取耳机电量ID + /** 获取耳机电量ID */ static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 - /// APP控制编解码指令 + /** APP控制编解码指令 */ static let CMD_CONTROL_CODEC: UInt8 = 0x05 - /// 音量增大 + /** 音量增大 */ static let CMD_VOLUME_UP: UInt8 = 0x06 - /// 音量减小 + /** 音量减小 */ static let CMD_VOLUME_DOWN: UInt8 = 0x07 - /// A2DP播放 + /** A2DP播放 */ static let CMD_PLAY_A2DP: UInt8 = 0x08 - /// 唤醒指令 + + /** 唤醒指令 */ static let CMD_WAKE_UP: UInt8 = 0x11 // ================== 编解码控制值 ================== - /// 关闭编解码指令 + /** 关闭编解码指令 */ static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 - /// 打开解码指令 + /** 音乐或者通话远端声音 */ static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 - /// 通过A2DP播放 + /** mic和dac(音乐或者通话远端)声音 */ static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 - /// 打开编码指令 + /** 打开编码指令 */ static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 - /// 左声道 + /** 左声道 */ static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 - /// 右声道 + /** 右声道 */ static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 - /// 立体声 + /** 立体声 */ static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleDelegate.swift b/local_plugins/ble_service/ios/Classes/BleDelegate.swift deleted file mode 100644 index af104cb20..000000000 --- a/local_plugins/ble_service/ios/Classes/BleDelegate.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -/// BLE服务回调代理协议 -protocol BleDelegate: AnyObject { - /// 扫描结果回调 - /// - Parameter devices: 设备列表,每个设备包含id、name、rssi和uuids等信息 - func onScanResult(devices: [[String: Any]]) - - /// 连接状态改变回调 - /// - Parameter state: 连接状态,参考BleConst中的STATE_*常量 - func onConnectionStateChanged(state: Int) - - /// 音频数据接收回调 - /// - Parameter data: 接收到的音频数据 - func onAudioDataReceived(data: Data) - - /// 唤醒信号接收回调 - func onWakeupSignalReceived() - - /// 设备信息接收回调 - /// - Parameters: - /// - infoType: 信息类型 - /// - infoData: 信息数据 - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index 92f129d9a..a86acc11c 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -2,972 +2,455 @@ import Foundation import CoreBluetooth import os.log -/// BLE服务类 - 单例实现 -actor BleService { +@available(iOS 13.0, *) +class BleService: NSObject { // MARK: - 单例实现 static let shared = BleService() - // MARK: - 常量 - - /// 用于状态恢复的唯一标识符 - private let restoreIdentifier = "com.yunqiinnovation.ble_service.restore" - - private init() { - // 初始化连接状态 - connectionState = BleConst.STATE_DISCONNECTED + // MARK: - 回调代理协议 + protocol Callback: AnyObject { + // 扫描相关回调 + func onScanResult(devices: [[String: Any]]) - // 将通知处理移到非actor方法中 - setupNotifications() - } - - deinit { - // 移除通知观察者 - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } + // 连接状态相关回调 + func onConnectionStateChanged(state: Int) + + // 数据相关回调 + func onAudioDataReceived(data: Data) + + // 设备信息相关回调 + func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } // MARK: - 属性 - - /// 中央管理器 - private var centralManager: CBCentralManager? - - /// 当前连接的外设 - private var peripheral: CBPeripheral? - - /// 主服务特征 + private var centralManager: CBCentralManager! + private var peripheralDevice: CBPeripheral? private var writeCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic? private var audioCharacteristic: CBCharacteristic? - /// 连接状态 - private(set) var connectionState: Int { - didSet { - if oldValue != connectionState { - Task { await notifyConnectionStateChanged(state: connectionState) } - } - } - } - - /// 是否正在扫描 - private(set) var isScanning = false - - /// 扫描结果缓存 - private var scanResults = [CBPeripheral: [String: Any]]() + private var isScanning = false + private var delegate: Callback? + private var connectionState = BleConst.STATE_DISCONNECTED - /// 扫描超时任务 - private var scanTimeoutTask: Task? - - /// 是否已初始化 - private var isInitialized = false - - /// 回调代理列表 - 使用actor保证线程安全 - private var delegates = [BleDelegate]() - - /// 日志 private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") - /// 通知观察者 - private var notificationObservers = [NSObjectProtocol]() - - // MARK: - 通知设置 - - /// 设置应用生命周期通知 - private nonisolated func setupNotifications() { - // 使用nonisolated函数以避免actor隔离问题 - let becomeActiveObserver = NotificationCenter.default.addObserver( - forName: UIApplication.didBecomeActiveNotification, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { - await self.handleAppDidBecomeActive() - } - } + // MARK: - 初始化 + private override init() { + super.init() - let enterBackgroundObserver = NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { - await self.handleAppDidEnterBackground() - } - } + // 初始化中央管理器,提供恢复标识以支持后台恢复 + let options: [String: Any] = [ + CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", + CBCentralManagerOptionShowPowerAlertKey: true + ] - // 存储观察者以便后续移除 - notificationObservers = [becomeActiveObserver, enterBackgroundObserver] + centralManager = CBCentralManager(delegate: self, queue: nil, options: options) } // MARK: - 公共方法 - /// 初始化BLE服务 - /// - Returns: 是否初始化成功 - func initialize() -> Bool { - if isInitialized { - return true - } - - do { - // 初始化中央管理器 - 配置状态恢复选项 - let options: [String: Any] = [ - CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, - CBCentralManagerOptionShowPowerAlertKey: true - ] - - centralManager = CBCentralManager( - delegate: CBManagerHandler.shared, - queue: nil, - options: options - ) - - CBManagerHandler.shared.bleService = self - - isInitialized = true - os_log("BLE服务初始化成功", log: logger, type: .info) - return true - } catch { - os_log("BLE服务初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return false - } - } - - /// 添加回调代理 - /// - Parameter delegate: 要添加的代理 - /// - Returns: 是否添加成功 - func addDelegate(_ delegate: BleDelegate) -> Bool { - if !delegates.contains(where: { $0 === delegate }) { - delegates.append(delegate) - os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) - return true - } - return false - } - - /// 移除回调代理 - /// - Parameter delegate: 要移除的代理 - /// - Returns: 是否移除成功 - func removeDelegate(_ delegate: BleDelegate) -> Bool { - if let index = delegates.firstIndex(where: { $0 === delegate }) { - delegates.remove(at: index) - os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) - return true - } - return false + /// 设置回调代理 + func setDelegate(_ delegate: Callback) { + self.delegate = delegate } - /// 清除所有回调代理 - func clearDelegates() { - delegates.removeAll() - os_log("已清除所有BLE回调", log: logger, type: .debug) + /// 获取中央管理器 + func getCentralManager() -> CBCentralManager { + return centralManager } - /// 检查是否已连接 - /// - Returns: 是否已连接 - func isConnected() -> Bool { - return connectionState == BleConst.STATE_CONNECTED - } - - // MARK: - 扫描方法 - - /// 开始主动扫描BLE设备 - /// - Parameter timeout: 扫描超时时间(秒) - /// - Returns: 是否成功开始扫描 - func startScan(timeout: TimeInterval = 15.0) -> Bool { - if !isInitialized { - os_log("BLE服务未初始化", log: logger, type: .error) - return false + /// 开始扫描BLE设备 + func startScan() { + guard centralManager.state == .poweredOn else { + os_log("蓝牙未打开", log: logger, type: .error) + return } if isScanning { - return false + stopScan() } - guard let manager = centralManager, manager.state == .poweredOn else { - os_log("蓝牙未开启", log: logger, type: .error) - return false - } - - os_log("开始主动扫描BLE设备...", log: logger, type: .info) - unregisterBackgroundScan() // 避免双重扫描 - scanResults.removeAll() - - // 扫描选项 - 允许重复结果,以获取最新的RSSI值 - let options: [String: Any] = [ - CBCentralManagerScanOptionAllowDuplicatesKey: true - ] - - // 设置过滤服务UUID - let scanFilters = [ - // 如果需要过滤,添加CBUUID - // CBUUID(string: BleConst.PRIMARY_SERVICE_UUID.uuidString) - ] - - // 使用过滤器开始扫描 - manager.scanForPeripherals( - withServices: scanFilters.isEmpty ? nil : scanFilters, - options: options - ) - isScanning = true - // 设置扫描超时 - scanTimeoutTask?.cancel() - scanTimeoutTask = Task { [weak self] in - guard let self = self else { return } - - do { - try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) - if !Task.isCancelled { - await self.stopScan() - } - } catch { - // 任务被取消,忽略错误 - } - } + // 扫描特定服务的设备 + let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] + centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [ + CBCentralManagerScanOptionAllowDuplicatesKey: false + ]) - return true + os_log("开始扫描BLE设备", log: logger, type: .info) } - /// 停止主动扫描 - /// - Returns: 是否成功停止扫描 - func stopScan() -> Bool { - if !isScanning { - return false + /// 停止扫描 + func stopScan() { + if isScanning { + centralManager.stopScan() + isScanning = false + os_log("停止扫描BLE设备", log: logger, type: .info) } - - os_log("停止主动扫描", log: logger, type: .info) - - centralManager?.stopScan() - scanTimeoutTask?.cancel() - scanTimeoutTask = nil - isScanning = false - - // 重新注册后台扫描,保证唤醒能力 - registerBackgroundScan() - - return true - } - - /// 注册后台扫描 - /// - Returns: 是否成功注册 - func registerBackgroundScan() -> Bool { - // iOS中无法直接实现与Android相同的后台扫描唤醒功能 - os_log("iOS不支持直接的后台扫描唤醒,将使用状态保存和恢复功能", log: logger, type: .info) - return true - } - - /// 注销后台扫描 - /// - Returns: 是否成功注销 - func unregisterBackgroundScan() -> Bool { - // iOS没有直接的后台扫描注销机制 - os_log("iOS不支持直接的后台扫描注销", log: logger, type: .info) - return true } - // MARK: - 连接方法 - - /// 连接到指定地址的BLE设备 - /// - Parameter address: 设备UUID字符串 - func connect(address: String) async { - if connectionState == BleConst.STATE_CONNECTED { - os_log("已经连接中,忽略连接请求", log: logger, type: .warning) - return - } - - guard let central = centralManager, central.state == .poweredOn else { - updateConnectionState(BleConst.STATE_ERROR) + /// 连接到指定设备 + func connect(to peripheral: CBPeripheral) { + if connectionState == BleConst.STATE_CONNECTED || connectionState == BleConst.STATE_CONNECTING { + os_log("已连接或正在连接中", log: logger, type: .info) return } - // 关闭现有连接 - disconnectGatt() - - // 查找对应的外设 - if let uuid = UUID(uuidString: address), - let targetPeripheral = scanResults.keys.first(where: { $0.identifier == uuid }) { - // 如果在扫描结果中找到,直接连接 - os_log("从扫描结果中找到目标设备,开始连接...", log: logger, type: .debug) - await connectToPeripheral(targetPeripheral) - } else { - // 如果没有找到,尝试从已知设备中恢复 - os_log("尝试使用UUID恢复设备连接: %{public}@", log: logger, type: .debug, address) - if let uuid = UUID(uuidString: address) { - let peripherals = central.retrievePeripherals(withIdentifiers: [uuid]) - if let targetPeripheral = peripherals.first { - await connectToPeripheral(targetPeripheral) - } else { - os_log("未找到指定UUID的设备: %{public}@", log: logger, type: .error, address) - updateConnectionState(BleConst.STATE_ERROR) - } - } else { - os_log("无效的UUID格式: %{public}@", log: logger, type: .error, address) - updateConnectionState(BleConst.STATE_ERROR) - } - } - } - - /// 连接到指定外设 - /// - Parameter peripheral: 要连接的外设 - private func connectToPeripheral(_ peripheral: CBPeripheral) async { - self.peripheral = peripheral - peripheral.delegate = CBPeripheralHandler.shared - CBPeripheralHandler.shared.bleService = self updateConnectionState(BleConst.STATE_CONNECTING) - centralManager?.connect(peripheral, options: nil) + + peripheralDevice = peripheral + peripheralDevice?.delegate = self + centralManager.connect(peripheral, options: [ + CBConnectPeripheralOptionNotifyOnConnectionKey: true, + CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, + CBConnectPeripheralOptionNotifyOnNotificationKey: true + ]) } /// 断开当前连接 - func disconnect() async { - guard let peripheral = peripheral, let central = centralManager else { + func disconnect() { + guard let peripheral = peripheralDevice, + connectionState == BleConst.STATE_CONNECTED else { + os_log("未连接设备", log: logger, type: .info) return } updateConnectionState(BleConst.STATE_DISCONNECTING) - central.cancelPeripheralConnection(peripheral) + centralManager.cancelPeripheralConnection(peripheral) } - /// 安全关闭GATT连接 - private func disconnectGatt() { - if let peripheral = peripheral, let central = centralManager { - central.cancelPeripheralConnection(peripheral) - } - - peripheral = nil - writeCharacteristic = nil - notifyCharacteristic = nil - audioCharacteristic = nil - } - - /// 更新连接状态并通知回调 - private func updateConnectionState(_ state: Int) { - connectionState = state + /// 检查是否已连接 + func isConnected() -> Bool { + return connectionState == BleConst.STATE_CONNECTED } - // MARK: - 设备通信方法 + // MARK: - 获取设备信息 /// 获取设备版本号 - /// - Returns: 是否成功发送请求 func getDeviceVersion() -> Bool { return sendCommand(BleConst.CMD_GET_VERSION) } - ///获取产品ID - /// - Returns: 是否成功发送请求 + + /// 获取产品ID func getProductId() -> Bool { return sendCommand(BleConst.CMD_GET_PRODUCT_ID) } /// 获取电池信息 - /// - Returns: 是否成功发送请求 func getBatteryInfo() -> Bool { return sendCommand(BleConst.CMD_GET_BATTERY_INFO) } + // MARK: - 编解码控制 + /// 打开编码器 - /// - Returns: 是否成功发送请求 func openEncoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_ENCODE_ON) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_ENCODE_ON) } /// 打开解码器 - /// - Returns: 是否成功发送请求 func openDecoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_DECODE_ON) } - /// 打开解码器 - /// - Returns: 是否成功发送请求 + + /// 打开A2DP解码器 func openA2DPDecoder() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_A2DP_PLAY) } /// 关闭编解码器 - /// - Returns: 是否成功发送请求 func closeCodec() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_CLOSE) + return sendCommandWithParam(BleConst.CMD_CONTROL_CODEC, param: BleConst.CODEC_CONTROL_CLOSE) } /// 播放A2DP - /// - Returns: 是否成功发送请求 func playA2DP() -> Bool { - return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) + return sendCommand(BleConst.CMD_PLAY_A2DP) } - /// 发送编解码控制指令 - /// - Parameter control: 控制值 - /// - Returns: 是否成功发送 - private func sendCodecControl(_ control: UInt8) -> Bool { - let data = Data([BleConst.APP_REQUEST_HEADER, BleConst.CMD_CONTROL_CODEC, control]) - return writeData(data) - } + // MARK: - 私有方法 - /// 发送命令 - /// - Parameter command: 命令字 - /// - Returns: 是否成功发送 - private func sendCommand(_ command: UInt8) -> Bool { - let data = Data([BleConst.APP_REQUEST_HEADER, command]) - return writeData(data) + /// 更新连接状态并通知代理 + private func updateConnectionState(_ state: Int) { + connectionState = state + delegate?.onConnectionStateChanged(state: state) } - /// 写入数据 - /// - Parameter data: 要写入的数据 - /// - Returns: 是否成功写入 - private func writeData(_ data: Data) -> Bool { - guard let peripheral = peripheral, - let characteristic = writeCharacteristic, + /// 向设备发送命令(无参数) + private func sendCommand(_ cmd: UInt8) -> Bool { + guard let characteristic = writeCharacteristic, connectionState == BleConst.STATE_CONNECTED else { - os_log("无法写入数据,设备未连接或特征未找到", log: logger, type: .error) + os_log("设备未连接或写特征未找到", log: logger, type: .error) return false } - let hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: " ") - os_log("写入数据: %{public}@", log: logger, type: .debug, hexString) + // 构造帧: 帧头 + 命令ID + 长度(0) + 校验和 + let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x00, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x00]) - peripheral.writeValue(data, for: characteristic, type: .withoutResponse) + peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) return true } - // MARK: - 数据处理方法 - - /// 处理音频数据 - /// - Parameter data: 音频数据 - func processAudioData(_ data: Data) async { - os_log("收到音频数据: %d字节", log: logger, type: .debug, data.count) - await notifyAudioDataReceived(data: data) + /// 向设备发送带参数的命令 + private func sendCommandWithParam(_ cmd: UInt8, param: UInt8) -> Bool { + guard let characteristic = writeCharacteristic, + connectionState == BleConst.STATE_CONNECTED else { + os_log("设备未连接或写特征未找到", log: logger, type: .error) + return false + } + + // 构造帧: 帧头 + 命令ID + 长度(1) + 参数 + 校验和 + let frameData = Data([BleConst.APP_REQUEST_HEADER, cmd, 0x01, param, BleConst.APP_REQUEST_HEADER ^ cmd ^ 0x01 ^ param]) + + peripheralDevice?.writeValue(frameData, for: characteristic, type: .withResponse) + return true } - /// 处理设备响应 - /// - Parameter data: 设备响应数据 - func processDeviceResponse(_ data: Data) async { - guard data.count >= 2 else { return } + /// 处理接收到的响应数据 + private func handleResponseData(_ data: Data) { + guard data.count >= 4 else { + os_log("接收数据格式错误", log: logger, type: .error) + return + } + // 提取帧信息 + let header = data[0] let cmd = data[1] - os_log("收到设备响应: CMD=0x%02X", log: logger, type: .debug, cmd) + let length = data[2] + // 检查帧头,确保是设备响应 + guard header == BleConst.DEVICE_RESPONSE_HEADER else { + // 如果是设备主动上报 + if header == BleConst.DEVICE_NOTIFY_HEADER { + handleNotificationData(data) + } + return + } + + // 根据命令类型处理设备响应 switch cmd { case BleConst.CMD_GET_VERSION: - if data.count >= 3 { - // 解析版本号 - let version = String(format: "%d.%d.%d", data[2], data.count > 3 ? data[3] : 0, data.count > 4 ? data[4] : 0) - let infoData: [String: Any] = ["version": version] - await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) + if length > 0 && data.count >= 4 + Int(length) { + let versionData = data.subdata(in: 3..<(3 + Int(length))) + if let versionStr = String(data: versionData, encoding: .utf8) { + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_VERSION), + infoData: ["version": versionStr]) + } } + + case BleConst.CMD_GET_PRODUCT_ID: + if length >= 2 && data.count >= 5 { + let productId = UInt16(data[3]) << 8 | UInt16(data[4]) + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_PRODUCT_ID), + infoData: ["productId": productId]) + } + case BleConst.CMD_GET_BATTERY_INFO: - if data.count >= 3 { - // 解析电池电量 - let batteryLevel = Int(data[2]) - let infoData: [String: Any] = ["level": batteryLevel] - await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) + if length >= 1 && data.count >= 4 { + let batteryLevel = Int(data[3]) + delegate?.onDeviceInfoReceived(infoType: Int(BleConst.CMD_GET_BATTERY_INFO), + infoData: ["batteryLevel": batteryLevel]) } + default: - os_log("未处理的响应命令: 0x%02X", log: logger, type: .debug, cmd) + os_log("未知的响应命令: %{public}d", log: logger, type: .debug, cmd) } } - /// 处理设备通知 - /// - Parameter data: 设备通知数据 - func processDeviceNotification(_ data: Data) async { - guard data.count >= 2 else { return } + /// 处理设备主动上报的通知数据 + private func handleNotificationData(_ data: Data) { + guard data.count >= 4 else { return } - let notifyType = data[1] - os_log("收到设备通知: Type=0x%02X", log: logger, type: .debug, notifyType) + let cmd = data[1] + let length = data[2] - switch notifyType { + switch cmd { case BleConst.CMD_WAKE_UP: - // 设备唤醒信号 - await notifyWakeupSignalReceived() + // 处理唤醒指令 + os_log("收到设备唤醒指令", log: logger, type: .info) + // 可以在这里添加唤醒回调 + default: - os_log("未处理的通知类型: 0x%02X", log: logger, type: .debug, notifyType) + os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) } } - - // MARK: - 应用状态处理 - - /// 处理应用进入活跃状态 - private func handleAppDidBecomeActive() async { - os_log("应用进入活跃状态", log: logger, type: .debug) - // 重新初始化蓝牙管理器,如果需要 - if centralManager == nil { - _ = initialize() +} + +// MARK: - CBCentralManagerDelegate +@available(iOS 13.0, *) +extension BleService: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + case .poweredOn: + os_log("蓝牙已开启", log: logger, type: .info) + // 蓝牙已开启,可以开始扫描 + + case .poweredOff: + os_log("蓝牙已关闭", log: logger, type: .info) + updateConnectionState(BleConst.STATE_DISCONNECTED) + + case .resetting: + os_log("蓝牙正在重置", log: logger, type: .info) + + case .unauthorized: + os_log("蓝牙未授权", log: logger, type: .error) + + case .unsupported: + os_log("设备不支持蓝牙", log: logger, type: .error) + + case .unknown: + os_log("蓝牙状态未知", log: logger, type: .info) + + @unknown default: + os_log("蓝牙状态未定义", log: logger, type: .info) } } - /// 处理应用进入后台 - private func handleAppDidEnterBackground() async { - os_log("应用进入后台", log: logger, type: .debug) - // 在iOS中,后台蓝牙操作有一定限制,这里可以做一些清理工作 - } - - // MARK: - 回调通知方法 - - /// 通知扫描结果 - /// - Parameter devices: 设备列表 - func notifyScanResult(devices: [[String: Any]]) async { - let currentDelegates = delegates + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + // 将扫描结果转换为字典格式 + var deviceInfo: [String: Any] = [ + "name": peripheral.name ?? "Unknown", + "id": peripheral.identifier.uuidString, + "rssi": RSSI.intValue + ] - for delegate in currentDelegates { - await MainActor.run { - delegate.onScanResult(devices: devices) - } + // 添加广播数据 + if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { + deviceInfo["manufacturerData"] = manufacturerData } - } - - /// 通知连接状态变化 - /// - Parameter state: 连接状态 - func notifyConnectionStateChanged(state: Int) async { - let currentDelegates = delegates - for delegate in currentDelegates { - await MainActor.run { - delegate.onConnectionStateChanged(state: state) - } + if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { + deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString } } - } - - /// 通知音频数据接收 - /// - Parameter data: 音频数据 - func notifyAudioDataReceived(data: Data) async { - let currentDelegates = delegates - for delegate in currentDelegates { - await MainActor.run { - delegate.onAudioDataReceived(data: data) - } - } + // 报告给回调 + delegate?.onScanResult(devices: [deviceInfo]) } - /// 通知唤醒信号接收 - func notifyWakeupSignalReceived() async { - let currentDelegates = delegates + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.name ?? "Unknown") - for delegate in currentDelegates { - await MainActor.run { - delegate.onWakeupSignalReceived() - } - } - } - - /// 通知设备信息接收 - /// - Parameters: - /// - infoType: 信息类型 - /// - infoData: 信息数据 - func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) async { - let currentDelegates = delegates + updateConnectionState(BleConst.STATE_CONNECTED) - for delegate in currentDelegates { - await MainActor.run { - delegate.onDeviceInfoReceived(infoType: infoType, infoData: infoData) - } - } + // 查找服务 + peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) } - /// 处理扫描到的设备 - func handleDiscoveredPeripheral(_ peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) async { - // 打印外设信息 - os_log("扫描到设备: 名称=%{public}@, ID=%{public}@, RSSI=%d", log: logger, type: .debug, - peripheral.name ?? "未知", peripheral.identifier.uuidString, rssi.intValue) - - // 提取服务UUID - var uuids: [String] = [] - if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { - uuids = serviceUUIDs.map { $0.uuidString } - } - - // 创建设备信息字典 - let deviceInfo: [String: Any] = [ - "id": peripheral.identifier.uuidString, - "name": peripheral.name ?? "未知", - "rssi": rssi.intValue, - "uuids": uuids - ] - - // 更新扫描缓存 - scanResults[peripheral] = deviceInfo + func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + os_log("连接失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "Unknown error") - // 通知扫描结果 - await notifyScanResult(devices: [deviceInfo]) + updateConnectionState(BleConst.STATE_ERROR) } - /// 处理连接结果 - func handleConnectionResult(peripheral: CBPeripheral, success: Bool, error: Error?) async { - if success { - os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTED) - - // 开始服务发现 - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - } else { - os_log("连接设备失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "未知错误") - updateConnectionState(BleConst.STATE_ERROR) - - // 清理资源 - self.peripheral = nil - } + func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + os_log("设备断开连接: %{public}@", log: logger, type: .info, error?.localizedDescription ?? "Normal disconnection") + + updateConnectionState(BleConst.STATE_DISCONNECTED) } - /// 处理断开连接 - func handleDisconnection(peripheral: CBPeripheral, error: Error?) async { - if let error = error { - os_log("设备连接断开(错误): %{public}@", log: logger, type: .error, error.localizedDescription) - updateConnectionState(BleConst.STATE_ERROR) - } else { - os_log("设备连接断开(正常)", log: logger, type: .info) - updateConnectionState(BleConst.STATE_DISCONNECTED) - } + func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + os_log("恢复先前的连接状态", log: logger, type: .info) - // 清理资源 - if self.peripheral?.identifier == peripheral.identifier { - self.peripheral = nil - self.writeCharacteristic = nil - self.notifyCharacteristic = nil - self.audioCharacteristic = nil + // 从恢复状态中获取之前连接的外设 + if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral], + let peripheral = peripherals.first { + peripheralDevice = peripheral + peripheralDevice?.delegate = self + + // 如果之前已连接,更新状态 + if peripheral.state == .connected { + updateConnectionState(BleConst.STATE_CONNECTED) + // 重新发现服务 + peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) + } else { + // 如果之前未连接,尝试重新连接 + central.connect(peripheral, options: nil) + } } } - - /// 处理发现服务 - func handleDiscoveredServices(peripheral: CBPeripheral, error: Error?) async { +} + +// MARK: - CBPeripheralDelegate +@available(iOS 13.0, *) +extension BleService: CBPeripheralDelegate { + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { - os_log("发现服务失败: %{public}@", log: logger, type: .error, error.localizedDescription) - updateConnectionState(BleConst.STATE_ERROR) + os_log("服务发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let services = peripheral.services else { - os_log("没有发现任何服务", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - return - } + guard let services = peripheral.services else { return } - // 发现所有服务的特征 for service in services { os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString) - peripheral.discoverCharacteristics(nil, for: service) + + if service.uuid == BleConst.PRIMARY_SERVICE_UUID { + // 主服务,发现写入和通知特征 + peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) + } else if service.uuid == BleConst.AUDIO_SERVICE_UUID { + // 音频服务,发现音频特征 + peripheral.discoverCharacteristics([BleConst.RECEIVE_AUDIO_CHAR_UUID], for: service) + } } } - /// 处理发现的特征 - func handleDiscoveredCharacteristics(for service: CBService, peripheral: CBPeripheral, error: Error?) async { + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { - os_log("发现特征失败: %{public}@", log: logger, type: .error, error.localizedDescription) + os_log("特征发现错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let characteristics = service.characteristics else { - os_log("服务没有任何特征: %{public}@", log: logger, type: .warning, service.uuid.uuidString) - return - } + guard let characteristics = service.characteristics else { return } - // 根据UUID识别和保存特征 for characteristic in characteristics { os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - // 主服务特征 - if service.uuid == BleConst.PRIMARY_SERVICE_UUID { - if characteristic.uuid == BleConst.WRITE_CHAR_UUID { - writeCharacteristic = characteristic - os_log("找到写入特征", log: logger, type: .debug) - } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - notifyCharacteristic = characteristic - os_log("找到通知特征", log: logger, type: .debug) - // 设置通知 - await setupNotification(for: characteristic, peripheral: peripheral) - } - } - - // 音频服务特征 - if service.uuid == BleConst.AUDIO_SERVICE_UUID { - if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - audioCharacteristic = characteristic - os_log("找到音频特征", log: logger, type: .debug) - // 设置通知 - await setupNotification(for: characteristic, peripheral: peripheral) - } + if characteristic.uuid == BleConst.WRITE_CHAR_UUID { + writeCharacteristic = characteristic + } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { + notifyCharacteristic = characteristic + // 订阅通知 + peripheral.setNotifyValue(true, for: characteristic) + } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { + audioCharacteristic = characteristic + // 订阅音频通知 + peripheral.setNotifyValue(true, for: characteristic) } } - - // 检查是否找到所有必要的特征 - if writeCharacteristic == nil || notifyCharacteristic == nil { - os_log("未找到所有必要的特征", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - } } - /// 设置特征的通知 - /// - Parameters: - /// - characteristic: 特征 - /// - peripheral: 外设 - private func setupNotification(for characteristic: CBCharacteristic, peripheral: CBPeripheral) async { - if characteristic.properties.contains(.notify) { - peripheral.setNotifyValue(true, for: characteristic) - } - } - - /// 处理特征值更新 - func handleValueUpdate(for characteristic: CBCharacteristic, peripheral: CBPeripheral, error: Error?) async { + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("特征值更新失败: %{public}@", log: logger, type: .error, error.localizedDescription) + os_log("接收数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) return } - guard let data = characteristic.value else { - os_log("特征没有值", log: logger, type: .warning) - return - } + guard let data = characteristic.value, !data.isEmpty else { return } - let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ") - os_log("收到特征数据: %{public}@ - %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString, hexString) - - // 根据特征UUID处理不同类型的数据 - if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - // 音频数据处理 - await processAudioData(data) - } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - // 通知特征处理 - if !data.isEmpty { - switch data[0] { - case BleConst.DEVICE_RESPONSE_HEADER: - // 设备响应 - await processDeviceResponse(data) - case BleConst.DEVICE_NOTIFY_HEADER: - // 设备通知 - await processDeviceNotification(data) - default: - os_log("未知数据类型: %d", log: logger, type: .debug, data[0]) - } - } - } - } - - // MARK: - 状态恢复处理 - - /// 处理蓝牙状态恢复 - func handleStateRestoration(dict: [String: Any]) async { - os_log("正在恢复蓝牙状态...", log: logger, type: .info) - - // 1. 恢复之前连接的外设 - if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { - os_log("恢复 %d 个已连接的外设", log: logger, type: .debug, peripherals.count) - - for peripheral in peripherals { - self.peripheral = peripheral - peripheral.delegate = CBPeripheralHandler.shared - CBPeripheralHandler.shared.bleService = self - - // 根据外设当前状态采取不同操作 - if peripheral.state == .connected { - os_log("外设 %{public}@ 已连接", log: logger, type: .debug, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTED) - - // 重新发现服务 - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - } else if peripheral.state == .connecting { - os_log("外设 %{public}@ 正在连接中", log: logger, type: .debug, peripheral.identifier.uuidString) - updateConnectionState(BleConst.STATE_CONNECTING) - } else { - os_log("外设 %{public}@ 状态异常: %d", log: logger, type: .debug, peripheral.identifier.uuidString, peripheral.state.rawValue) - // 尝试重新连接 - updateConnectionState(BleConst.STATE_CONNECTING) - centralManager?.connect(peripheral, options: nil) - } - } - } - - // 2. 恢复扫描状态 - if let scanServices = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { - os_log("恢复扫描任务,服务UUID数: %d", log: logger, type: .debug, scanServices.count) - - let scanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] ?? [ - CBCentralManagerScanOptionAllowDuplicatesKey: true - ] - - // 重新启动扫描任务 - centralManager?.scanForPeripherals( - withServices: scanServices.isEmpty ? nil : scanServices, - options: scanOptions - ) - - isScanning = true - - // 设置新的扫描超时(默认15秒) - scanTimeoutTask?.cancel() - scanTimeoutTask = Task { [weak self] in - guard let self = self else { return } - - do { - try await Task.sleep(nanoseconds: UInt64(15.0 * 1_000_000_000)) - if !Task.isCancelled { - await self.stopScan() - } - } catch { - // 任务被取消,忽略错误 - } - } - } - - // 通知外部:BLE状态已恢复 - await notifyStateRestored() - } - - /// 通知状态恢复 - private func notifyStateRestored() async { - let currentDelegates = delegates - - for delegate in currentDelegates { - if let stateDelegate = delegate as? BleStateRestorationDelegate { - await MainActor.run { - stateDelegate.onBleStateRestored() - } - } - } - } -} - -// MARK: - 中央管理器委托处理器 - -class CBManagerHandler: NSObject, CBCentralManagerDelegate { - static let shared = CBManagerHandler() - weak var bleService: BleService? - - func centralManagerDidUpdateState(_ central: CBCentralManager) { - Task { - guard let service = bleService else { return } - - switch central.state { - case .poweredOn: - await MainActor.run { - os_log("蓝牙已开启", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) - } - case .poweredOff: - await MainActor.run { - os_log("蓝牙已关闭", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .warning) - } - await service.updateConnectionState(BleConst.STATE_DISCONNECTED) - case .unauthorized, .unsupported: - await MainActor.run { - os_log("蓝牙不可用: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, central.state.rawValue) - } - await service.updateConnectionState(BleConst.STATE_ERROR) - default: - await MainActor.run { - os_log("蓝牙状态变化: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, central.state.rawValue) - } - } - } - } - - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - Task { - await bleService?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: RSSI) - } - } - - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { - await bleService?.handleConnectionResult(peripheral: peripheral, success: true, error: nil) - } - } - - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - Task { - await bleService?.handleConnectionResult(peripheral: peripheral, success: false, error: error) - } - } - - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - Task { - await bleService?.handleDisconnection(peripheral: peripheral, error: error) - } - } - - /// 状态恢复处理 - func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - os_log("蓝牙状态恢复被触发", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) - - // 打印恢复信息 - if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { - os_log("恢复外设数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, peripherals.count) - for (i, peripheral) in peripherals.enumerated() { - os_log("恢复外设[%d]: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, i, peripheral.identifier.uuidString) - } - } - - if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { - os_log("恢复扫描服务数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, services.count) - } - - // 异步处理状态恢复 - Task { - if let service = bleService { - await service.handleStateRestoration(dict: dict) - } - } - } -} - -// MARK: - 外设委托处理器 - -class CBPeripheralHandler: NSObject, CBPeripheralDelegate { - static let shared = CBPeripheralHandler() - weak var bleService: BleService? - - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - Task { - await bleService?.handleDiscoveredServices(peripheral: peripheral, error: error) - } - } - - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - Task { - await bleService?.handleDiscoveredCharacteristics(for: service, peripheral: peripheral, error: error) - } - } - - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - Task { - await bleService?.handleValueUpdate(for: characteristic, peripheral: peripheral, error: error) + if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { + // 处理通知数据 + handleResponseData(data) + } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { + // 处理音频数据 + delegate?.onAudioDataReceived(data: data) } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("写入特征值失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) - } else { - os_log("写入特征值成功", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug) + os_log("写入数据错误: %{public}@", log: logger, type: .error, error.localizedDescription) } } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error { - os_log("设置通知状态失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) - } else { - os_log("设置通知状态成功: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, characteristic.uuid.uuidString) + os_log("订阅通知错误: %{public}@", log: logger, type: .error, error.localizedDescription) + return } + + os_log("特征 %{public}@ 的通知状态更新为 %{public}d", log: logger, type: .debug, + characteristic.uuid.uuidString, characteristic.isNotifying ? 1 : 0) } -} - -// MARK: - BLE状态恢复代理 -/// 蓝牙状态恢复代理 -protocol BleStateRestorationDelegate: AnyObject { - /// 当蓝牙状态被系统恢复时调用 - func onBleStateRestored() -} - -// MARK: - BLE代理 -/// 蓝牙事件代理 -protocol BleDelegate: AnyObject { - /// 扫描结果回调 - func onScanResult(devices: [[String: Any]]) - - /// 连接状态变化回调 - func onConnectionStateChanged(state: Int) - - /// 音频数据接收回调 - func onAudioDataReceived(data: Data) - - /// 唤醒信号接收回调 - func onWakeupSignalReceived() - - /// 设备信息接收回调 - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleServicePlugin.h b/local_plugins/ble_service/ios/Classes/BleServicePlugin.h index e28dfaa93..af92be7c5 100644 --- a/local_plugins/ble_service/ios/Classes/BleServicePlugin.h +++ b/local_plugins/ble_service/ios/Classes/BleServicePlugin.h @@ -1,4 +1,4 @@ #import - + @interface BleServicePlugin : NSObject @end \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift index e3004c43c..388e3bd92 100644 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift @@ -1,140 +1,257 @@ import Flutter import UIKit +import CoreBluetooth +import os -public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { - private let TAG = "BleServicePlugin" - - // 方法通道 - private var methodChannel: FlutterMethodChannel? - - // 事件通道 +@available(iOS 13.0, *) +public class SwiftBleServicePlugin: NSObject, FlutterPlugin { + // MARK: - 属性 + private var methodChannel: FlutterMethodChannel private var dataEventSink: FlutterEventSink? private var statusEventSink: FlutterEventSink? - // BLE服务实例 - private let bleService = BleService.shared - - public static func register(with registrar: FlutterPluginRegistrar) { - let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) - - // 创建插件实例 - let instance = SwiftBleServicePlugin() - instance.methodChannel = methodChannel - - // 设置方法通道处理器 - registrar.addMethodCallDelegate(instance, channel: methodChannel) - - // 设置事件通道 - instance.setupEventChannels(with: registrar) + // MARK: - 初始化方法 + init(_ channel: FlutterMethodChannel) { + self.methodChannel = channel + super.init() - // 初始化BLE服务 - _ = instance.bleService.initialize() + // 设置BLE服务回调 + BleService.shared.setDelegate(self) - // 添加自身作为BLE回调 - _ = instance.bleService.addDelegate(instance) + // 初始化AccessorySetup + BleAccessorySetup.shared.setBleService(BleService.shared) + BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in + self?.handleDiscoveredDevice(uuid: uuid) + } + BleAccessorySetup.shared.initializeDiscoverySession() } - /// 设置事件通道 - private func setupEventChannels(with registrar: FlutterPluginRegistrar) { - // 数据接收事件通道 - let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) - dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in - self?.dataEventSink = sink - } onCancel: { [weak self] in - self?.dataEventSink = nil - }) + // MARK: - FlutterPlugin Protocol + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) + let instance = SwiftBleServicePlugin(channel) + registrar.addMethodCallDelegate(instance, channel: channel) - // 状态更新事件通道 + // 注册事件通道 + let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) - statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in - self?.statusEventSink = sink - } onCancel: { [weak self] in - self?.statusEventSink = nil - }) - } - - /// 发送事件到Flutter - private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) { - DispatchQueue.main.async { - eventSink?(data) - } + + dataEventChannel.setStreamHandler(instance) + statusEventChannel.setStreamHandler(instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - NSLog("Method call: \(call.method)") - switch call.method { case "isConnected": - result(bleService.isConnected()) + result(BleService.shared.isConnected()) - case "getBatteryInfo": - result(bleService.getBatteryInfo()) + case "getBatteryLevel": + // 获取电池信息,通过回调返回结果 + if BleService.shared.getBatteryInfo() { + result(0) // 通过回调返回实际值 + } else { + result(FlutterError(code: "UNAVAILABLE", + message: "无法获取电池信息", + details: nil)) + } + + case "isCompanionSupported": + // 检查设备是否支持iOS 18的AccessorySetupKit功能 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + // 设备运行iOS 18+,支持AccessorySetupKit + result(true) + } else { + // 设备运行iOS 17或更低版本 + result(false) + } + #else + // 编译时没有AccessorySetupKit框架 + result(false) + #endif + + case "getPairedMacAddress": + // 获取已配对设备的标识符 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { + // 检查是否有配对设备 + if let firstAccessory = wrapper.session.accessories.first, + let btId = firstAccessory.bluetoothIdentifier { + // 在iOS中,我们返回UUID字符串作为"MAC地址"的等价物 + result(btId.uuidString) + } else { + // 无配对设备 + result(nil) + } + } else { + // 会话未初始化 + result(nil) + } + } else { + // iOS版本太低 + result(nil) + } + #else + // 编译环境不支持 + result(nil) + #endif + + case "registerBackgroundScan": + // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + // 在iOS 18+上,不需要额外操作 + result(true) + } else { + // iOS版本太低 + result(false) + } + #else + result(false) + #endif + + case "unregisterBackgroundScan": + // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销 + result(true) + + case "clearAssociations": + // 清除设备关联 + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { + // 遍历所有配对设备 + for accessory in wrapper.session.accessories { + if let btId = accessory.bluetoothIdentifier { + // 移除设备的系统处理已经集成在设置中,这里什么都不做 + os_log("设备需要在系统设置中手动移除: %{public}@", + log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), + type: .info, + btId.uuidString) + } + } + } + // 返回成功 + result(true) + } else { + // iOS版本太低 + result(false) + } + #else + result(false) + #endif case "getDeviceVersion": - result(bleService.getDeviceVersion()) + result(BleService.shared.getDeviceVersion()) case "getProductId": - result(bleService.getProductId()) + result(BleService.shared.getProductId()) + + case "getBatteryInfo": + result(BleService.shared.getBatteryInfo()) case "openEncoder": - result(bleService.openEncoder()) + result(BleService.shared.openEncoder()) case "openDecoder": - result(bleService.openDecoder()) - + result(BleService.shared.openDecoder()) + case "openA2DPDecoder": - result(bleService.openA2DPDecoder()) - + result(BleService.shared.openA2DPDecoder()) + case "closeCodec": - result(bleService.closeCodec()) + result(BleService.shared.closeCodec()) case "playA2DP": - result(bleService.playA2DP()) - - case "registerBackgroundScan": - result(bleService.registerBackgroundScan()) - - case "unregisterBackgroundScan": - result(bleService.unregisterBackgroundScan()) + result(BleService.shared.playA2DP()) - case "startBleService": - guard let config = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil)) - return + // iOS 18+ AccessorySetupKit相关方法 + case "startDeviceAssociation": + #if canImport(AccessorySetupKit) + if #available(iOS 18.0, *) { + BleAccessorySetup.shared.showDevicePicker() + result(true) + } else { + result(FlutterError(code: "UNAVAILABLE", + message: "AccessorySetupKit需要iOS 18或更高版本", + details: nil)) } - - // 处理配置参数 - if let address = config["mac"] as? String { - // 如果提供了设备地址,尝试连接 - bleService.connect(address: address) - } - - // 开始扫描,寻找设备 - let scanSuccess = bleService.startScan() - result(scanSuccess) - - case "stopBleService": - // 停止扫描和断开连接 - _ = bleService.stopScan() - if bleService.isConnected() { - bleService.disconnect() - } - result(true) + #else + result(FlutterError(code: "UNAVAILABLE", + message: "编译环境不支持AccessorySetupKit", + details: nil)) + #endif default: result(FlutterMethodNotImplemented) } } - // MARK: - BleDelegate Methods + // MARK: - 私有方法 + private func handleDiscoveredDevice(uuid: UUID) { + BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid) + } - func onScanResult(devices: [[String: Any]]) { - // 目前Flutter层没有直接使用此事件,可以选择不转发 + private func sendEvent(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) { + if let eventSink = eventSink { + DispatchQueue.main.async { + eventSink(data) + } + } + } +} + +// MARK: - FlutterStreamHandler +@available(iOS 13.0, *) +extension SwiftBleServicePlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + if let args = arguments as? [String: Any], let type = args["type"] as? String { + if type == "data" { + dataEventSink = events + } else if type == "status" { + statusEventSink = events + } + } else { + // 默认行为:根据注册的通道来确定事件类型 + if arguments == nil { + // 如果无参数,根据函数被调用的顺序确定 + if dataEventSink == nil { + dataEventSink = events + } else { + statusEventSink = events + } + } + } + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + if let args = arguments as? [String: Any], let type = args["type"] as? String { + if type == "data" { + dataEventSink = nil + } else if type == "status" { + statusEventSink = nil + } + } else { + // 同时清除两个sink + dataEventSink = nil + statusEventSink = nil + } + return nil + } +} + +// MARK: - BleService.Callback +@available(iOS 13.0, *) +extension SwiftBleServicePlugin: BleService.Callback { + func onScanResult(devices: [[String : Any]]) { + sendEvent(statusEventSink, + data: ["type": "scanResult", "devices": devices], + errorPrefix: "发送扫描结果异常") } func onConnectionStateChanged(state: Int) { - // 将连接状态变化通过状态通道发送 - let stateStr: String + var stateStr = "Unknown" switch state { case BleConst.STATE_DISCONNECTED: stateStr = "Disconnected" @@ -156,54 +273,29 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { "stateStr": stateStr ] - sendEvent(statusEventSink, stateMap) + sendEvent(statusEventSink, data: stateMap, errorPrefix: "发送连接状态异常") } func onAudioDataReceived(data: Data) { - // 将音频数据通过数据通道发送 let dataMap: [String: Any] = [ "type": "audioData", - "data": FlutterStandardTypedData(bytes: data) + "data": data ] - sendEvent(dataEventSink, dataMap) - } - - func onWakeupSignalReceived() { - // 将唤醒事件通过状态通道发送 - sendEvent(statusEventSink, ["type": "wakeup"]) + sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常") } - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - // 将设备信息通过状态通道发送 - var resultMap = infoData - resultMap["type"] = "deviceInfo" - resultMap["result"] = infoType + func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) { + var dataMap: [String: Any] = [ + "type": "deviceInfo", + "infoType": infoType + ] - sendEvent(statusEventSink, resultMap) - } -} - -// MARK: - 事件处理器类 - -/// 简化的事件处理器类 -class StreamHandler: NSObject, FlutterStreamHandler { - private let onListen: (FlutterEventSink) -> Void - private let onCancel: () -> Void - - init(onListen: @escaping (FlutterEventSink) -> Void, onCancel: @escaping () -> Void) { - self.onListen = onListen - self.onCancel = onCancel - super.init() - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - onListen(events) - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - onCancel() - return nil + // 合并infoData + for (key, value) in infoData { + dataMap[key] = value + } + + sendEvent(statusEventSink, data: dataMap, errorPrefix: "发送设备信息异常") } } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/README.md b/local_plugins/ble_service/ios/README.md new file mode 100644 index 000000000..c5cfd687c --- /dev/null +++ b/local_plugins/ble_service/ios/README.md @@ -0,0 +1,87 @@ +# ble_service iOS 集成说明 + +## 概述 + +该插件提供了蓝牙低功耗(BLE)设备的连接和通信功能,特别支持BLE耳机设备的音频数据传输。 + +- 支持 iOS 13.0+ 的基础蓝牙连接功能 +- 支持 iOS 18.0+ 的 AccessorySetupKit 集成,实现应用后台唤醒 + +## 集成步骤 + +### 1. 权限配置 + +在应用的 `Info.plist` 中添加以下权限描述: + +```xml + +NSBluetoothAlwaysUsageDescription +需要使用蓝牙连接您的耳机设备 + + +NSBluetoothPeripheralUsageDescription +需要使用蓝牙连接您的耳机设备 + + +UIBackgroundModes + + bluetooth-central + +``` + +### 2. iOS 18+ AccessorySetupKit 配置 + +如果需要支持 iOS 18+ 的 AccessorySetupKit 功能(应用后台唤醒),还需要在 `Info.plist` 中添加: + +```xml + +NSAccessorySetupBluetoothCompanyIdentifiers + + ABEF + + +NSAccessorySetupBluetoothServices + + 0000ABC0-0000-1111-2222-123456789ABC + 0000AE00-0000-1000-8000-00805F9B34FB + + +NSAccessorySetupKitSupports + + Bluetooth + +``` + +### 3. 集成到 Runner 项目 + +1. 确保项目的最低部署版本为 iOS 13.0 + +2. 在 Xcode 中编辑 `Runner` 项目的 `info.plist`,添加上述配置 + +3. 如需自定义 UUID 或厂商 ID,请同时修改 `BleConst.swift` 文件中的对应常量 + +## 使用说明 + +### 基本流程 + +1. 首次使用时,应用需要在前台扫描并连接设备 +2. 配对成功后,后续可通过 AccessorySetupKit (iOS 18+) 自动唤醒应用 + +### 支持的命令 + +- 获取设备版本: `getDeviceVersion()` +- 获取电池信息: `getBatteryInfo()` +- 打开/关闭编解码: `openEncoder()`, `openDecoder()`, `closeCodec()` +- A2DP 控制: `playA2DP()` + +## 后台运行说明 + +1. 该插件支持在后台接收 BLE 通知和数据 +2. iOS 18+ 设备上,支持应用在后台/被杀死状态下被设备唤醒 +3. 后台保活和状态恢复由系统管理,无需额外代码 + +## 注意事项 + +- 使用前确保已在用户设备上获得蓝牙权限 +- 由于苹果限制,手动杀死应用后,仍需用户主动重新打开一次应用才能完全激活后台唤醒 +- 厂商 ID 和服务 UUID 需要与实际设备匹配 \ No newline at end of file diff --git a/local_plugins/ble_service/ios/ble_service.podspec b/local_plugins/ble_service/ios/ble_service.podspec index 72f1802e5..cad3ddffb 100644 --- a/local_plugins/ble_service/ios/ble_service.podspec +++ b/local_plugins/ble_service/ios/ble_service.podspec @@ -1,21 +1,23 @@ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint ble_service.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'ble_service' s.version = '0.0.1' - s.summary = 'BLE服务Flutter插件' + s.summary = 'BLE服务插件,提供蓝牙扫描、连接和通信功能' s.description = <<-DESC -BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。 +BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ AccessorySetupKit唤醒。 DESC s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' + s.info_plist = { + 'NSAccessorySetupKitSupports' => ['Bluetooth'] + } s.dependency 'Flutter' - s.platform = :ios, '11.0' + s.platform = :ios, '18.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 9b28d7cfc..076da7705 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -161,13 +161,7 @@ class BleService { } /// 启动BLE后台服务 - /// [config] 配置参数,可包含: - /// - scan_interval: 扫描间隔时间(毫秒) - /// - scan_duration: 扫描持续时间(毫秒) - /// - notification_title: 通知标题 - /// - notification_content: 通知内容 - /// - mac: 设备地址 - /// - device_name: 设备名称 + Future startBleService(Map config) async { try { final result = diff --git a/local_plugins/ble_service/pubspec.yaml b/local_plugins/ble_service/pubspec.yaml index a0c48bfaa..47cf208c7 100644 --- a/local_plugins/ble_service/pubspec.yaml +++ b/local_plugins/ble_service/pubspec.yaml @@ -21,4 +21,6 @@ flutter: platforms: android: package: com.yunqiinnovation.ble_service - pluginClass: BleServicePlugin \ No newline at end of file + pluginClass: BleServicePlugin + ios: + pluginClass: BleServicePlugin \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/build.gradle.kts b/local_plugins/bytedance_speech/android/build.gradle.kts index 0073e010c..afd104ba2 100644 --- a/local_plugins/bytedance_speech/android/build.gradle.kts +++ b/local_plugins/bytedance_speech/android/build.gradle.kts @@ -3,11 +3,28 @@ version = "1.0.0" plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") + kotlin("android") +} + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.1.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0") + } +} + +repositories { + google() + mavenCentral() } android { - namespace = "com.yunqiinnovation.ble_service" + namespace = "com.deep_voice.bytedance_speech" compileSdk = 35 defaultConfig { @@ -22,15 +39,18 @@ android { kotlinOptions { jvmTarget = "1.8" + languageVersion = "1.7" + apiVersion = "1.7" } } - dependencies { - implementation(kotlin("stdlib-jdk7")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") implementation("com.google.code.gson:gson:2.10.1") + + + implementation(project(":speech")) } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml b/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml index 9f3f16d19..3fcca60bd 100644 --- a/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml +++ b/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt index 34eefdf4b..9e9da5ee8 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt @@ -1,6 +1,7 @@ package com.deep_voice.bytedance_speech import android.content.Context +import android.util.Log import com.google.gson.Gson import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers @@ -13,6 +14,7 @@ import okhttp3.WebSocketListener import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Level import okio.ByteString +import org.json.JSONObject import java.io.ByteArrayOutputStream import java.util.UUID import java.util.concurrent.TimeUnit @@ -31,6 +33,7 @@ class BytedanceASR( private val statusCallback: (String) -> Unit ) { companion object { + private const val TAG = "BytedanceASR" // 协议常量 private const val PROTOCOL_VERSION = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01 @@ -80,7 +83,7 @@ class BytedanceASR( private var supportedLanguages: List = listOf("zh-CN") // 状态控制 - private var isRunning = false + private var isRecognizing = false private var isLastPacket = false private var isContinuousMode = false private var isInitialized = false @@ -97,10 +100,10 @@ class BytedanceASR( } client = OkHttpClient.Builder() - .pingInterval(50, TimeUnit.SECONDS) + .pingInterval(30, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) - .readTimeout(100, TimeUnit.SECONDS) - .writeTimeout(100, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) .build() } @@ -146,7 +149,7 @@ class BytedanceASR( return@withContext false } - if (isRunning) { + if (isRecognizing) { errorCallback("BUSY", "语音识别正在进行中") return@withContext false } @@ -163,12 +166,12 @@ class BytedanceASR( // 等待连接建立 var attempts = 0 - while (!isRunning && attempts < 10) { + while (!isRecognizing && attempts < 10) { delay(300) attempts++ } - if (!isRunning) { + if (!isRecognizing) { errorCallback("CONNECTION_ERROR", "无法建立连接") return@withContext false } @@ -197,7 +200,7 @@ class BytedanceASR( return@withContext false } - if (isRunning) { + if (isRecognizing) { errorCallback("BUSY", "语音识别正在进行中") return@withContext false } @@ -223,7 +226,7 @@ class BytedanceASR( */ suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) { try { - if (!isRunning || !isContinuousMode) { + if (!isRecognizing || !isContinuousMode) { return@withContext true // 已经停止或非连续模式 } @@ -242,7 +245,7 @@ class BytedanceASR( */ suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) { try { - if (!isRunning) { + if (!isRecognizing) { errorCallback("NOT_RUNNING", "语音识别未启动") return@withContext false } @@ -277,10 +280,10 @@ class BytedanceASR( /** * 开始语音识别 */ - private suspend fun start() = withContext(Dispatchers.IO) { - if (isRunning) return@withContext + suspend fun start() = withContext(Dispatchers.IO) { + if (isRecognizing) return@withContext - isRunning = true + isRecognizing = true isLastPacket = false seq = 1 @@ -303,114 +306,81 @@ class BytedanceASR( webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { val logId = response.header("X-Tt-Logid") - println("ASR WebSocket连接成功, X-Tt-Logid: $logId") + Log.d(TAG, "ASR WebSocket连接成功, X-Tt-Logid: $logId") // 发送初始配置请求 sendInitialRequest(webSocket) } - override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + override fun onMessage(webSocket: WebSocket, text: String) { + Log.d(TAG, "收到文本消息: $text") try { - val data = bytes.toByteArray() - val response = parseResponse(data) - - // 根据消息类型处理响应 - when (response.messageType) { - FULL_SERVER_RESPONSE -> { - // 解析识别结果 - val isFinal = response.messageTypeSpecificFlags == NEG_WITH_SEQUENCE - - val resultText = if (response.payload.isNotEmpty()) { - val decompressedData = if (response.compression == GZIP) { - gzipDecompress(response.payload) - } else { - response.payload - } - - val jsonStr = String(decompressedData) - extractTextFromJson(jsonStr) - } else { - "" - } - - if (resultText.isNotEmpty()) { - // 处理单次识别模式 - if (!isContinuousMode) { - onceResultBuilder.append(resultText) - } - - // 回调识别结果 - resultCallback(resultText, isFinal) - } - - // 如果是最后一个包,关闭连接 - if (isFinal) { - // 单次识别完成回调 - if (!isContinuousMode && singleRecognitionCallback != null) { - val finalResult = onceResultBuilder.toString() - singleRecognitionCallback?.invoke(finalResult) - onceResultBuilder.clear() - singleRecognitionCallback = null - } - - if (!isContinuousMode) { - isRunning = false - updateStatus(STATUS_STOPPED) - webSocket.close(1000, "Finished") - } - } - } - SERVER_ERROR_RESPONSE -> { - val errorMessage = String(response.payload) - errorCallback(response.sequence.toString(), errorMessage) - isRunning = false - updateStatus(STATUS_ERROR) - webSocket.close(1000, "Error") - } - } + val json = JSONObject(text) + // 解析结果 + resultCallback(json.optString("text", ""), false) } catch (e: Exception) { - errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") - updateStatus(STATUS_ERROR) + Log.e(TAG, "解析消息失败", e) } } - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - isRunning = false - errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") - updateStatus(STATUS_ERROR) + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + // Log.d(TAG, "收到二进制消息") + // 在实际实现中解析二进制消息 + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG, "连接关闭中: $code, $reason") + isRecognizing = false } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - isRunning = false - updateStatus(STATUS_STOPPED) - println("ASR WebSocket连接关闭: $code, $reason") + Log.d(TAG, "连接已关闭: $code, $reason") + isRecognizing = false + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(TAG, "连接失败", t) + errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") + isRecognizing = false } }) } /** - * 发送音频数据 + * 向服务器发送音频数据 */ - private suspend fun feedAudioData(audioData: ByteArray) = withContext(Dispatchers.IO) { - if (!isRunning || isLastPacket) return@withContext + suspend fun feedAudioData(audioData: ByteArray) { + if (!isRecognizing || webSocket == null) { + Log.w(TAG, "未开始识别或WebSocket未连接") + return + } - webSocket?.let { ws -> - // 发送音频请求 - sendAudioRequest(ws, audioData, false) + try { + // 在实际实现中需要按照火山引擎的协议格式封装音频数据 + val byteString = ByteString.of(*audioData) + webSocket?.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送音频数据失败", e) } } /** - * 停止识别 + * 停止语音识别 */ - private suspend fun stop() = withContext(Dispatchers.IO) { - if (!isRunning || isLastPacket) return@withContext - - isLastPacket = true + suspend fun stop() { + if (!isRecognizing) { + return + } - webSocket?.let { ws -> - // 发送最后一个音频包 - sendAudioRequest(ws, ByteArray(0), true) + try { + // 发送最后一帧音频数据,标记结束 + // 在实际实现中需要按照协议发送结束标记 + + isRecognizing = false + webSocket?.close(1000, "正常关闭") + webSocket = null + } catch (e: Exception) { + Log.e(TAG, "停止识别失败", e) } } @@ -420,7 +390,7 @@ class BytedanceASR( fun release() { webSocket?.cancel() webSocket = null - isRunning = false + isRecognizing = false isLastPacket = false updateStatus(STATUS_STOPPED) } @@ -429,123 +399,133 @@ class BytedanceASR( * 发送初始配置请求 */ private fun sendInitialRequest(webSocket: WebSocket) { - // 构建请求参数 - val user = JsonObject().apply { - addProperty("uid", "test") - } - - val audio = JsonObject().apply { - addProperty("format", "pcm") - addProperty("sample_rate", sampleRate) - addProperty("bits", bits) - addProperty("channel", channels) - addProperty("codec", "raw") - } - - val request = JsonObject().apply { - addProperty("model_name", "bigmodel") - addProperty("enable_punc", enablePunctuation) - } - - val payload = JsonObject().apply { - add("user", user) - add("audio", audio) - add("request", request) + try { + // 构建请求参数 + val user = JsonObject().apply { + addProperty("uid", "test") + } + + val audio = JsonObject().apply { + addProperty("format", "pcm") + addProperty("sample_rate", sampleRate) + addProperty("bits", bits) + addProperty("channel", channels) + addProperty("codec", "raw") + } + + val request = JsonObject().apply { + addProperty("model_name", "bigmodel") + addProperty("enable_punc", enablePunctuation) + } + + val payload = JsonObject().apply { + add("user", user) + add("audio", audio) + add("request", request) + } + + val payloadStr = payload.toString() + Log.d(TAG, "ASR初始请求: $payloadStr") + + // 压缩负载 + val compressedPayload = gzipCompress(payloadStr.toByteArray()) + + // 构建消息头 + val header = getHeader( + FULL_CLIENT_REQUEST, + POS_SEQUENCE, + JSON, + GZIP, + 0 + ) + + // 构建消息序列号 + val seqBytes = intToBytes(seq) + + // 构建负载大小 + val payloadSizeBytes = intToBytes(compressedPayload.size) + + // 组装完整请求数据 + val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) + var offset = 0 + + System.arraycopy(header, 0, requestBytes, offset, header.size) + offset += header.size + + System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) + offset += seqBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + + // 发送请求 + val byteString = ByteString.of(*requestBytes) + webSocket.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送初始请求失败", e) } - - val payloadStr = payload.toString() - println("ASR初始请求: $payloadStr") - - // 压缩负载 - val compressedPayload = gzipCompress(payloadStr.toByteArray()) - - // 构建消息头 - val header = getHeader( - FULL_CLIENT_REQUEST, - POS_SEQUENCE, - JSON, - GZIP, - 0 - ) - - // 构建消息序列号 - val seqBytes = intToBytes(seq) - - // 构建负载大小 - val payloadSizeBytes = intToBytes(compressedPayload.size) - - // 组装完整请求数据 - val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) - var offset = 0 - - System.arraycopy(header, 0, requestBytes, offset, header.size) - offset += header.size - - System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) - offset += seqBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) - - // 发送请求 - webSocket.send(ByteString.of(requestBytes)) } /** * 发送音频数据请求 */ private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) { - seq++ - - // 如果是最后一个包,使用负序列号 - val actualSeq = if (isLast) -seq else seq - - // 设置消息标志 - val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE - - // 构建消息头 - val header = getHeader( - AUDIO_ONLY_REQUEST, - flag, - NO_SERIALIZATION, - GZIP, - 0 - ) - - // 构建消息序列号 - val seqBytes = intToBytes(actualSeq) - - // 压缩音频数据 - val compressedPayload = if (audioData.isNotEmpty()) { - gzipCompress(audioData) - } else { - ByteArray(0) - } - - // 构建负载大小 - val payloadSizeBytes = intToBytes(compressedPayload.size) - - // 组装完整请求数据 - val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) - var offset = 0 - - System.arraycopy(header, 0, requestBytes, offset, header.size) - offset += header.size - - System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) - offset += seqBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - if (compressedPayload.isNotEmpty()) { - System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + try { + seq++ + + // 如果是最后一个包,使用负序列号 + val actualSeq = if (isLast) -seq else seq + + // 设置消息标志 + val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE + + // 构建消息头 + val header = getHeader( + AUDIO_ONLY_REQUEST, + flag, + NO_SERIALIZATION, + GZIP, + 0 + ) + + // 构建消息序列号 + val seqBytes = intToBytes(actualSeq) + + // 压缩音频数据 + val compressedPayload = if (audioData.isNotEmpty()) { + gzipCompress(audioData) + } else { + ByteArray(0) + } + + // 构建负载大小 + val payloadSizeBytes = intToBytes(compressedPayload.size) + + // 组装完整请求数据 + val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) + var offset = 0 + + System.arraycopy(header, 0, requestBytes, offset, header.size) + offset += header.size + + System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) + offset += seqBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + if (compressedPayload.isNotEmpty()) { + System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + } + + // 发送请求 + val byteString = ByteString.of(*requestBytes) + webSocket.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送音频请求失败", e) } - - // 发送请求 - webSocket.send(ByteString.of(requestBytes)) } /** @@ -619,7 +599,7 @@ class BytedanceASR( return "" } catch (e: Exception) { - println("解析JSON失败: ${e.message}") + Log.e(TAG, "解析JSON失败", e) return "" } } diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt new file mode 100644 index 000000000..9587f401f --- /dev/null +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt @@ -0,0 +1,329 @@ +package com.deep_voice.bytedance_speech + +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.CancellationException +/** + * 字节跳动语音合成音频播放器 + * + * 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理 + */ +class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { + companion object { + private const val TAG = "BytedanceAudioPlayer" + private const val SAMPLE_RATE = 24000 // 样本率 + private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道 + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM + private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间 + } + + // 协程相关 + private val job = SupervisorJob() + override val coroutineContext = Dispatchers.IO + job + + // 音频处理相关 + private var audioTrack: AudioTrack? = null + private val isPlaying = AtomicBoolean(false) + private val isPaused = AtomicBoolean(false) + + // 播放状态 + private val playStarted = AtomicBoolean(false) + private val completionReported = AtomicBoolean(false) + + // 使用简单的并发队列,保证线程安全 + private val audioDataQueue = ConcurrentLinkedQueue() + + // 播放状态监听 + private var onPlayStarted: (() -> Unit)? = null + private var onPlayCompleted: (() -> Unit)? = null + private var onError: ((String) -> Unit)? = null + + // 处理协程 + private var processingJob: Job? = null + private var lastDataTime = 0L + + init { + Log.d(TAG, "BytedanceAudioPlayer初始化") + } + + /** + * 开始播放 + */ + fun start() { + if (isPlaying.getAndSet(true)) return + + isPaused.set(false) + playStarted.set(false) + completionReported.set(false) + lastDataTime = System.currentTimeMillis() + + processingJob = launch { + try { + initAudioTrack() + + // 处理队列中的音频数据 + while (isActive && isPlaying.get()) { + processQueuedAudio() + + // 检查是否播放完成 + checkPlaybackCompletion() + + // 队列为空时短暂延迟 + if (audioDataQueue.isEmpty()) { + delay(10) + } + } + } catch (e: CancellationException) { + // 协程被取消,正常行为 + } catch (e: Exception) { + Log.e(TAG, "播放错误: ${e.message}") + onError?.invoke("播放错误: ${e.message}") + stopInternal(false) + } + } + } + + /** + * 暂停播放 + */ + fun pause() { + if (!isPlaying.get() || isPaused.getAndSet(true)) return + audioTrack?.pause() + } + + /** + * 恢复播放 + */ + fun resume() { + if (!isPlaying.get() || !isPaused.getAndSet(false)) return + audioTrack?.play() + } + + /** + * 停止播放 + */ + fun stop() { + Log.d(TAG, "停止播放") + stopInternal(true) + } + + /** + * 内部停止处理 + */ + private fun stopInternal(reportCompletion: Boolean) { + if (!isPlaying.getAndSet(false)) return + + isPaused.set(false) + processingJob?.cancel() + + audioTrack?.stop() + // 不在stopInternal中释放资源,只停止播放 + + // 清空队列 + audioDataQueue.clear() + + // 播放完成通知 + if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) { + launch(Dispatchers.Main) { + onPlayCompleted?.invoke() + } + } + } + + /** + * 释放资源 + */ + fun release() { + stopInternal(false) + + // 在release方法中释放AudioTrack资源 + audioTrack?.release() + audioTrack = null + + // 释放协程资源 + job.cancel() + } + + /** + * 设置播放开始回调 + */ + fun setOnPlayStarted(listener: () -> Unit) { + onPlayStarted = listener + } + + /** + * 设置播放完成回调 + */ + fun setOnPlayCompleted(listener: () -> Unit) { + onPlayCompleted = listener + } + + /** + * 设置错误回调 + */ + fun setOnError(listener: (String) -> Unit) { + onError = listener + } + + /** + * 接收音频数据(实现AudioDataListener接口) + */ + override fun onAudioData(data: ByteArray) { + if (data.isEmpty()) return + + lastDataTime = System.currentTimeMillis() + + if (!isPlaying.get()) { + start() + } + + // 添加到队列 - 简单有效,保证FIFO顺序 + audioDataQueue.add(data.copyOf()) + } + + /** + * 检查是否正在播放 + */ + fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get() + + /** + * 处理队列中的音频数据 + */ + private suspend fun processQueuedAudio() { + if (isPaused.get() || !isPlaying.get() || !isActive) return + + // 获取并处理队列中的数据 + val data = audioDataQueue.poll() ?: return + + // 写入音频数据 + val result = audioTrack?.write(data, 0, data.size) ?: 0 + + if (result > 0) { + // 标记播放开始 + if (!playStarted.getAndSet(true)) { + withContext(Dispatchers.Main) { + onPlayStarted?.invoke() + } + } + } else if (result < 0) { + // 处理错误 + handleAudioTrackError(result) + } + } + + /** + * 检查播放是否完成(超时无数据) + */ + private suspend fun checkPlaybackCompletion() { + val currentTime = System.currentTimeMillis() + + // 超时判断 - 队列为空且超过超时时间 + if (playStarted.get() && audioDataQueue.isEmpty() && + currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) { + + // 播放完成 + if (!completionReported.getAndSet(true)) { + withContext(Dispatchers.Main) { + onPlayCompleted?.invoke() + } + stopInternal(false) // 已报告,不需要再次报告 + } + } + } + + /** + * 处理AudioTrack错误 + */ + private fun handleAudioTrackError(result: Int) { + val errorMsg = when(result) { + AudioTrack.ERROR_INVALID_OPERATION -> "无效操作" + AudioTrack.ERROR_BAD_VALUE -> "参数错误" + AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁" + else -> "未知错误" + } + + // 对象已销毁时尝试重建 + if (result == AudioTrack.ERROR_DEAD_OBJECT) { + try { + audioTrack?.release() + initAudioTrack() + } catch (e: Exception) { + Log.e(TAG, "重建AudioTrack失败: ${e.message}") + throw e + } + } else { + Log.e(TAG, "AudioTrack错误: $errorMsg") + } + } + + /** + * 初始化AudioTrack + */ + private fun initAudioTrack() { + val minBufferSize = AudioTrack.getMinBufferSize( + SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT + ) + + if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) { + throw IllegalStateException("无法获取有效的音频缓冲区大小") + } + + // 使用较大的缓冲区提高稳定性 + val bufferSize = minBufferSize * 4 + + // 创建AudioTrack + audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0) + AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AUDIO_FORMAT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(CHANNEL_CONFIG) + .build() + ) + .setBufferSizeInBytes(bufferSize) + .setTransferMode(AudioTrack.MODE_STREAM) + .build() + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, + CHANNEL_CONFIG, + AUDIO_FORMAT, + bufferSize, + AudioTrack.MODE_STREAM + ) + } + + // 检查初始化状态 + if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) { + throw IllegalStateException("AudioTrack初始化失败") + } + + // 开始播放 + audioTrack?.play() + } +} \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt index 0ee9b1092..d86faa71f 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt @@ -3,217 +3,176 @@ package com.deep_voice.bytedance_speech import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.EventChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.AudioDataListener +import java.util.concurrent.ConcurrentLinkedQueue -/** 火山引擎语音识别和语音合成插件 */ +/** + * 字节跳动语音服务Flutter插件 + * 提供TTS和ASR能力,通过独立的事件通道进行通信 + */ class BytedanceSpeechPlugin : FlutterPlugin { - private val TAG = "BytedanceSpeechPlugin" - private lateinit var context: Context - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val mainHandler = Handler(Looper.getMainLooper()) + companion object { + private const val TAG = "BytedanceSpeechPlugin" + + // 通道名称 + private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts" + private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr" + + // 事件通道 + private const val TTS_EVENT_CHANNEL = "com.deep_voice.bytedance_tts/events" + private const val TTS_AUDIO_CHANNEL = "com.deep_voice.bytedance_tts/audio" + private const val ASR_EVENT_CHANNEL = "com.deep_voice.bytedance_asr/events" + } - // ASR相关 + // Flutter插件相关 + private lateinit var ttsChannel: MethodChannel private lateinit var asrChannel: MethodChannel - private lateinit var asrEventChannel: EventChannel - private var asrEventSink: EventChannel.EventSink? = null - private var asrInstance: BytedanceASR? = null + private lateinit var context: Context - // TTS相关 - private lateinit var ttsChannel: MethodChannel - private lateinit var ttsEventChannel: EventChannel + // TTS事件通道 private var ttsEventSink: EventChannel.EventSink? = null - private var ttsInstance: BytedanceTTS? = null + private var ttsAudioSink: EventChannel.EventSink? = null - // ASR事件发送方法 - private fun sendAsrEvent(event: Map) { - println("发送ASR事件: $event") - if (asrEventSink == null) { - println("无法发送ASR事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - asrEventSink?.success(event) - println("ASR事件发送成功") - } catch (e: Exception) { - println("发送ASR事件失败: ${e.message}") - } - } - } + // ASR事件通道 + private var asrEventSink: EventChannel.EventSink? = null - // TTS事件发送方法 - private fun sendTtsEvent(event: Map) { - println("发送TTS事件: $event") - if (ttsEventSink == null) { - println("无法发送TTS事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - ttsEventSink?.success(event) - println("TTS事件发送成功") - } catch (e: Exception) { - println("发送TTS事件失败: ${e.message}") - } - } - } + // 语音服务实例 + private var bytedanceTTS: BytedanceTTS? = null + private var bytedanceASR: BytedanceASR? = null + + // 协程作用域 + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.Main + job) + + // 主线程Handler + private val mainHandler = Handler(Looper.getMainLooper()) override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - // 初始化ASR通道 - asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr") + // 初始化TTS方法通道 + ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL) + ttsChannel.setMethodCallHandler(TtsMethodHandler()) + + // 初始化ASR方法通道 + asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL) asrChannel.setMethodCallHandler(AsrMethodHandler()) - // 初始化TTS通道 - ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts") - ttsChannel.setMethodCallHandler(TtsMethodHandler()) + // 设置TTS事件通道 + setupTtsEventChannels(flutterPluginBinding) - // 初始化ASR事件通道 - asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events") - asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - asrEventSink = events - } - - override fun onCancel(arguments: Any?) { - asrEventSink = null + // 设置ASR事件通道 + setupAsrEventChannel(flutterPluginBinding) + + Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎") + } + + /** + * 设置TTS相关的事件通道 + */ + private fun setupTtsEventChannels(binding: FlutterPlugin.FlutterPluginBinding) { + // TTS事件通道 + EventChannel(binding.binaryMessenger, TTS_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + ttsEventSink = events + Log.d(TAG, "TTS事件流已建立连接") + } + + override fun onCancel(arguments: Any?) { + ttsEventSink = null + Log.d(TAG, "TTS事件流已取消") + } } - }) + ) - // 初始化TTS事件通道 - ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events") - ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - ttsEventSink = events + // TTS音频数据通道 + EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + ttsAudioSink = events + Log.d(TAG, "TTS音频流已建立连接") + } + + override fun onCancel(arguments: Any?) { + ttsAudioSink = null + Log.d(TAG, "TTS音频流已取消") + } } - - override fun onCancel(arguments: Any?) { - ttsEventSink = null + ) + } + + /** + * 设置ASR相关的事件通道 + */ + private fun setupAsrEventChannel(binding: FlutterPlugin.FlutterPluginBinding) { + // ASR事件通道 + EventChannel(binding.binaryMessenger, ASR_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + asrEventSink = events + Log.d(TAG, "ASR事件流已建立连接") + } + + override fun onCancel(arguments: Any?) { + asrEventSink = null + Log.d(TAG, "ASR事件流已取消") + } } - }) + ) } - // ASR方法处理器 - inner class AsrMethodHandler : MethodCallHandler { + /** + * TTS方法处理器 + */ + inner class TtsMethodHandler : MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "initialize" -> { - val appId = call.argument("appId") ?: "" - val accessToken = call.argument("accessToken") ?: "" - val streamMode = call.argument("streamMode") ?: true - val enablePunctuation = call.argument("enablePunctuation") ?: true - val sampleRate = call.argument("sampleRate") ?: 16000 - val bits = call.argument("bits") ?: 16 - val channels = call.argument("channels") ?: 1 + val ttsAppId = call.argument("ttsAppId") ?: "" + val ttsAppToken = call.argument("ttsAppToken") ?: "" + val language = call.argument("language") ?: "zh-CN" - try { - asrInstance = BytedanceASR( - context = context, - appId = appId, - accessToken = accessToken, - streamMode = streamMode, - enablePunctuation = enablePunctuation, - sampleRate = sampleRate, - bits = bits, - channels = channels, - resultCallback = { text, isFinal -> - val eventType = if (isFinal) "result" else "recognizing" - sendAsrEvent(mapOf( - "type" to eventType, - "text" to text, - "isFinal" to isFinal - )) - }, - errorCallback = { errorCode, errorMessage -> - sendAsrEvent(mapOf( - "type" to "error", - "errorCode" to errorCode, - "errorMessage" to errorMessage - )) - } - ) - result.success(true) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } + initializeTTS(ttsAppId, ttsAppToken, language, result) } - "startRecognition" -> { - if (asrEventSink == null) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null) - return - } - - scope.launch { - try { - asrInstance?.start() - sendAsrEvent(mapOf("type" to "sessionStarted")) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("START_ERROR", e.message, null) - } - } - } + "setVoice" -> { + val voice = call.argument("voice") ?: "" + setVoice(voice, result) } - "feedAudioData" -> { - val audioData = call.argument("audioData") - if (audioData != null) { - scope.launch { - try { - asrInstance?.feedAudioData(audioData) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("FEED_ERROR", e.message, null) - } - } - } - } else { - result.error("INVALID_ARGUMENT", "音频数据不能为空", null) - } + "speakOnce" -> { + val text = call.argument("text") ?: "" + speakOnce(text, result) } - "stopRecognition" -> { - scope.launch { - try { - asrInstance?.stop() - sendAsrEvent(mapOf("type" to "sessionStopped")) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("STOP_ERROR", e.message, null) - } - } - } + "speakStream" -> { + val text = call.argument("text") ?: "" + speakStream(text, result) } - "dispose" -> { - try { - asrInstance?.release() - asrInstance = null - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } + "flushStream" -> { + flushStream(result) + } + "stopTTS" -> { + stopTTS(result) } + "disposeTTS" -> { + disposeTTS(result) + } + else -> { result.notImplemented() } @@ -221,93 +180,23 @@ class BytedanceSpeechPlugin : FlutterPlugin { } } - // TTS方法处理器 - inner class TtsMethodHandler : MethodCallHandler { + /** + * ASR方法处理器 + */ + inner class AsrMethodHandler : MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { - "initialize" -> { - val appId = call.argument("appId") ?: "" - val accessToken = call.argument("accessToken") ?: "" - val speaker = call.argument("speaker") ?: "" - val format = call.argument("format") ?: "mp3" - val sampleRate = call.argument("sampleRate") ?: 24000 - val enableTimestamp = call.argument("enableTimestamp") ?: true - - try { - ttsInstance = BytedanceTTS( - context = context, - appId = appId, - accessToken = accessToken, - speaker = speaker, - format = format, - sampleRate = sampleRate, - enableTimestamp = enableTimestamp, - audioCallback = { audioData -> - sendTtsEvent(mapOf( - "type" to "audio", - "audioData" to audioData - )) - }, - completionCallback = { - sendTtsEvent(mapOf("type" to "completed")) - }, - errorCallback = { errorCode, errorMessage -> - sendTtsEvent(mapOf( - "type" to "error", - "errorCode" to errorCode, - "errorMessage" to errorMessage - )) - } - ) - result.success(true) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } + "initializeASR" -> { + result.notImplemented() } - "synthesize" -> { - val text = call.argument("text") ?: "" - - if (ttsEventSink == null) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始合成", null) - return - } - - scope.launch { - try { - sendTtsEvent(mapOf("type" to "started")) - ttsInstance?.synthesize(text) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("SYNTHESIS_ERROR", e.message, null) - } - } - } + "startListening" -> { + result.notImplemented() } - "stopSynthesis" -> { - scope.launch { - try { - ttsInstance?.stop() - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("STOP_ERROR", e.message, null) - } - } - } + "stopListening" -> { + result.notImplemented() } - "dispose" -> { - try { - ttsInstance?.release() - ttsInstance = null - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } + "cancelASR" -> { + result.notImplemented() } else -> { result.notImplemented() @@ -317,15 +206,274 @@ class BytedanceSpeechPlugin : FlutterPlugin { } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - asrChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null) - asrEventChannel.setStreamHandler(null) - ttsEventChannel.setStreamHandler(null) + asrChannel.setMethodCallHandler(null) + + // 释放资源 + bytedanceTTS?.release() + bytedanceTTS = null + + bytedanceASR?.release() + bytedanceASR = null + + job.cancel() + + Log.d(TAG, "字节跳动语音插件已从Flutter引擎分离") + } + + /** + * 初始化TTS服务 + */ + private fun initializeTTS(ttsAppId: String, ttsAppToken: String, language: String, result: Result) { + scope.launch { + try { + if (ttsAppId.isEmpty() || ttsAppToken.isEmpty()) { + result.error( + "INVALID_PARAMS", + "AppID和AppToken不能为空", + null + ) + return@launch + } + + // 确保只有一个TTS实例 + bytedanceTTS?.release() + + // 创建新的TTS实例 + bytedanceTTS = BytedanceTTS(context) + + // 添加事件监听器 + bytedanceTTS?.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> { + sendTTSEvent("started", null) + } + TtsEventType.SYNTHESIS_COMPLETED -> { + sendTTSEvent("completed", null) + } + TtsEventType.SYNTHESIS_CANCELED -> { + sendTTSEvent("canceled", null) + } + TtsEventType.ERROR -> { + val params = HashMap() + params["code"] = event.params["errorCode"] ?: "UNKNOWN_ERROR" + params["message"] = event.params["errorMessage"] ?: "未知错误" + sendTTSEvent("error", params) + } + } + } + }) + + // 添加音频数据监听器 + bytedanceTTS?.addAudioDataListener(object : AudioDataListener { + override fun onAudioData(data: ByteArray) { + sendTTSAudioData(data) + } + }) + + // 初始化TTS + val success = bytedanceTTS?.initialize( + ttsAppId = ttsAppId, + ttsAppToken = ttsAppToken, + ttsResource = "", // 火山TTS不使用resource + language = language + ) ?: false + + if (success) { + result.success(true) + } else { + result.error( + "INIT_FAILED", + "TTS初始化失败", + null + ) + } + } catch (e: Exception) { + Log.e(TAG, "TTS初始化异常: ${e.message}", e) + result.error( + "INIT_EXCEPTION", + "TTS初始化异常: ${e.message}", + null + ) + } + } + } + + /** + * 设置TTS音色 + */ + private fun setVoice(voice: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + try { + val success = bytedanceTTS?.setVoice(voice) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "设置音色异常: ${e.message}", e) + result.error( + "VOICE_EXCEPTION", + "设置音色异常: ${e.message}", + null + ) + } + } + + /** + * 合成并播放文本(单次) + */ + private fun speakOnce(text: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } - scope.cancel() - asrInstance?.release() - ttsInstance?.release() - asrInstance = null - ttsInstance = null + try { + val success = bytedanceTTS?.speakOnce(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "合成文本异常: ${e.message}", e) + result.error( + "SPEAK_EXCEPTION", + "合成文本异常: ${e.message}", + null + ) + } + } + + /** + * 流式合成文本 + */ + private fun speakStream(text: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + try { + val success = bytedanceTTS?.speakStream(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "流式合成异常: ${e.message}", e) + result.error( + "STREAM_EXCEPTION", + "流式合成异常: ${e.message}", + null + ) + } + } + + /** + * 刷新流式合成缓冲区 + */ + private fun flushStream(result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + try { + val success = bytedanceTTS?.flushStream() ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "刷新流异常: ${e.message}", e) + result.error( + "FLUSH_EXCEPTION", + "刷新流异常: ${e.message}", + null + ) + } + } + + /** + * 停止TTS + */ + private fun stopTTS(result: Result) { + if (bytedanceTTS == null) { + result.success(true) + return + } + + try { + val success = bytedanceTTS?.stop() ?: true + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "停止TTS异常: ${e.message}", e) + result.error( + "STOP_EXCEPTION", + "停止TTS异常: ${e.message}", + null + ) + } + } + + /** + * 释放TTS资源 + */ + private fun disposeTTS(result: Result) { + try { + bytedanceTTS?.release() + bytedanceTTS = null + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "释放TTS资源异常: ${e.message}", e) + result.error( + "DISPOSE_EXCEPTION", + "释放TTS资源异常: ${e.message}", + null + ) + } + } + + + /** + * 发送TTS事件到Flutter端 + */ + private fun sendTTSEvent(eventName: String, params: Map?) { + if (ttsEventSink == null) return + + val event = HashMap() + event["event"] = eventName + + if (params != null) { + event["params"] = params + } + + mainHandler.post { + ttsEventSink?.success(event) + } + } + + /** + * 发送TTS音频数据到Flutter端 + */ + private fun sendTTSAudioData(data: ByteArray) { + if (ttsAudioSink == null) return + + mainHandler.post { + ttsAudioSink?.success(data) + } + } + + /** + * 发送ASR事件到Flutter端 + */ + private fun sendASREvent(eventType: String, data: Any, params: Map? = null) { + if (asrEventSink == null) return + + val event = HashMap() + event["type"] = eventType + event["data"] = data + + if (params != null) { + event["params"] = params + } + + mainHandler.post { + asrEventSink?.success(event) + } } } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index d78be4d24..d202a36e4 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -1,37 +1,40 @@ package com.deep_voice.bytedance_speech import android.content.Context -import com.google.gson.Gson -import com.google.gson.JsonObject +import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.CancellationException import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener -import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.logging.HttpLoggingInterceptor.Level import okio.ByteString -import java.io.ByteArrayOutputStream +import org.json.JSONObject import java.util.UUID import java.util.concurrent.TimeUnit -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream +import java.util.concurrent.atomic.AtomicBoolean /** * 火山引擎大模型语音合成实现 * * 基于WebSocket协议实现双向流式TTS */ -class BytedanceTTS( - private val context: Context, - private val audioCallback: (ByteArray) -> Unit, - private val completionCallback: () -> Unit, - private val errorCallback: (String, String) -> Unit, - private val statusCallback: (String) -> Unit -) { +class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { companion object { + private const val TAG = "BytedanceTTS" + // 协议常量 private const val PROTOCOL_VERSION = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01 @@ -80,8 +83,23 @@ class BytedanceTTS( private const val STATUS_SPEAKING = "speaking" private const val STATUS_STOPPED = "stopped" private const val STATUS_ERROR = "error" + + // 连接控制常量 + private const val MAX_CONNECTION_ATTEMPTS = 3 + private const val CONNECTION_TIMEOUT_MS = 10000 // 10秒 } + // 协程相关 + private val job = Job() + override val coroutineContext = Dispatchers.IO + job + + // 事件监听器列表 + private val eventListeners = mutableListOf() + private val audioDataListeners = mutableListOf() + + // 内部音频播放器 + private val audioPlayer = BytedanceAudioPlayer() + // WebSocket连接 private var webSocket: WebSocket? = null private val client: OkHttpClient @@ -94,75 +112,190 @@ class BytedanceTTS( // TTS配置参数 private var appId: String = "" private var accessToken: String = "" - private var speaker: String = "zh_female_qingxin" // 默认语音 - private var format: String = "pcm" - private var sampleRate: Int = 16000 + private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 + private var format: String = "pcm" // 使用mp3格式 + private var sampleRate: Int = 24000 // 使用24kHz采样率 private var enableTimestamp: Boolean = false private var language: String = "zh-CN" // 流式处理状态 private var isStreamMode = false private var currentStatus = STATUS_STOPPED + private var isSpeaking = false + + // 连接相关状态 + private var connectionAttempts = 0 + private var isConnecting = AtomicBoolean(false) + private var lastConnectionAttemptTime = 0L + + // 在类级别添加文本流通道 + private val textChannel = Channel(Channel.BUFFERED) + private var textProcessingJob: Job? = null init { // 配置OkHttp客户端 - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = Level.HEADERS - } - client = OkHttpClient.Builder() - .pingInterval(50, TimeUnit.SECONDS) - .addInterceptor(loggingInterceptor) - .readTimeout(100, TimeUnit.SECONDS) - .writeTimeout(100, TimeUnit.SECONDS) + .pingInterval(50, TimeUnit.SECONDS) // 匹配Java示例设置50秒 + .readTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒 + .writeTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒 .build() // 生成会话ID - sessionId = UUID.randomUUID().toString().replace("-", "") + // sessionId = UUID.randomUUID().toString().replace("-", "") + Log.d(TAG, "BytedanceTTS实例化完成!") + + // 初始化音频播放器回调 + initAudioPlayerCallbacks() + + // 启动文本处理协程 + startTextProcessing() + } + + /** + * 初始化音频播放器回调 + */ + private fun initAudioPlayerCallbacks() { + // 添加播放器作为音频数据监听器 + addAudioDataListener(audioPlayer) + + audioPlayer.setOnPlayStarted { + Log.d(TAG, "播放开始") + isSpeaking = true + updateStatus(STATUS_SPEAKING) + notifyEvent(TtsEventType.SYNTHESIS_STARTED) + } + + // 设置播放完成回调 + audioPlayer.setOnPlayCompleted { + Log.d(TAG, "播放结束") + isSpeaking = false + updateStatus(if (isStreamMode) STATUS_READY else STATUS_STOPPED) + notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) + } + + // 设置错误回调 + audioPlayer.setOnError { errorMsg -> + isSpeaking = false + updateStatus(STATUS_ERROR) + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "PLAYER_ERROR", "errorMessage" to errorMsg)) + } + } + + /** + * 启动文本处理协程 + */ + private fun startTextProcessing() { + textProcessingJob?.cancel() + textProcessingJob = launch { + try { + for (text in textChannel) { + try { + sendStreamTextRequest(text) + } catch (e: Exception) { + Log.e(TAG, "文本处理错误: ${e.message}") + } + } + } catch (e: CancellationException) { + // 协程被取消,正常情况 + } catch (e: Exception) { + Log.e(TAG, "文本处理异常: ${e.message}") + } + } + } + + /** + * 停止语音合成 + */ + override fun stop(): Boolean { + try { + audioPlayer.stop() + textProcessingJob?.cancel() + + launch { + if (isSessionStarted) { + finishSession() + } + isSpeaking = false // 显式设置,因为这是强制停止 + updateStatus(STATUS_STOPPED) + } + return true + } catch (e: Exception) { + Log.e(TAG, "停止合成失败: ${e.message}") + return false + } } /** * 初始化火山语音服务 */ - suspend fun initialize(subscriptionKey: String, region: String, language: String = "zh-CN"): Boolean = withContext(Dispatchers.IO) { + override fun initialize( + ttsAppId: String, + ttsAppToken: String, + ttsResource: String, + language: String + ): Boolean { try { updateStatus(STATUS_INITIALIZING) - // 设置必要参数 - this@BytedanceTTS.appId = region // 火山引擎中region对应应用ID - this@BytedanceTTS.accessToken = subscriptionKey // 订阅密钥作为访问令牌 - this@BytedanceTTS.language = language + // 设置基本参数 + this.appId = ttsAppId + this.accessToken = ttsAppToken + this.language = language - // 根据语言设置默认speaker - if (language.startsWith("zh")) { - this@BytedanceTTS.speaker = "zh_female_qingxin" - } else if (language.startsWith("en")) { - this@BytedanceTTS.speaker = "en_female_crystal" - } + // 重置连接状态 + connectionAttempts = 0 + isConnecting.set(false) + lastConnectionAttemptTime = 0 // 释放旧连接 - release() - - // 建立新连接 - connect() + webSocket?.cancel() + webSocket = null + isConnected = false + isSessionStarted = false - true + return true } catch (e: Exception) { - errorCallback("INIT_ERROR", e.message ?: "初始化失败") + Log.e(TAG, "初始化失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } + /** + * 重置服务状态 + */ + fun reset() { + stop() + textProcessingJob?.cancel() + + connectionAttempts = 0 + isConnecting.set(false) + lastConnectionAttemptTime = 0 + + webSocket?.cancel() + webSocket = null + + isConnected = false + isSessionStarted = false + isSpeaking = false + + updateStatus(STATUS_STOPPED) + startTextProcessing() + } + /** * 设置语音角色 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { try { speaker = voiceName return true } catch (e: Exception) { - errorCallback("VOICE_ERROR", e.message ?: "设置语音角色失败") + val errorMsg = e.message ?: "设置语音角色失败" + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "VOICE_ERROR", + "errorMessage" to errorMsg + )) return false } } @@ -170,138 +303,298 @@ class BytedanceTTS( /** * 单次播放(非流式) */ - suspend fun speakOnce(text: String): Boolean = withContext(Dispatchers.IO) { + override fun speakOnce(text: String): Boolean { try { if (text.isEmpty()) { - errorCallback("PARAM_ERROR", "合成文本不能为空") - return@withContext false + return false } isStreamMode = false - // 如果没有连接或会话,重新初始化 - if (webSocket == null || !isConnected) { - connect() - // 等待连接和会话建立 - var attempts = 0 - while (!isSessionStarted && attempts < 10) { - delay(300) - attempts++ + launch { + if (!ensureSessionReady()) { + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "SESSION_ERROR", + "errorMessage" to "无法建立会话")) + return@launch } - if (!isSessionStarted) { - errorCallback("SESSION_ERROR", "无法建立会话") - return@withContext false - } + sendTextRequest(text) } - // 发送文本进行合成 - sendTextRequest(text) - return@withContext true + return true } catch (e: Exception) { - errorCallback("SYNTH_ERROR", e.message ?: "语音合成失败") + Log.e(TAG, "语音合成失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } /** * 流式播放开始 */ - suspend fun speakStream(text: String): Boolean = withContext(Dispatchers.IO) { + override fun speakStream(text: String): Boolean { try { if (text.isEmpty()) { - errorCallback("PARAM_ERROR", "合成文本不能为空") - return@withContext false + return true } - isStreamMode = true // 确保会话已就绪 - ensureSessionReady() + if (!isSessionStarted && !ensureSessionReady()) { + Log.e(TAG, "流式播放失败:无法建立会话") + return false + } - // 发送文本进行合成,但不自动结束会话 - sendStreamTextRequest(text) - updateStatus(STATUS_SPEAKING) - return@withContext true + // 如果文本处理协程不活跃,重新启动它 + if (textProcessingJob == null || textProcessingJob?.isActive == false) { + startTextProcessing() + } + // Log.d(TAG, "speakStream $sessionId, $text ") + + textChannel.trySend(text) + return true } catch (e: Exception) { - errorCallback("STREAM_ERROR", e.message ?: "流式合成失败") + Log.e(TAG, "流式合成失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } /** * 结束流式播放 */ - suspend fun flushStream(): Boolean = withContext(Dispatchers.IO) { + override fun flushStream(): Boolean { try { - if (!isStreamMode) { - return@withContext true - } - - if (isSessionStarted) { - finishSession() + if (!isStreamMode) return true + // Log.d(TAG, "flushStream: $sessionId") + launch { + withContext(Dispatchers.IO) { + delay(300) // 确保现有文本处理完成 + } + + if (isSessionStarted) { + isSessionStarted = false + finishSession() + } } - isStreamMode = false - updateStatus(STATUS_STOPPED) - return@withContext true + return true } catch (e: Exception) { - errorCallback("FLUSH_ERROR", e.message ?: "结束流式合成失败") - false + Log.e(TAG, "结束流式合成失败: ${e.message}") + return false } } /** - * 停止语音合成 + * 释放资源 */ - suspend fun stop() = withContext(Dispatchers.IO) { - if (isSessionStarted) { - finishSession() + override fun release() { + Log.d(TAG, "BytedanceTTS release called.") + + // 关闭文本通道 + textChannel.close() + textProcessingJob?.cancel() + + job.cancel() + + // 释放音频播放器资源 + audioPlayer.release() + + // 重置所有状态 + reset() + + // 确保WebSocket连接完全关闭 + webSocket?.cancel() + webSocket = null + } + + /** + * 添加TTS事件监听器 + */ + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) } - updateStatus(STATUS_STOPPED) } /** - * 释放资源 + * 移除TTS事件监听器 */ - fun release() { - webSocket?.cancel() - webSocket = null - isConnected = false - isSessionStarted = false - updateStatus(STATUS_STOPPED) + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) + } + + /** + * 添加音频数据监听器 + */ + override fun addAudioDataListener(listener: AudioDataListener) { + if (!audioDataListeners.contains(listener)) { + audioDataListeners.add(listener) + } + } + + /** + * 移除音频数据监听器 + */ + override fun removeAudioDataListener(listener: AudioDataListener) { + audioDataListeners.remove(listener) + } + + /** + * 通知事件处理 + */ + private fun notifyEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) + val listeners = ArrayList(eventListeners) + + for (listener in listeners) { + try { + listener.onEvent(event) + } catch (e: Exception) { + Log.e(TAG, "事件回调异常: ${e.message}") + } + } + + } + + /** + * 通知音频数据 + */ + private fun notifyAudioData(data: ByteArray) { + launch(Dispatchers.Main) { + for (listener in audioDataListeners) { + try { + listener.onAudioData(data) + } catch (e: Exception) { + Log.e(TAG, "音频数据回调异常: ${e.message}") + } + } + } } /** * 确保会话已就绪 */ - private suspend fun ensureSessionReady() { - // 如果没有连接或会话,重新初始化 - if (webSocket == null || !isConnected) { - connect() - // 等待连接和会话建立 + private fun ensureSessionReady(): Boolean { + // 如果已连接和会话已开始,直接返回成功 + if (isConnected && isSessionStarted) { + return true + } + + // 如果已连接但会话未开始,只需要开始会话 + if (isConnected && !isSessionStarted) { + Log.d(TAG, "WebSocket已连接,正在启动新会话...") + return startSessionOnly() + } + + // 如果正在连接中,等待连接完成 + if (isConnecting.get()) { + Log.d(TAG, "已有连接正在进行中,等待其完成...") + // 等待连接完成,最多5秒 var attempts = 0 - while (!isSessionStarted && attempts < 10) { - delay(300) - attempts++ + while (isConnecting.get() && attempts < 10) { + try { + Thread.sleep(500) + attempts++ + } catch (e: InterruptedException) { + return false + } } - if (!isSessionStarted) { - throw Exception("无法建立会话") + // 连接完成后,检查是否需要启动会话 + if (isConnected && !isSessionStarted) { + return startSessionOnly() } + + return isConnected + } + + // 检查连接重试次数限制 + if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) { + Log.e(TAG, "已达到最大连接尝试次数($MAX_CONNECTION_ATTEMPTS),不再尝试连接") + return false } + + // 检查连接间隔时间 - 避免短时间内频繁重连 + val currentTime = System.currentTimeMillis() + if (currentTime - lastConnectionAttemptTime < 2000 && lastConnectionAttemptTime > 0) { + Log.w(TAG, "连接请求太频繁,等待冷却时间...") + Thread.sleep(2000) + } + + // 更新连接计数和时间 + connectionAttempts++ + lastConnectionAttemptTime = System.currentTimeMillis() + + // 尝试建立连接 + isConnecting.set(true) + try { + connect() + + // 等待连接建立,设置超时 + val startWaitTime = System.currentTimeMillis() + while (!isConnected && System.currentTimeMillis() - startWaitTime < CONNECTION_TIMEOUT_MS) { + try { + Thread.sleep(500) + // 如果连接已失败,提前退出等待 + if (!isConnecting.get() && !isConnected) { + Log.e(TAG, "连接已失败,终止等待") + break + } + } catch (e: InterruptedException) { + isConnecting.set(false) + return false + } + } + + // 连接成功后,启动会话 + if (isConnected && !isSessionStarted) { + startSessionOnly() + } + } finally { + isConnecting.set(false) + } + + // 检查连接结果 + if (!isConnected) { + Log.e(TAG, "连接超时: 无法在${CONNECTION_TIMEOUT_MS/1000}秒内建立连接,连接尝试次数: $connectionAttempts") + return false + } + + // 连接成功,重置尝试计数 + connectionAttempts = 0 + return true } /** - * 用于暂停执行的辅助函数 + * 只启动会话,不重新建立WebSocket连接 */ - private suspend fun delay(ms: Long) = withContext(Dispatchers.IO) { + private fun startSessionOnly(): Boolean { + if (!isConnected || webSocket == null) { + Log.e(TAG, "无法启动会话:WebSocket未连接") + return false + } + + if (isSessionStarted) { + return true + } + try { - Thread.sleep(ms) - } catch (e: InterruptedException) { - // 忽略中断异常 + // 重新生成会话ID + sessionId = UUID.randomUUID().toString().replace("-", "") + Log.d(TAG, "启动新会话,会话ID: $sessionId") + + // 发送开始会话请求 + startTtsSession(webSocket!!) + + // 立即将会话视为已启动,不等待服务器确认 + isSessionStarted = true + updateStatus(STATUS_READY) + + return true + } catch (e: Exception) { + Log.e(TAG, "启动会话失败: ${e.message}", e) + return false } } @@ -310,29 +603,32 @@ class BytedanceTTS( */ private fun updateStatus(status: String) { currentStatus = status - statusCallback(status) } /** * 建立WebSocket连接 */ private fun connect() { + webSocket?.cancel() + webSocket = null + val url = "wss://openspeech.bytedance.com/api/v3/tts/bidirection" + val connectId = UUID.randomUUID().toString() val request = Request.Builder() .url(url) .header("X-Api-App-Key", appId) .header("X-Api-Access-Key", accessToken) .header("X-Api-Resource-Id", "volc.service_type.10029") - .header("X-Api-Connect-Id", UUID.randomUUID().toString()) + .header("X-Api-Connect-Id", connectId) .build() + isConnected = false + isSessionStarted = false + webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { - val logId = response.header("X-Tt-Logid") - println("TTS WebSocket连接成功, X-Tt-Logid: $logId") - - // 发送开始连接事件 + isConnected = true startConnection(webSocket) } @@ -341,7 +637,6 @@ class BytedanceTTS( val data = bytes.toByteArray() val response = parseResponse(data) - // 处理事件 when (response.event) { EVENT_CONNECTION_FAILED, EVENT_SESSION_FAILED -> { val errorMsg = if (response.responseMetaJson.isNotEmpty()) { @@ -349,65 +644,68 @@ class BytedanceTTS( } else { "连接或会话失败" } - errorCallback("TTS_ERROR", errorMsg) + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "TTS_ERROR", + "errorMessage" to errorMsg)) webSocket.close(1000, "Error") updateStatus(STATUS_ERROR) - } - - EVENT_CONNECTION_STARTED -> { - isConnected = true - // 开始TTS会话 - startTtsSession(webSocket) + isSpeaking = false // 错误情况下显式设置 + isSessionStarted = false + isConnected = false + isConnecting.set(false) } EVENT_SESSION_STARTED -> { - isSessionStarted = true - updateStatus(STATUS_READY) + connectionAttempts = 0 } EVENT_TTS_RESPONSE -> { - // 音频数据 if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) { - audioCallback(response.payload) + notifyAudioData(response.payload) } } EVENT_CONNECTION_FINISHED -> { isConnected = false isSessionStarted = false - updateStatus(STATUS_STOPPED) - completionCallback() + isConnecting.set(false) webSocket.close(1000, "Connection finished") } EVENT_SESSION_FINISHED -> { isSessionStarted = false - if (!isStreamMode) { + + if (!isStreamMode) { finishConnection(webSocket) - } else { - // 流模式下保持连接 - updateStatus(STATUS_READY) } } } } catch (e: Exception) { - errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") + Log.e(TAG, "解析响应失败: ${e.message}") updateStatus(STATUS_ERROR) + isSpeaking = false // 错误情况下显式设置 + isConnecting.set(false) } } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { isConnected = false isSessionStarted = false - errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") + isSpeaking = false + isConnecting.set(false) + + Log.e(TAG, "WebSocket连接失败: ${t.message}") + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "CONNECTION_ERROR", + "errorMessage" to (t.message ?: "连接失败"))) updateStatus(STATUS_ERROR) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { isConnected = false isSessionStarted = false + isSpeaking = false + isConnecting.set(false) updateStatus(STATUS_STOPPED) - println("TTS WebSocket连接关闭: $code, $reason") + notifyEvent(TtsEventType.SYNTHESIS_CANCELED) } }) } @@ -477,30 +775,25 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_START_SESSION) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - addProperty("enable_timestamp", enableTimestamp) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_START_SESSION) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) + audioParams.put("enable_timestamp", enableTimestamp) - val payload = payloadObj.toString().toByteArray() + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) + + val payload = jsonObject.toString().toByteArray() sendEvent(webSocket, header, optional, payload) } @@ -525,36 +818,31 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_TASK_REQUEST) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("text", text) - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + // 将#和*替换为空格 + val processedText = text.replace("#", " ").replace("*", " ") + reqParams.put("text", processedText) + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_TASK_REQUEST) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) + audioParams.put("emotion", "happy") - val payload = payloadObj.toString().toByteArray() + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) - sendEvent(ws, header, optional, payload) + val payload = jsonObject.toString().toByteArray() + Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}") - // 发送完文本后结束会话 - updateStatus(STATUS_SPEAKING) - finishSession() + sendEvent(ws, header, optional, payload) } } @@ -578,30 +866,29 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_TASK_REQUEST) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("text", text) - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + // 将#和*替换为空格 + val processedText = text.replace("#", " ").replace("*", " ") + reqParams.put("text", processedText) + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_TASK_REQUEST) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) + audioParams.put("emotion", "happy") - val payload = payloadObj.toString().toByteArray() + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) + + val payload = jsonObject.toString().toByteArray() + // Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}") sendEvent(ws, header, optional, payload) // 流式模式下不自动结束会话 @@ -631,6 +918,9 @@ class BytedanceTTS( val payload = "{}".toByteArray() sendEvent(ws, header, optional, payload) + Log.d(TAG, "发送结束会话请求,会话ID: $sessionId") + // 不在这里设置 isSessionStarted = false + // 等待服务器的 SESSION_FINISHED 事件来设置 } } @@ -638,27 +928,31 @@ class BytedanceTTS( * 发送事件 */ private fun sendEvent(webSocket: WebSocket, header: Header, optional: Optional, payload: ByteArray) { - val headerBytes = header.getBytes() - val optionalBytes = optional.getBytes() - val payloadSizeBytes = intToBytes(payload.size) - - val requestBytes = ByteArray( - headerBytes.size + optionalBytes.size + payloadSizeBytes.size + payload.size - ) - var offset = 0 - - System.arraycopy(headerBytes, 0, requestBytes, offset, headerBytes.size) - offset += headerBytes.size - - System.arraycopy(optionalBytes, 0, requestBytes, offset, optionalBytes.size) - offset += optionalBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - System.arraycopy(payload, 0, requestBytes, offset, payload.size) - - webSocket.send(ByteString.of(requestBytes)) + try { + val headerBytes = header.getBytes() + val optionalBytes = optional.getBytes() + val payloadSizeBytes = intToBytes(payload.size) + + val requestBytes = ByteArray( + headerBytes.size + optionalBytes.size + payloadSizeBytes.size + payload.size + ) + var offset = 0 + + System.arraycopy(headerBytes, 0, requestBytes, offset, headerBytes.size) + offset += headerBytes.size + + System.arraycopy(optionalBytes, 0, requestBytes, offset, optionalBytes.size) + offset += optionalBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + System.arraycopy(payload, 0, requestBytes, offset, payload.size) + + webSocket.send(ByteString.of(*requestBytes)) + } catch (e: Exception) { + Log.e(TAG, "发送WebSocket消息失败: ${e.message}") + } } /** @@ -935,4 +1229,5 @@ class BytedanceTTS( return result } } + } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/.gitkeep b/local_plugins/chat_storage/ios/Classes/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift index d0702e65c..e008b2e78 100644 --- a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift @@ -1,410 +1,237 @@ import Foundation import SQLite3 -/// ChatStorageHelper - iOS版本的聊天存储助手 -/// 负责SQLite数据库操作和业务逻辑 +/** + * ChatStorageHelper - 聊天数据存储助手 + * 负责处理SQLite数据库操作,提供消息存储和检索功能 + */ class ChatStorageHelper { - // MARK: - Constants - private let TAG = "ChatStorageHelper" - - // 数据库常量 - private let DATABASE_NAME = "chat_messages.db" - private let DATABASE_VERSION = 1 - - // 表和列名 - private let TABLE_MESSAGES = "messages" - private let COLUMN_ID = "id" - private let COLUMN_SESSION_ID = "session_id" - private let COLUMN_MESSAGE = "message" - private let COLUMN_SENDER = "sender" - private let COLUMN_TIMESTAMP = "timestamp" - private let COLUMN_METADATA = "metadata" - - // MARK: - Properties private var db: OpaquePointer? private let dbPath: String - // MARK: - Initialization - init(_ context: Any) { - // 获取数据库文件路径 - let fileManager = FileManager.default - if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - let databaseDirectory = documentsDirectory.appendingPathComponent("Databases") - - // 确保目录存在 - try? fileManager.createDirectory(at: databaseDirectory, withIntermediateDirectories: true) - - dbPath = databaseDirectory.appendingPathComponent(DATABASE_NAME).path - - // 打开数据库连接 - openDatabase() - } else { - dbPath = "" - print("\(TAG) 无法获取文档目录路径") - } - } - - deinit { - closeDatabase() - } - - // MARK: - Database Operations - - /// 打开数据库连接 - private func openDatabase() { - if sqlite3_open(dbPath, &db) == SQLITE_OK { - createTables() - print("\(TAG) 数据库已成功打开") - } else { - print("\(TAG) 无法打开数据库: \(String(describing: errorMessage))") - } - } - - /// 关闭数据库连接 - private func closeDatabase() { - if db != nil { - sqlite3_close(db) - db = nil + init() { + // 获取文档目录路径 + let fileURL = try! FileManager.default + .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + .appendingPathComponent("chat_storage.db") + + dbPath = fileURL.path + + // 打开数据库 + if sqlite3_open(dbPath, &db) != SQLITE_OK { + print("无法打开数据库") + return } - } - - /// 创建必要的数据库表 - private func createTables() { + // 创建消息表 - let createTableSQL = """ - CREATE TABLE IF NOT EXISTS \(TABLE_MESSAGES) ( - \(COLUMN_ID) INTEGER PRIMARY KEY AUTOINCREMENT, - \(COLUMN_SESSION_ID) TEXT NOT NULL, - \(COLUMN_MESSAGE) TEXT NOT NULL, - \(COLUMN_SENDER) TEXT NOT NULL, - \(COLUMN_TIMESTAMP) TEXT NOT NULL, - \(COLUMN_METADATA) TEXT - ); + let createTableString = """ + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message TEXT NOT NULL, + sender TEXT NOT NULL, + metadata TEXT, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); + CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); """ - if executeUpdate(sql: createTableSQL) { - // 创建索引以加快查询速度 - executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_session_id ON \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID))") - executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_timestamp ON \(TABLE_MESSAGES) (\(COLUMN_TIMESTAMP))") - } - } - - /// 获取SQLite错误消息 - private var errorMessage: String { - if let errorPointer = sqlite3_errmsg(db) { - let errorMessage = String(cString: errorPointer) - return errorMessage - } else { - return "未知错误" + if sqlite3_exec(db, createTableString, nil, nil, nil) != SQLITE_OK { + let errmsg = String(cString: sqlite3_errmsg(db)!) + print("创建表失败: \(errmsg)") } } - /// 执行SQL更新操作(INSERT, UPDATE, DELETE, CREATE等) - private func executeUpdate(sql: String, parameters: [Any]? = nil) -> Bool { - var statement: OpaquePointer? - - if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { - // 绑定参数 - if let params = parameters { - for (index, param) in params.enumerated() { - bindParameter(statement: statement, index: index + 1, value: param) - } - } - - // 执行语句 - let result = sqlite3_step(statement) - sqlite3_finalize(statement) - - return result == SQLITE_DONE - } else { - print("\(TAG) 准备SQL语句失败: \(errorMessage)") - return false - } - } - - /// 绑定SQL参数 - private func bindParameter(statement: OpaquePointer?, index: Int, value: Any) { - if let value = value as? Int { - sqlite3_bind_int(statement, Int32(index), Int32(value)) - } else if let value = value as? Double { - sqlite3_bind_double(statement, Int32(index), value) - } else if let value = value as? String { - sqlite3_bind_text(statement, Int32(index), (value as NSString).utf8String, -1, nil) - } else if value is NSNull { - sqlite3_bind_null(statement, Int32(index)) - } - } - - /// 查询数据并处理结果 - private func executeQuery(sql: String, parameters: [Any]? = nil, handler: (OpaquePointer) -> Void) { - var statement: OpaquePointer? - - if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { - // 绑定参数 - if let params = parameters { - for (index, param) in params.enumerated() { - bindParameter(statement: statement, index: index + 1, value: param) - } - } - - // 处理结果 - while sqlite3_step(statement) == SQLITE_ROW { - handler(statement!) - } - } else { - print("\(TAG) 执行查询失败: \(errorMessage)") + deinit { + if db != nil { + sqlite3_close(db) } - - sqlite3_finalize(statement) } - // MARK: - Public Methods - - /// 保存消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - message: 消息内容 - /// - sender: 发送者 - /// - metadata: 元数据 - /// - Returns: 成功返回消息ID,失败返回-1 - func saveMessage(sessionId: String, message: String, sender: String, metadata: String? = nil) -> Int64 { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - let timestamp = dateFormatter.string(from: Date()) - - let sql = """ - INSERT INTO \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID), \(COLUMN_MESSAGE), \(COLUMN_SENDER), \(COLUMN_TIMESTAMP), \(COLUMN_METADATA)) - VALUES (?, ?, ?, ?, ?) - """ - - var parameters: [Any] = [sessionId, message, sender, timestamp] - if let metadata = metadata { - parameters.append(metadata) + /** + * 保存消息到数据库 + * @param sessionId 会话ID + * @param message 消息内容 + * @param sender 发送者("user" 或 "agent") + * @param metadata 可选的元数据 + * @return 插入的消息ID,失败则返回-1 + */ + func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);" + var insertStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) + + if let metadata = metadata { + sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil) + } else { + sqlite3_bind_null(insertStatement, 4) + } + + let currentTime = Int(Date().timeIntervalSince1970) + sqlite3_bind_int(insertStatement, 5, Int32(currentTime)) + + if sqlite3_step(insertStatement) == SQLITE_DONE { + let id = sqlite3_last_insert_rowid(db) + sqlite3_finalize(insertStatement) + return id + } else { + print("插入消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } + + sqlite3_finalize(insertStatement) } else { - parameters.append(NSNull()) + print("插入消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") } - if executeUpdate(sql: sql, parameters: parameters) { - return sqlite3_last_insert_rowid(db) - } else { - print("\(TAG) 保存消息失败") - return -1 - } + return -1 } - /// 获取指定会话的消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - page: 页码 - /// - pageSize: 每页大小 - /// - Returns: JSON字符串,包含消息列表和分页信息 - func getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20) -> [String: Any] { - var messages: [[String: Any]] = [] + /** + * 获取特定会话的消息,支持分页 + * @param sessionId 会话ID + * @param page 页码(从1开始) + * @param pageSize 每页消息数量 + * @return 消息列表的JSON字符串 + */ + func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { let offset = (page - 1) * pageSize - - // 查询消息 - let sql = """ - SELECT * FROM \(TABLE_MESSAGES) - WHERE \(COLUMN_SESSION_ID) = ? - ORDER BY \(COLUMN_TIMESTAMP) DESC - LIMIT ? OFFSET ? + var messagesArray: [[String: Any]] = [] + + let queryString = """ + SELECT id, message, sender, metadata, created_at + FROM messages + WHERE session_id = ? + ORDER BY created_at DESC + LIMIT ? OFFSET ? """ - executeQuery(sql: sql, parameters: [sessionId, pageSize, offset]) { statement in - var message: [String: Any] = [:] - - // 获取ID - message["id"] = Int(sqlite3_column_int(statement, 0)) - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 1) { - message["sessionId"] = String(cString: sessionId) - } - - // 获取消息内容 - if let messageText = sqlite3_column_text(statement, 2) { - message["message"] = String(cString: messageText) - } + var queryStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_int(queryStatement, 2, Int32(pageSize)) + sqlite3_bind_int(queryStatement, 3, Int32(offset)) - // 获取发送者 - if let sender = sqlite3_column_text(statement, 3) { - message["sender"] = String(cString: sender) + while sqlite3_step(queryStatement) == SQLITE_ROW { + let id = sqlite3_column_int(queryStatement, 0) + + let messagePtr = sqlite3_column_text(queryStatement, 1) + let message = messagePtr != nil ? String(cString: messagePtr!) : "" + + let senderPtr = sqlite3_column_text(queryStatement, 2) + let sender = senderPtr != nil ? String(cString: senderPtr!) : "" + + let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil + + let createdAt = sqlite3_column_int(queryStatement, 4) + + var messageDict: [String: Any] = [ + "id": id, + "message": message, + "sender": sender, + "created_at": createdAt + ] + + if let metadata = metadata { + messageDict["metadata"] = metadata + } + + messagesArray.append(messageDict) } - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 4) { - message["timestamp"] = String(cString: timestamp) - } + sqlite3_finalize(queryStatement) - // 获取元数据(如果有) - if sqlite3_column_type(statement, 5) != SQLITE_NULL { - if let metadata = sqlite3_column_text(statement, 5) { - message["metadata"] = String(cString: metadata) + do { + let jsonData = try JSONSerialization.data(withJSONObject: messagesArray, options: []) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } + } catch { + print("转换消息为JSON失败: \(error.localizedDescription)") } - - messages.append(message) - } - - // 获取总记录数 - var totalCount = 0 - let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" - - executeQuery(sql: countSql, parameters: [sessionId]) { statement in - totalCount = Int(sqlite3_column_int(statement, 0)) + } else { + print("查询消息失败: \(String(cString: sqlite3_errmsg(db)!))") } - // 计算总页数 - let totalPages = (totalCount + pageSize - 1) / pageSize - - return [ - "messages": messages, - "page": page, - "pageSize": pageSize, - "totalCount": totalCount, - "totalPages": totalPages - ] + return "[]" } - /// 获取会话列表 - /// - Returns: 会话列表数据 - func getSessions() -> [[String: Any]] { - var sessions: [[String: Any]] = [] - - // 查询不同的会话ID及其最新消息 - let sql = """ - SELECT s1.\(COLUMN_SESSION_ID), s1.\(COLUMN_MESSAGE), s1.\(COLUMN_SENDER), s1.\(COLUMN_TIMESTAMP) - FROM \(TABLE_MESSAGES) s1 - INNER JOIN ( - SELECT \(COLUMN_SESSION_ID), MAX(\(COLUMN_TIMESTAMP)) as max_timestamp - FROM \(TABLE_MESSAGES) - GROUP BY \(COLUMN_SESSION_ID) - ) s2 ON s1.\(COLUMN_SESSION_ID) = s2.\(COLUMN_SESSION_ID) AND s1.\(COLUMN_TIMESTAMP) = s2.max_timestamp - ORDER BY s1.\(COLUMN_TIMESTAMP) DESC - """ - - executeQuery(sql: sql) { statement in - var session: [String: Any] = [:] - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 0) { - let sessionIdStr = String(cString: sessionId) - session["sessionId"] = sessionIdStr + + + /** + * 删除消息 + * @param sessionId 可选的会话ID,指定则删除该会话所有消息 + * @param messageIds 可选的消息ID列表,指定则删除特定消息 + * @return 是否删除成功 + */ + func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { + if let sessionId = sessionId { + let deleteString = "DELETE FROM messages WHERE session_id = ?;" + var deleteStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + sqlite3_bind_text(deleteStatement, 1, (sessionId as NSString).utf8String, -1, nil) - // 获取该会话的消息数量 - var messageCount = 0 - let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" + if sqlite3_step(deleteStatement) == SQLITE_DONE { + sqlite3_finalize(deleteStatement) + return true + } else { + print("删除会话消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } - executeQuery(sql: countSql, parameters: [sessionIdStr]) { countStmt in - messageCount = Int(sqlite3_column_int(countStmt, 0)) + sqlite3_finalize(deleteStatement) + } else { + print("删除会话消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") + } + } else if let messageIds = messageIds, !messageIds.isEmpty { + // 构建参数占位符 + let placeholders = String(repeating: "?,", count: messageIds.count).dropLast() + let deleteString = "DELETE FROM messages WHERE id IN (\(placeholders));" + var deleteStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + for (index, id) in messageIds.enumerated() { + sqlite3_bind_int(deleteStatement, Int32(index + 1), Int32(id)) } - session["messageCount"] = messageCount - } - - // 获取最后一条消息 - if let message = sqlite3_column_text(statement, 1) { - session["lastMessage"] = String(cString: message) - } - - // 获取最后发送者 - if let sender = sqlite3_column_text(statement, 2) { - session["lastSender"] = String(cString: sender) - } - - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 3) { - session["timestamp"] = String(cString: timestamp) + if sqlite3_step(deleteStatement) == SQLITE_DONE { + sqlite3_finalize(deleteStatement) + return true + } else { + print("删除特定消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } + + sqlite3_finalize(deleteStatement) + } else { + print("删除特定消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") } - - sessions.append(session) - } - - return sessions - } - - /// 删除消息 - /// - Parameters: - /// - sessionId: 会话ID,如果提供则删除整个会话 - /// - messageIds: 消息ID列表,如果提供则删除指定消息 - /// - Returns: 成功返回true,失败返回false - func deleteMessages(sessionId: String? = nil, messageIds: [Int]? = nil) -> Bool { - if let messageIds = messageIds, !messageIds.isEmpty { - // 删除指定ID的消息 - let placeholders = Array(repeating: "?", count: messageIds.count).joined(separator: ",") - let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_ID) IN (\(placeholders))" - - return executeUpdate(sql: sql, parameters: messageIds.map { $0 }) - } else if let sessionId = sessionId, !sessionId.isEmpty { - // 删除会话中的所有消息 - let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" - return executeUpdate(sql: sql, parameters: [sessionId]) + } else { + return false } return false } - /// 清空数据库 - /// - Returns: 成功返回true,失败返回false + /** + * 清空数据库 + * @return 是否清空成功 + */ func clearDatabase() -> Bool { - let sql = "DELETE FROM \(TABLE_MESSAGES)" - return executeUpdate(sql: sql) - } - - /// 获取指定会话的最近消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - limit: 限制数量 - /// - Returns: 消息列表,按时间正序排列 - func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { - var messages: [[String: Any]] = [] - - // 查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 - let sql = """ - SELECT * FROM ( - SELECT * FROM \(TABLE_MESSAGES) - WHERE \(COLUMN_SESSION_ID) = ? - ORDER BY \(COLUMN_TIMESTAMP) DESC - LIMIT ? - ) tmp ORDER BY \(COLUMN_TIMESTAMP) ASC - """ + let deleteString = "DELETE FROM messages;" - executeQuery(sql: sql, parameters: [sessionId, limit]) { statement in - var message: [String: Any] = [:] - - // 获取ID - message["id"] = Int(sqlite3_column_int(statement, 0)) - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 1) { - message["sessionId"] = String(cString: sessionId) - } - - // 获取消息内容 - if let messageText = sqlite3_column_text(statement, 2) { - message["message"] = String(cString: messageText) - } - - // 获取发送者 - if let sender = sqlite3_column_text(statement, 3) { - message["sender"] = String(cString: sender) - } - - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 4) { - message["timestamp"] = String(cString: timestamp) - } - - // 获取元数据(如果有) - if sqlite3_column_type(statement, 5) != SQLITE_NULL { - if let metadata = sqlite3_column_text(statement, 5) { - message["metadata"] = String(cString: metadata) - } - } - - messages.append(message) + if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK { + return true + } else { + print("清空数据库失败: \(String(cString: sqlite3_errmsg(db)!))") + return false } - - return messages } + + } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift b/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift deleted file mode 100644 index 031cb370f..000000000 --- a/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift +++ /dev/null @@ -1,182 +0,0 @@ -import Flutter -import UIKit - -public class ChatStoragePlugin: NSObject, FlutterPlugin { - private let TAG = "ChatStoragePlugin" - private var chatStorageHelper: ChatStorageHelper? - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) - let instance = ChatStoragePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - override init() { - super.init() - chatStorageHelper = ChatStorageHelper(()) - print("\(TAG) 聊天存储插件已加载") - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let helper = chatStorageHelper else { - result(FlutterError(code: "NOT_INITIALIZED", message: "存储助手未初始化", details: nil)) - return - } - - switch call.method { - case "saveMessage": - handleSaveMessage(call, result, helper) - - case "getMessages": - handleGetMessages(call, result, helper) - - case "getSessions": - handleGetSessions(call, result, helper) - - case "deleteMessages": - handleDeleteMessages(call, result, helper) - - case "clearDatabase": - handleClearDatabase(call, result, helper) - - case "getRecentMessages": - handleGetRecentMessages(call, result, helper) - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - Method handlers - - private func handleSaveMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String, - let message = arguments["message"] as? String, - let sender = arguments["sender"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少必要参数", details: nil)) - return - } - - let metadata = arguments["metadata"] as? String - - do { - let id = helper.saveMessage(sessionId: sessionId, message: message, sender: sender, metadata: metadata) - - if id != -1 { - result(["success": true, "id": id]) - } else { - result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) - } - } catch { - print("\(TAG) 保存消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) - return - } - - let page = arguments["page"] as? Int ?? 1 - let pageSize = arguments["pageSize"] as? Int ?? 20 - - do { - let messagesData = helper.getMessages(sessionId: sessionId, page: page, pageSize: pageSize) - - // 将结果转换为JSON字符串 - if let jsonData = try? JSONSerialization.data(withJSONObject: messagesData), - let jsonString = String(data: jsonData, encoding: .utf8) { - result(jsonString) - } else { - result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) - } - } catch { - print("\(TAG) 获取消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetSessions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - do { - let sessions = helper.getSessions() - - // 将结果转换为JSON字符串 - if let jsonData = try? JSONSerialization.data(withJSONObject: sessions), - let jsonString = String(data: jsonData, encoding: .utf8) { - result(jsonString) - } else { - result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) - } - } catch { - print("\(TAG) 获取会话列表时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取会话列表时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleDeleteMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - let sessionId = arguments["sessionId"] as? String - let messageIds = arguments["messageIds"] as? [Int] - - do { - let success = helper.deleteMessages(sessionId: sessionId, messageIds: messageIds) - - if success { - result(["success": true]) - } else { - result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) - } - } catch { - print("\(TAG) 删除消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleClearDatabase(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - do { - let success = helper.clearDatabase() - result(["success": success]) - } catch { - print("\(TAG) 清空数据库时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetRecentMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) - return - } - - let limit = arguments["limit"] as? Int ?? 10 - - do { - let messages = helper.getRecentMessages(sessionId: sessionId, limit: limit) - result(messages) - } catch { - print("\(TAG) 获取最近消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取最近消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } -} \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift index bd72a61bc..daf865444 100644 --- a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift +++ b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift @@ -1,11 +1,119 @@ import Flutter import UIKit -// 这个类是为了满足Flutter插件的命名约定 -// 实际实现在ChatStoragePlugin.swift中 +/** + * SwiftChatStoragePlugin - Flutter插件接口 + * 负责处理Flutter端的方法调用,并委托给ChatStorageHelper + */ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用实际实现类的注册方法 - ChatStoragePlugin.register(with: registrar) - } + private let TAG = "ChatStoragePlugin" + private var chatStorageHelper: ChatStorageHelper! + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) + let instance = SwiftChatStoragePlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + print("\(instance.TAG): 聊天存储插件已加载") + } + + override init() { + super.init() + chatStorageHelper = ChatStorageHelper() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "saveMessage": + do { + guard let args = call.arguments as? [String: Any], + let sessionId = args["sessionId"] as? String, + let message = args["message"] as? String, + let sender = args["sender"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let metadata = args["metadata"] as? String + + let id = chatStorageHelper.saveMessage( + sessionId: sessionId, + message: message, + sender: sender, + metadata: metadata + ) + + if id != -1 { + result(["success": true, "id": id]) + } else { + result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) + } + } catch { + print("\(TAG): 保存消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + case "getMessages": + do { + guard let args = call.arguments as? [String: Any], + let sessionId = args["sessionId"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let page = args["page"] as? Int ?? 1 + let pageSize = args["pageSize"] as? Int ?? 20 + + let messagesJson = chatStorageHelper.getMessages( + sessionId: sessionId, + page: page, + pageSize: pageSize + ) + + result(messagesJson) + } catch { + print("\(TAG): 获取消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + + case "deleteMessages": + do { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let sessionId = args["sessionId"] as? String + let messageIds = args["messageIds"] as? [Int] + + let success = chatStorageHelper.deleteMessages( + sessionId: sessionId, + messageIds: messageIds + ) + + if success { + result(["success": true]) + } else { + result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) + } + } catch { + print("\(TAG): 删除消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + case "clearDatabase": + do { + let success = chatStorageHelper.clearDatabase() + result(["success": success]) + } catch { + print("\(TAG): 清空数据库时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) + } + + + default: + result(FlutterMethodNotImplemented) + } + } } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/README.md b/local_plugins/chat_storage/ios/README.md index 4bf35eb37..3d685d41e 100644 --- a/local_plugins/chat_storage/ios/README.md +++ b/local_plugins/chat_storage/ios/README.md @@ -1,64 +1,47 @@ # Chat Storage 插件 - iOS 实现 -## 概述 +这是 Chat Storage 插件的 iOS 实现,用于本地存储聊天消息。 -Chat Storage 插件的 iOS 实现,为 Flutter 应用提供本地 SQLite 数据库支持,用于存储和管理聊天记录。 +## 功能 -## 功能特性 +- 保存消息到本地 SQLite 数据库 +- 分页获取会话消息 +- 获取会话列表 +- 删除指定会话或消息 +- 清空数据库 +- 获取最近消息 -- 基于 SQLite 的聊天记录存储 -- 支持按会话分组消息 -- 提供分页查询功能 -- 支持元数据存储 -- 完整的增删改查 API +## 实现细节 -## 技术实现 +- 使用 Swift 语言编写 +- 使用原生 SQLite3 API 进行数据库操作 +- 数据库文件位置:应用文档目录下的 `chat_storage.db` +- 表结构: + ```sql + CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message TEXT NOT NULL, + sender TEXT NOT NULL, + metadata TEXT, + created_at INTEGER NOT NULL + ); + ``` -本插件使用原生 Swift 实现,通过 SQLite C API 直接操作数据库,不依赖第三方 SQLite 封装库,以保持最小的依赖要求和最佳性能。 +## 构建要求 -### 核心组件 +- iOS 12.0+ +- Xcode 14.0+ +- Swift 5.0+ -- `ChatStorageHelper`: 负责 SQLite 数据库操作,提供完整的数据操作 API -- `ChatStoragePlugin`: 插件主类,处理与 Flutter 的通信 +## 安装 -### 数据库结构 +该插件已包含在 Flutter 应用的依赖中,不需要单独安装。 -数据库包含一个 `messages` 表,结构如下: +## 开发者 -```sql -CREATE TABLE messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - message TEXT NOT NULL, - sender TEXT NOT NULL, - timestamp TEXT NOT NULL, - metadata TEXT -); +若需修改插件代码,请确保: --- 索引 -CREATE INDEX idx_session_id ON messages (session_id); -CREATE INDEX idx_timestamp ON messages (timestamp); -``` - -## 使用注意事项 - -### SQLite 多线程访问 - -本插件所有数据库操作均在主线程执行,以避免 SQLite 多线程访问的复杂性。在处理大量数据时,建议在 Flutter 层面使用 `compute` 函数或 `Isolate` 进行异步处理。 - -### 数据格式 - -- 时间戳使用 `yyyy-MM-dd HH:mm:ss` 格式的字符串 -- JSON 转换遵循 Swift/Objective-C 的类型映射规则 - -## 调试与故障排除 - -- 日志前缀为 `ChatStoragePlugin` 和 `ChatStorageHelper` -- 可通过 Console.app 查看日志输出 -- 数据库文件存储在应用的 Documents/Databases 目录下 - -## 开发者须知 - -如果需要扩展此插件的功能,比如添加新的表或字段,应同时更新 Android 和 iOS 端的实现,并确保在版本升级时提供适当的数据库迁移策略。 - -数据库迁移逻辑目前实现为重建表,在生产环境中应该改为保留用户数据的方式。 \ No newline at end of file +1. 修改后运行 `flutter clean` 和 `flutter pub get` 重新加载插件 +2. 若修改了数据库结构,需要考虑数据迁移方案 +3. 如需测试,可以使用 `pod lib lint` 验证插件的 iOS 部分 \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/chat_storage.podspec b/local_plugins/chat_storage/ios/chat_storage.podspec index c397034ec..58ce1adf1 100644 --- a/local_plugins/chat_storage/ios/chat_storage.podspec +++ b/local_plugins/chat_storage/ios/chat_storage.podspec @@ -5,22 +5,20 @@ Pod::Spec.new do |s| s.name = 'chat_storage' s.version = '0.0.1' - s.summary = '聊天记录存储插件' + s.summary = '聊天消息本地存储插件' s.description = <<-DESC -用于存储聊天记录的Flutter插件,支持SQLite数据库操作。 +聊天消息本地存储插件,提供基于SQLite的消息存储和检索功能。 DESC - s.homepage = 'http://yunqiinnovation.com' + s.homepage = 'https://yunqiinnovation.com' s.license = { :file => '../LICENSE' } - s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } + s.author = { '云奇创新' => 'info@yunqiinnovation.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' - - # 添加SQLite库依赖 - s.library = 'sqlite3' end \ No newline at end of file diff --git a/local_plugins/chat_storage/lib/chat_storage.dart b/local_plugins/chat_storage/lib/chat_storage.dart index 66e229b5f..7e474e97a 100644 --- a/local_plugins/chat_storage/lib/chat_storage.dart +++ b/local_plugins/chat_storage/lib/chat_storage.dart @@ -44,6 +44,8 @@ class ChatMessage { } } + + /// 分页结果数据模型 class PagedResult { final List messages; @@ -123,6 +125,8 @@ class ChatStorage { } } + + /// 删除聊天记录,可按会话ID或消息ID列表删除 static Future> deleteMessages({ String? sessionId, @@ -152,4 +156,6 @@ class ChatStorage { return {'success': false, 'error': e.toString()}; } } + + } diff --git a/local_plugins/classic_bluetooth/LICENSE b/local_plugins/classic_bluetooth/LICENSE new file mode 100644 index 000000000..087369e4f --- /dev/null +++ b/local_plugins/classic_bluetooth/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 YunQi Innovation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt index c6b632deb..16c4a8279 100644 --- a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt +++ b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt @@ -15,7 +15,6 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import com.yunqiinnovation.deepvoice_utils.FileLogger /** ClassicBluetoothPlugin */ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { @@ -119,7 +118,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { sendEvent(eventMap) } } catch (e: Exception) { - FileLogger.e(TAG, "蓝牙状态获取失败", e) + Log.e(TAG, "蓝牙状态获取失败", e) } } @@ -188,7 +187,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { eventSink?.success(event) } catch (e: Exception) { - FileLogger.e(TAG, "事件发送失败", e) + Log.e(TAG, "事件发送失败", e) } } } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift index a9aa9b3b5..f9a9c782c 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift @@ -1,6 +1,6 @@ import Foundation -import ExternalAccessory import CoreBluetooth +import AVFoundation /// 蓝牙事件类型 enum BluetoothEventType { @@ -14,113 +14,205 @@ enum BluetoothState: String { case unknown = "unknown" case off = "off" case on = "on" - case turningOn = "turningOn" - case turningOff = "turningOff" + case unauthorized = "unauthorized" + case unsupported = "unsupported" + case resetting = "resetting" } -/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理 +/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测 class ClassicBluetoothHelper: NSObject { - private let TAG = "ClassicBluetoothHelper" - // 状态监控相关 private var isMonitoring = false private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? - // 用于监控蓝牙系统状态 + // 用于监控蓝牙系统状态,延迟初始化 private var centralManager: CBCentralManager? - // 通知观察者 - private var accessoryConnectionObserver: NSObjectProtocol? - private var accessoryDisconnectionObserver: NSObjectProtocol? + // 音频路由变化观察者 + private var routeChangeObserver: NSObjectProtocol? override init() { super.init() - centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false]) + // 初始化时不立即创建centralManager,延迟到实际需要时 } deinit { stopBluetoothStateMonitoring() } - /// 检查蓝牙是否启用 + /// 延迟初始化CBCentralManager,只在实际需要时创建 + private func initializeCentralManagerIfNeeded() { + if centralManager == nil { + centralManager = CBCentralManager( + delegate: self, + queue: DispatchQueue.main, + options: [CBCentralManagerOptionShowPowerAlertKey: true] + ) + } + } + + /// 检查蓝牙是否启用,此方法会触发权限请求 func isBluetoothEnabled() -> Bool { - guard let centralManager = centralManager else { return false } - return centralManager.state == .poweredOn + // 此方法调用会触发权限申请 + initializeCentralManagerIfNeeded() + + guard let central = centralManager else { + print("检查蓝牙状态失败:centralManager未初始化") + return false + } + + // 检查蓝牙状态并记录 + let stateDescription: String + let isEnabled: Bool + + switch central.state { + case .poweredOn: + stateDescription = "已开启" + isEnabled = true + case .poweredOff: + stateDescription = "已关闭" + isEnabled = false + case .unauthorized: + stateDescription = "未授权" + isEnabled = false + case .unsupported: + stateDescription = "不支持" + isEnabled = false + case .resetting: + stateDescription = "重置中" + isEnabled = false + case .unknown: + stateDescription = "未知" + isEnabled = false + @unknown default: + stateDescription = "未定义状态" + isEnabled = false + } + + print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)") + return isEnabled } - /// 获取已连接的蓝牙设备 - /// - Parameter completion: 完成回调 + /// 获取已连接的蓝牙耳机,此方法会触发权限请求 func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { - // 获取所有已连接的外部配件 - let accessoryManager = EAAccessoryManager.shared() - let connectedAccessories = accessoryManager.connectedAccessories - - // 将配件转换为设备信息数组 - let devices = connectedAccessories.map { accessory -> [String: String] in - return [ - "name": accessory.name, - "address": accessory.serialNumber // iOS使用序列号作为唯一标识 + // 确保蓝牙已初始化 + initializeCentralManagerIfNeeded() + + // 获取音频设备(主要是蓝牙耳机) + let audioDevices = getConnectedAudioDevices() + + // 日志输出 + if !audioDevices.isEmpty { + print("已找到\(audioDevices.count)个蓝牙音频设备:") + for device in audioDevices { + print("- \(device["name"] ?? "未知") (\(device["address"] ?? "未知地址"))") + } + } else { + print("未找到蓝牙音频设备") + } + + completion(audioDevices, nil) + } + + // 获取连接的蓝牙音频设备 + private func getConnectedAudioDevices() -> [[String: String]] { + var result: [[String: String]] = [] + + // 获取当前音频路由 + let outputs = AVAudioSession.sharedInstance().currentRoute.outputs + + // 过滤蓝牙相关设备 + for output in outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) ] + result.append(device) } - // 返回结果 - completion(devices, nil) + return result + } + + // 检查是否为蓝牙输出 + private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { + return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE + } + + // 清理设备地址 + private func cleanDeviceAddress(_ address: String) -> String { + // 移除类似 "-tacl" 的后缀 + if let range = address.range(of: "-", options: .backwards) { + return String(address[.. Void) { self.stateCallback = callback if !isMonitoring { isMonitoring = true - // 设置蓝牙中央管理器代理 - centralManager?.delegate = self + // 初始化蓝牙管理器 + initializeCentralManagerIfNeeded() - // 注册蓝牙配件连接/断开通知 + // 观察音频路由变化 let notificationCenter = NotificationCenter.default - - // 观察配件连接 - accessoryConnectionObserver = notificationCenter.addObserver( - forName: NSNotification.Name.EAAccessoryDidConnect, + routeChangeObserver = notificationCenter.addObserver( + forName: AVAudioSession.routeChangeNotification, object: nil, queue: nil ) { [weak self] notification in - guard let self = self, - let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { - return - } - - let device: [String: String] = [ - "name": accessory.name, - "address": accessory.serialNumber - ] - - self.stateCallback?(.deviceConnected, ["device": device]) + self?.handleAudioRouteChange(notification) } - // 观察配件断开 - accessoryDisconnectionObserver = notificationCenter.addObserver( - forName: NSNotification.Name.EAAccessoryDidDisconnect, - object: nil, - queue: nil - ) { [weak self] notification in - guard let self = self, - let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { - return + // 激活音频会话 + activateAudioSession() + } + } + + // 处理音频路由变化 + private func handleAudioRouteChange(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { + return + } + + switch reason { + case .newDeviceAvailable: + // 新音频设备已连接 + let audioDevices = getConnectedAudioDevices() + for device in audioDevices { + stateCallback?(.deviceConnected, ["device": device]) + } + + case .oldDeviceUnavailable: + // 获取断开之前的路由 + if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { + for output in previousRoute.outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) + ] + stateCallback?(.deviceDisconnected, ["device": device]) } - - let device: [String: String] = [ - "name": accessory.name, - "address": accessory.serialNumber - ] - - self.stateCallback?(.deviceDisconnected, ["device": device]) } - // 开始监视配件变化 - EAAccessoryManager.shared().registerForLocalNotifications() + default: + break + } + } + + // 激活音频会话 + private func activateAudioSession() { + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback, mode: .default) + try audioSession.setActive(true) + } catch { + print("设置音频会话失败: \(error.localizedDescription)") } } @@ -129,48 +221,36 @@ class ClassicBluetoothHelper: NSObject { if isMonitoring { isMonitoring = false - // 移除蓝牙中央管理器代理 - centralManager?.delegate = nil - - // 注销配件通知观察者 - if let observer = accessoryConnectionObserver { + // 移除音频路由观察者 + if let observer = routeChangeObserver { NotificationCenter.default.removeObserver(observer) - accessoryConnectionObserver = nil + routeChangeObserver = nil } - if let observer = accessoryDisconnectionObserver { - NotificationCenter.default.removeObserver(observer) - accessoryDisconnectionObserver = nil - } - - // 停止监视配件变化 - EAAccessoryManager.shared().unregisterForLocalNotifications() + // 关闭音频会话 + deactivateAudioSession() } // 清除回调 stateCallback = nil } + // 关闭音频会话 + private func deactivateAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print("关闭音频会话失败: \(error.localizedDescription)") + } + } + /// 报告当前蓝牙状态 func reportCurrentBluetoothState() { + // 如果centralManager未初始化,不触发状态报告 guard let centralManager = centralManager else { return } - let state: BluetoothState - - switch centralManager.state { - case .poweredOn: - state = .on - case .poweredOff: - state = .off - case .resetting: - state = .turningOff // 最接近的近似值 - case .unauthorized, .unsupported: - state = .off // 功能上等同于关闭 - default: - state = .unknown - } - - stateCallback?(.stateChanged, ["state": state.rawValue]) + // 直接调用delegate方法保持状态处理一致性 + centralManagerDidUpdateState(centralManager) } } @@ -179,19 +259,47 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { let state: BluetoothState + // 添加详细日志输出 + print("iOS蓝牙状态原始值: \(central.state.rawValue)") + switch central.state { case .poweredOn: + print("蓝牙状态: 已开启(.poweredOn)") state = .on case .poweredOff: + print("蓝牙状态: 已关闭(.poweredOff)") state = .off + case .unauthorized: + print("蓝牙状态: 未授权(.unauthorized)") + state = .unauthorized + case .unsupported: + print("蓝牙状态: 不支持(.unsupported)") + state = .unsupported case .resetting: - state = .turningOff - case .unauthorized, .unsupported: - state = .off - default: + print("蓝牙状态: 重置中(.resetting)") + state = .resetting + case .unknown: + print("蓝牙状态: 未知(.unknown)") + state = .unknown + @unknown default: + print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))") state = .unknown } - stateCallback?(.stateChanged, ["state": state.rawValue]) + print("向Flutter端报告的蓝牙状态: \(state.rawValue)") + + // 仅发送兼容的状态(为保持向后兼容,只发送on/off/unknown) + let reportState: String + switch state { + case .on: + reportState = BluetoothState.on.rawValue + case .off, .unauthorized, .unsupported: + // 这些状态对用户来说都意味着蓝牙不可用 + reportState = BluetoothState.off.rawValue + default: + reportState = BluetoothState.unknown.rawValue + } + + stateCallback?(.stateChanged, ["state": reportState]) } } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift deleted file mode 100644 index b2cb2d30f..000000000 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Flutter -import UIKit -import ExternalAccessory - -public class ClassicBluetoothPlugin: NSObject, FlutterPlugin { - private let TAG = "ClassicBluetoothPlugin" - private var methodChannel: FlutterMethodChannel? - private var eventChannel: FlutterEventChannel? - private var eventSink: FlutterEventSink? - private let bluetoothHelper = ClassicBluetoothHelper() - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) - let instance = ClassicBluetoothPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - - // 设置事件通道(用于蓝牙状态监控) - let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) - eventChannel.setStreamHandler(instance) - - instance.methodChannel = channel - instance.eventChannel = eventChannel - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getConnectedDevices": - getConnectedDevices(result: result) - case "isBluetoothEnabled": - result(bluetoothHelper.isBluetoothEnabled()) - default: - result(FlutterMethodNotImplemented) - } - } - - private func getConnectedDevices(result: @escaping FlutterResult) { - if !bluetoothHelper.isBluetoothEnabled() { - result([]) - return - } - - bluetoothHelper.getConnectedDevices { devices, error in - if let error = error { - result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) - return - } - - result(devices) - } - } - - // 发送事件到Flutter - private func sendEvent(_ event: [String: Any]) { - if let eventSink = eventSink { - eventSink(event) - } - } -} - -// MARK: - FlutterStreamHandler -extension ClassicBluetoothPlugin: FlutterStreamHandler { - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - - // 开始监听蓝牙状态变化 - bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in - guard let self = self else { return } - - switch eventType { - case .stateChanged: - if let state = data["state"] as? String { - self.sendEvent([ - "type": "stateChanged", - "state": state, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - case .deviceConnected: - if let device = data["device"] as? [String: String] { - self.sendEvent([ - "type": "deviceConnected", - "device": device, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - case .deviceDisconnected: - if let device = data["device"] as? [String: String] { - self.sendEvent([ - "type": "deviceDisconnected", - "device": device, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - } - } - - // 报告当前蓝牙状态 - bluetoothHelper.reportCurrentBluetoothState() - - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - eventSink = nil - bluetoothHelper.stopBluetoothStateMonitoring() - return nil - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift b/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift deleted file mode 100644 index 7bbc903c9..000000000 --- a/local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import os.log - -/// iOS端的文件日志助手类,提供与Android端FileLogger类似的功能 -class FileLogger { - - private static let logQueue = DispatchQueue(label: "com.yunqiinnovation.classic_bluetooth.logger", qos: .background) - private static let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" - return formatter - }() - - /// 日志类型 - enum LogLevel: String { - case debug = "DEBUG" - case info = "INFO" - case warning = "WARNING" - case error = "ERROR" - } - - /// 输出调试日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func d(_ tag: String, _ message: String) { - log(level: .debug, tag: tag, message: message) - } - - /// 输出信息日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func i(_ tag: String, _ message: String) { - log(level: .info, tag: tag, message: message) - } - - /// 输出警告日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - static func w(_ tag: String, _ message: String) { - log(level: .warning, tag: tag, message: message) - } - - /// 输出错误日志 - /// - Parameters: - /// - tag: 标签 - /// - message: 日志消息 - /// - error: 可选的错误对象 - static func e(_ tag: String, _ message: String, _ error: Error? = nil) { - var fullMessage = message - if let error = error { - fullMessage += " - Error: \(error.localizedDescription)" - } - log(level: .error, tag: tag, message: fullMessage) - } - - /// 输出日志 - /// - Parameters: - /// - level: 日志级别 - /// - tag: 标签 - /// - message: 日志消息 - private static func log(level: LogLevel, tag: String, message: String) { - let timestamp = dateFormatter.string(from: Date()) - let logMessage = "[\(timestamp)] [\(level.rawValue)] [\(tag)]: \(message)" - - // 输出到控制台 - print(logMessage) - - // 异步写入日志文件 - logQueue.async { - writeToLogFile(logMessage) - } - - // 使用系统日志 - if #available(iOS 10.0, *) { - let osLog = OSLog(subsystem: "com.yunqiinnovation.classic_bluetooth", category: tag) - let osLogType: OSLogType - - switch level { - case .debug: - osLogType = .debug - case .info: - osLogType = .info - case .warning: - osLogType = .default - case .error: - osLogType = .error - } - - os_log("%{public}@", log: osLog, type: osLogType, message) - } - } - - /// 将日志写入文件 - /// - Parameter message: 日志消息 - private static func writeToLogFile(_ message: String) { - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - return - } - - let logDirectoryURL = documentsDirectory.appendingPathComponent("Logs") - let dateString = DateFormatter().with { $0.dateFormat = "yyyy-MM-dd" }.string(from: Date()) - let logFileURL = logDirectoryURL.appendingPathComponent("bluetooth_\(dateString).log") - - do { - // 创建日志文件夹 - try FileManager.default.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true) - - // 如果文件不存在,创建文件 - if !FileManager.default.fileExists(atPath: logFileURL.path) { - FileManager.default.createFile(atPath: logFileURL.path, contents: nil) - } - - // 追加日志内容 - if let fileHandle = FileHandle(forWritingAtPath: logFileURL.path) { - fileHandle.seekToEndOfFile() - if let data = "\(message)\n".data(using: .utf8) { - fileHandle.write(data) - } - fileHandle.closeFile() - } - } catch { - print("写入日志文件失败: \(error.localizedDescription)") - } - } -} - -// MARK: - DateFormatter 扩展 -extension DateFormatter { - func with(_ block: (DateFormatter) -> Void) -> DateFormatter { - block(self) - return self - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift index 9f70212e7..c3755d0d7 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift @@ -1,11 +1,146 @@ import Flutter import UIKit +import ExternalAccessory +import AVFoundation // 这个文件是为了满足Flutter插件的命名约定 // 实际实现在ClassicBluetoothPlugin.swift中 public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用真正的实现 - ClassicBluetoothPlugin.register(with: registrar) - } + private let TAG = "ClassicBluetoothPlugin" + private var methodChannel: FlutterMethodChannel? + private var eventChannel: FlutterEventChannel? + private var eventSink: FlutterEventSink? + private let bluetoothHelper = ClassicBluetoothHelper() + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) + let instance = SwiftClassicBluetoothPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + // 设置事件通道(用于蓝牙状态监控) + let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) + eventChannel.setStreamHandler(instance) + + instance.methodChannel = channel + instance.eventChannel = eventChannel + + // 记录插件初始化 + print("经典蓝牙插件初始化成功") + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getConnectedDevices": + getConnectedDevices(result: result) + case "isBluetoothEnabled": + result(bluetoothHelper.isBluetoothEnabled()) + default: + result(FlutterMethodNotImplemented) + } + } + + private func getConnectedDevices(result: @escaping FlutterResult) { + // 先检查蓝牙状态 + if !bluetoothHelper.isBluetoothEnabled() { + print("蓝牙未启用,无法获取设备") + result([]) + return + } + + bluetoothHelper.getConnectedDevices { devices, error in + if let error = error { + print("获取设备失败: \(error.localizedDescription)") + result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) + return + } + + print("获取到 \(devices.count) 个蓝牙设备") + for device in devices { + if let name = device["name"], let address = device["address"], let type = device["type"] { + print("设备: \(name), 地址: \(address), 类型: \(type)") + } + } + + // 转换为Flutter可用的格式 + let resultList = devices.map { device -> [String: String] in + // 仅保留Flutter端需要的字段 + return [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] + } + + result(resultList) + } + } + + // 发送事件到Flutter + private func sendEvent(_ event: [String: Any]) { + if let eventSink = eventSink { + eventSink(event) + } + } +} + +// MARK: - FlutterStreamHandler +extension SwiftClassicBluetoothPlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + + // 开始监听蓝牙状态变化 + bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in + guard let self = self else { return } + + switch eventType { + case .stateChanged: + if let state = data["state"] as? String { + print("蓝牙状态变化: \(state)") + self.sendEvent([ + "type": "stateChanged", + "state": state, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + case .deviceConnected: + if let device = data["device"] as? [String: String] { + print("设备连接: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]") + // 只保留Flutter端需要的字段 + let deviceData: [String: String] = [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] + self.sendEvent([ + "type": "deviceConnected", + "device": deviceData, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + case .deviceDisconnected: + if let device = data["device"] as? [String: String] { + print("设备断开: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]") + // 只保留Flutter端需要的字段 + let deviceData: [String: String] = [ + "name": device["name"] ?? "未知设备", + "address": device["address"] ?? "未知地址" + ] + self.sendEvent([ + "type": "deviceDisconnected", + "device": deviceData, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + } + } + + // 报告当前蓝牙状态 + bluetoothHelper.reportCurrentBluetoothState() + + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + bluetoothHelper.stopBluetoothStateMonitoring() + return nil + } } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec index 8dbc9c504..0ed858671 100644 --- a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec +++ b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec @@ -14,7 +14,8 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '13.0' + s.dependency 'Flutter' s.swift_version = '5.0' # 添加CoreBluetooth和ExternalAccessory框架依赖 diff --git a/local_plugins/classic_bluetooth/pubspec.yaml b/local_plugins/classic_bluetooth/pubspec.yaml index c618200f1..6e4012e2f 100644 --- a/local_plugins/classic_bluetooth/pubspec.yaml +++ b/local_plugins/classic_bluetooth/pubspec.yaml @@ -9,8 +9,6 @@ environment: dependencies: flutter: sdk: flutter - deepvoice_utils: - path: ../deepvoice_utils flutter: plugin: diff --git a/local_plugins/deepvoice_utils/android/build.gradle.kts b/local_plugins/deepvoice_utils/android/build.gradle.kts deleted file mode 100644 index 61848a582..000000000 --- a/local_plugins/deepvoice_utils/android/build.gradle.kts +++ /dev/null @@ -1,46 +0,0 @@ -plugins { - // Android Library 插件 - id("com.android.library") - // Kotlin Android 插件 - id("org.jetbrains.kotlin.android") - id("com.github.johnrengelman.shadow") version "8.1.1" -} - -android { - // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.deepvoice_utils" - - // 目标 SDK 版本 - compileSdk = 35 - - defaultConfig { - // 最低 SDK 版本 - minSdk = 21 - targetSdk = 34 - } - - // Java 语言级别兼容配置 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - // Kotlin 语言级别 - kotlinOptions { - jvmTarget = "11" - } - - // 添加packaging配置,排除冲突文件 - packaging { - resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/LICENSE") - excludes.add("META-INF/LICENSE.txt") - excludes.add("META-INF/license.txt") - excludes.add("META-INF/NOTICE") - excludes.add("META-INF/NOTICE.txt") - excludes.add("META-INF/notice.txt") - excludes.add("META-INF/*.kotlin_module") - } - } -} diff --git a/local_plugins/deepvoice_utils/android/settings.gradle.kts b/local_plugins/deepvoice_utils/android/settings.gradle.kts deleted file mode 100644 index f1b0c086e..000000000 --- a/local_plugins/deepvoice_utils/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "deepvoice_utils" \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml b/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml deleted file mode 100644 index c043918b0..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt b/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt deleted file mode 100644 index a330e85b8..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.yunqiinnovation.deepvoice_utils - -import androidx.annotation.NonNull -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result - -/** DeepvoiceUtilsPlugin */ -class DeepvoiceUtilsPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "deepvoice_utils") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "getPlatformVersion" -> { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt b/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt deleted file mode 100644 index b31331689..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.yunqiinnovation.deepvoice_utils - -import android.content.Context -import android.util.Log -import java.io.File -import java.io.FileWriter -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.* -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -/** - * 文件日志工具 - * 将日志同时输出到Logcat和文件系统 - */ -class FileLogger private constructor(context: Context) { - private val logDir: File - private var currentLogFile: File? = null - private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) - private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor() - - init { - // 初始化日志目录 - logDir = File(context.getExternalFilesDir(null), "logs") - if (!logDir.exists()) { - logDir.mkdirs() - } - - // 创建当天的日志文件 - updateLogFile() - } - - /** - * 更新日志文件为当天日期 - */ - private fun updateLogFile() { - val today = dateFormat.format(Date()) - val logFile = File(logDir, "log_$today.txt") - - if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) { - currentLogFile = logFile - - // 如果是新文件,添加文件头 - if (!logFile.exists()) { - try { - logFile.createNewFile() - FileWriter(logFile, true).use { writer -> - writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n") - } - } catch (e: IOException) { - Log.e(TAG, "创建日志文件失败: ${e.message}") - } - } - } - } - - /** - * 调试级别日志 - */ - fun d(tag: String, message: String) { - // 输出到Logcat - Log.d(tag, message) - - // 异步写入文件 - logToFile("D", tag, message) - } - - /** - * 信息级别日志 - */ - fun i(tag: String, message: String) { - // 输出到Logcat - Log.i(tag, message) - - // 异步写入文件 - logToFile("I", tag, message) - } - - /** - * 警告级别日志 - */ - fun w(tag: String, message: String) { - // 输出到Logcat - Log.w(tag, message) - - // 异步写入文件 - logToFile("W", tag, message) - } - - /** - * 错误级别日志 - */ - fun e(tag: String, message: String, throwable: Throwable? = null) { - // 输出到Logcat - if (throwable != null) { - Log.e(tag, message, throwable) - } else { - Log.e(tag, message) - } - - // 异步写入文件 - val errorMsg = if (throwable != null) { - "$message\n${throwable.message}\n${throwable.stackTraceToString()}" - } else { - message - } - - logToFile("E", tag, errorMsg) - } - - /** - * 将日志写入文件 - */ - private fun logToFile(level: String, tag: String, message: String) { - - logExecutor.execute { - try { - // 检查是否需要更新日志文件 - updateLogFile() - - // 格式化日志内容 - val timestamp = timeFormat.format(Date()) - val logLine = "$timestamp $level/$tag: $message\n" - - // 写入文件 - if (currentLogFile != null && currentLogFile!!.exists()) { - FileWriter(currentLogFile!!, true).use { writer -> - writer.append(logLine) - } - } - } catch (e: IOException) { - Log.e(TAG, "写入日志文件失败: ${e.message}") - } - } - } - - /** - * 清理旧日志文件 - * @param daysToKeep 保留的天数 - */ - fun cleanOldLogs(daysToKeep: Int = 7) { - logExecutor.execute { - try { - val currentTime = System.currentTimeMillis() - val maxAge = daysToKeep * 24 * 60 * 60 * 1000L - - logDir.listFiles()?.forEach { file -> - if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) { - val fileAge = currentTime - file.lastModified() - if (fileAge > maxAge) { - val deleted = file.delete() - if (deleted) { - Log.d(TAG, "已删除旧日志文件: ${file.name}") - } - } - } - } - } catch (e: Exception) { - Log.e(TAG, "清理旧日志失败: ${e.message}") - } - } - } - - /** - * 关闭日志系统 - */ - fun shutdown() { - logExecutor.shutdown() - } - - companion object { - private const val TAG = "FileLogger" - private var INSTANCE: FileLogger? = null - - /** - * 初始化日志系统 - */ - @Synchronized - fun init(context: Context) { - if (INSTANCE == null) { - INSTANCE = FileLogger(context.applicationContext) - d(TAG, "文件日志系统已初始化") - } - } - - /** - * 调试日志 - */ - fun d(tag: String, message: String) { - INSTANCE?.d(tag, message) ?: Log.d(tag, message) - } - - /** - * 信息日志 - */ - fun i(tag: String, message: String) { - INSTANCE?.i(tag, message) ?: Log.i(tag, message) - } - - /** - * 警告日志 - */ - fun w(tag: String, message: String) { - INSTANCE?.w(tag, message) ?: Log.w(tag, message) - } - - /** - * 错误日志 - */ - fun e(tag: String, message: String, throwable: Throwable? = null) { - INSTANCE?.e(tag, message, throwable) ?: run { - if (throwable != null) { - Log.e(tag, message, throwable) - } else { - Log.e(tag, message) - } - } - } - - /** - * 清理旧日志 - */ - fun cleanOldLogs(daysToKeep: Int = 7) { - INSTANCE?.cleanOldLogs(daysToKeep) - } - - /** - * 关闭日志系统 - */ - fun shutdown() { - INSTANCE?.shutdown() - INSTANCE = null - } - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h deleted file mode 100644 index f64c3569a..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface DeepvoiceUtilsPlugin : NSObject -@end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m deleted file mode 100644 index af5d121b1..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "DeepvoiceUtilsPlugin.h" -#if __has_include() -#import -#else -#import "deepvoice_utils-Swift.h" -#endif - -@implementation DeepvoiceUtilsPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar]; -} -@end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift deleted file mode 100644 index 922bb3c4b..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Flutter -import UIKit - -public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin { - private static let TAG = "DeepvoiceUtilsPlugin" - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger()) - let instance = DeepvoiceUtilsPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - - // 初始化文件日志 - FileLogger.initialize() - FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化") - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("iOS \(UIDevice.current.systemVersion)") - default: - result(FlutterMethodNotImplemented) - } - } - - public func detachFromEngine(for registrar: FlutterPluginRegistrar) { - // 关闭日志系统 - FileLogger.shutdown() - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift b/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift deleted file mode 100644 index f54e040f8..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift +++ /dev/null @@ -1,286 +0,0 @@ -import Foundation -import os.log - -/** - * 文件日志工具 - * 将日志同时输出到控制台和文件系统 - */ -@objc public class FileLogger: NSObject { - - private static let TAG = "FileLogger" - private static var instance: FileLogger? - - private let logDir: URL - private var currentLogFile: URL? - private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility) - - private let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - return formatter - }() - - private let timeFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss.SSS" - return formatter - }() - - /** - * 初始化日志系统 - */ - private init(directory: URL) { - self.logDir = directory.appendingPathComponent("logs", isDirectory: true) - super.init() - - // 创建日志目录 - try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) - - // 创建当天日志文件 - updateLogFile() - - // 记录初始化完成 - self.d(FileLogger.TAG, "文件日志系统已初始化") - } - - /** - * 更新日志文件为当天日期 - */ - private func updateLogFile() { - let today = dateFormatter.string(from: Date()) - let logFile = logDir.appendingPathComponent("log_\(today).txt") - - if currentLogFile == nil || currentLogFile?.path != logFile.path { - currentLogFile = logFile - - // 如果是新文件,添加文件头 - if !FileManager.default.fileExists(atPath: logFile.path) { - let headerDate = DateFormatter() - headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss" - let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n" - - try? headerLine.write(to: logFile, atomically: true, encoding: .utf8) - } - } - } - - /** - * 将日志写入文件 - */ - private func logToFile(level: String, tag: String, message: String) { - logQueue.async { [weak self] in - guard let self = self else { return } - - do { - // 检查是否需要更新日志文件 - self.updateLogFile() - - guard let logFile = self.currentLogFile, - FileManager.default.fileExists(atPath: logFile.path) else { - return - } - - // 格式化日志内容 - let timestamp = self.timeFormatter.string(from: Date()) - let logLine = "\(timestamp) \(level)/\(tag): \(message)\n" - - // 获取文件句柄并追加内容 - if let fileHandle = try? FileHandle(forWritingTo: logFile) { - fileHandle.seekToEndOfFile() - if let data = logLine.data(using: .utf8) { - fileHandle.write(data) - } - fileHandle.closeFile() - } else { - // 如果无法获取文件句柄,尝试直接写入 - let existingContent = try? String(contentsOf: logFile, encoding: .utf8) - let newContent = (existingContent ?? "") + logLine - try newContent.write(to: logFile, atomically: true, encoding: .utf8) - } - } catch { - print("写入日志文件失败: \(error.localizedDescription)") - } - } - } - - /** - * 调试级别日志 - */ - private func d(_ tag: String, _ message: String) { - // 输出到控制台 - print("D/\(tag): \(message)") - - // 写入文件 - logToFile(level: "D", tag: tag, message: message) - } - - /** - * 信息级别日志 - */ - private func i(_ tag: String, _ message: String) { - // 输出到控制台 - print("I/\(tag): \(message)") - - // 写入文件 - logToFile(level: "I", tag: tag, message: message) - } - - /** - * 警告级别日志 - */ - private func w(_ tag: String, _ message: String) { - // 输出到控制台 - print("W/\(tag): \(message)") - - // 写入文件 - logToFile(level: "W", tag: tag, message: message) - } - - /** - * 错误级别日志 - */ - private func e(_ tag: String, _ message: String, _ error: Error? = nil) { - var fullMessage = message - - // 如果有错误,添加错误信息 - if let error = error { - fullMessage += "\n\(error.localizedDescription)" - } - - // 输出到控制台 - print("E/\(tag): \(fullMessage)") - - // 写入文件 - logToFile(level: "E", tag: tag, message: fullMessage) - } - - /** - * 清理旧日志文件 - */ - private func cleanOldLogs(daysToKeep: Int = 7) { - logQueue.async { - do { - let currentDate = Date() - let fileManager = FileManager.default - - let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: []) - - for fileURL in logFiles where fileURL.pathExtension == "txt" { - if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), - let creationDate = fileAttributes[.creationDate] as? Date { - - let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate) - - if let days = age.day, days > daysToKeep { - try? fileManager.removeItem(at: fileURL) - print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)") - } - } - } - } catch { - print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)") - } - } - } - - // MARK: - 公共静态方法 - - /** - * 初始化日志系统 - */ - @objc public static func initialize() { - if instance == nil { - let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - instance = FileLogger(directory: documentsDirectory) - } - } - - /** - * 初始化日志系统(使用自定义目录) - */ - @objc public static func initialize(withDirectory directory: URL) { - if instance == nil { - instance = FileLogger(directory: directory) - } - } - - /** - * 调试日志 - */ - @objc public static func d(_ tag: String, _ message: String) { - // 如果实例不存在,先使用默认目录初始化 - if instance == nil { - initialize() - } - - instance?.d(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message) - } - } - - /** - * 信息日志 - */ - @objc public static func i(_ tag: String, _ message: String) { - if instance == nil { - initialize() - } - - instance?.i(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message) - } - } - - /** - * 警告日志 - */ - @objc public static func w(_ tag: String, _ message: String) { - if instance == nil { - initialize() - } - - instance?.w(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message) - } - } - - /** - * 错误日志 - */ - @objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) { - if instance == nil { - initialize() - } - - instance?.e(tag, message, error) - - // 使用系统日志 - if #available(iOS 10.0, *) { - let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage) - } - } - - /** - * 清理旧日志 - */ - @objc public static func cleanOldLogs(daysToKeep: Int = 7) { - instance?.cleanOldLogs(daysToKeep: daysToKeep) - } - - /** - * 关闭日志系统 - */ - @objc public static func shutdown() { - instance = nil - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift b/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift deleted file mode 100644 index 408f07402..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Flutter -import UIKit - -// 这个类是为了满足Flutter插件的命名约定 -// 实际实现在DeepvoiceUtilsPlugin.swift中 -public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用真正的实现 - DeepvoiceUtilsPlugin.register(with: registrar) - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/README.md b/local_plugins/deepvoice_utils/ios/README.md deleted file mode 100644 index 7f12f83cb..000000000 --- a/local_plugins/deepvoice_utils/ios/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# DeepVoice Utils iOS 实现 - -## 简介 - -本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能: - -- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查 - -## 使用说明 - -### 日志系统 - -该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。 - -## 配置要求 - -- iOS 11.0 或更高版本 -- Swift 5.0 -- 确保应用拥有文件写入权限 - -## 开发说明 - -### 文件结构 - -- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互 -- `FileLogger.swift`:日志系统实现,提供日志记录功能 -- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类 -- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件 - -### 自定义日志目录 - -如需使用自定义日志目录,可以在应用启动时手动调用初始化方法: - -```swift -import deepvoice_utils - -// 在 AppDelegate 中 -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // 自定义日志目录 - let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs") - FileLogger.initialize(withDirectory: customDirectory) - - return true -} -``` - -### 清理旧日志 - -默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用: - -```swift -// 清理7天前的日志 -FileLogger.cleanOldLogs(daysToKeep: 7) -``` \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec b/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec deleted file mode 100644 index 758fe120c..000000000 --- a/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec +++ /dev/null @@ -1,19 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# -Pod::Spec.new do |s| - s.name = 'deepvoice_utils' - s.version = '0.0.1' - s.summary = '深语工具库' - s.description = <<-DESC -提供文件日志记录等通用功能的Flutter插件。 - DESC - s.homepage = 'http://yunqiinnovation.com' - s.license = { :file => '../LICENSE' } - s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - - s.ios.deployment_target = '11.0' - s.swift_version = '5.0' -end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart b/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart deleted file mode 100644 index e3387f790..000000000 --- a/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart +++ /dev/null @@ -1,3 +0,0 @@ -library deepvoice_utils; - -export 'src/file_logger.dart'; \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/lib/src/file_logger.dart b/local_plugins/deepvoice_utils/lib/src/file_logger.dart deleted file mode 100644 index eae665185..000000000 --- a/local_plugins/deepvoice_utils/lib/src/file_logger.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; - -/// 文件日志记录器 -/// -/// 提供文件日志记录功能,支持简单的日志记录 -class FileLogger { - /// 记录调试日志 - static void d(String tag, String message) { - debugPrint('[$tag] DEBUG: $message'); - } - - /// 记录信息日志 - static void i(String tag, String message) { - debugPrint('[$tag] INFO: $message'); - } - - /// 记录警告日志 - static void w(String tag, String message) { - debugPrint('[$tag] WARNING: $message'); - } - - /// 记录错误日志 - static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) { - final errorMsg = error != null ? '$message: $error' : message; - debugPrint('[$tag] ERROR: $errorMsg'); - if (stackTrace != null) { - debugPrint(stackTrace.toString()); - } - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/pubspec.yaml b/local_plugins/deepvoice_utils/pubspec.yaml deleted file mode 100644 index 24796b435..000000000 --- a/local_plugins/deepvoice_utils/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: deepvoice_utils -description: 通用工具库,包含日志、文件处理等功能 -version: 0.0.1 - -environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - logger: ^2.0.0 - path_provider: ^2.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.deepvoice_utils - pluginClass: DeepvoiceUtilsPlugin - ios: - pluginClass: DeepvoiceUtilsPlugin \ No newline at end of file diff --git a/local_plugins/jieli/README.md b/local_plugins/jieli/README.md deleted file mode 100644 index d926109c8..000000000 --- a/local_plugins/jieli/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Jieli 插件 - -一个用于连接杰理蓝牙耳机的Flutter插件,支持Android 12+的BLE通信。 - -## 功能 - -- 扫描支持特定BLE服务的耳机设备 -- 连接设备并发现服务 -- 接收耳机的唤醒通知 -- 向耳机发送控制命令启动录音 -- 接收音频数据流 - -## 技术规范 - -此插件实现了以下BLE特性: - -- **服务UUID**: 0000A100-0000-1000-8000-00805F9B34FB -- **音频数据特征UUID**: 0000A101-0000-1000-8000-00805F9B34FB -- **控制特征UUID**: 0000A102-0000-1000-8000-00805F9B34FB - -## 使用方法 - -### 初始化插件 - -```dart -final jieli = Jieli(); -``` - -### 检查蓝牙状态 - -```dart -bool isEnabled = await jieli.isBluetoothEnabled(); -if (!isEnabled) { - await jieli.requestBluetoothEnable(); -} -``` - -### 扫描设备 - -```dart -// 监听扫描结果 -jieli.scanResults.listen((devices) { - // 处理扫描到的设备列表 - for (var device in devices) { - print('发现设备: ${device.name} (${device.id}), RSSI: ${device.rssi}'); - } -}); - -// 开始扫描,10秒后自动停止 -await jieli.startScan(scanTimeout: 10000); - -// 手动停止扫描 -await jieli.stopScan(); -``` - -### 连接设备 - -```dart -// 监听连接状态 -jieli.connectionState.listen((state) { - switch (state) { - case ConnectionState.connected: - print('设备已连接'); - break; - case ConnectionState.disconnected: - print('设备已断开'); - break; - case ConnectionState.connecting: - print('正在连接...'); - break; - case ConnectionState.disconnecting: - print('正在断开...'); - break; - case ConnectionState.error: - print('连接错误'); - break; - } -}); - -// 连接到设备 -await jieli.connect(deviceId); - -// 断开连接 -await jieli.disconnect(); -``` - -### 接收唤醒信号 - -```dart -// 监听唤醒信号 -jieli.wakeupStream.listen((wakeup) { - print('收到唤醒信号'); - // 通常在收到唤醒后自动发送开始录音命令 -}); -``` - -### 发送录音命令 - -```dart -// 发送开始录音命令 -await jieli.sendStartRecordingCommand(); - -// 发送停止录音命令 -await jieli.sendStopRecordingCommand(); -``` - -### 接收音频数据 - -```dart -// 监听音频数据流 -jieli.audioDataStream.listen((data) { - print('收到音频数据: ${data.length} 字节'); - // 处理音频数据 -}); -``` - -### 资源清理 - -```dart -// 在不再需要插件时释放资源 -await jieli.dispose(); -``` - -## 权限 - -使用此插件需要以下Android权限: - -- Android 12+ (API 31+): `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` -- Android 11及以下: `BLUETOOTH`, `BLUETOOTH_ADMIN` - -插件会在需要时自动请求权限,但建议在应用清单文件中声明这些权限: - -```xml - - - - - - - -``` \ No newline at end of file diff --git a/local_plugins/jieli/android/settings.gradle.kts b/local_plugins/jieli/android/settings.gradle.kts deleted file mode 100644 index c0323d3bc..000000000 --- a/local_plugins/jieli/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "jieli" \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/AndroidManifest.xml b/local_plugins/jieli/android/src/main/AndroidManifest.xml deleted file mode 100644 index 3cf7a945e..000000000 --- a/local_plugins/jieli/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt deleted file mode 100644 index 5921ba603..000000000 --- a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt +++ /dev/null @@ -1,665 +0,0 @@ -package com.yunqiinnovation.jieli - -import android.bluetooth.* -import android.bluetooth.le.* -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.os.ParcelUuid -import android.util.Log -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.* -import java.io.File -import java.io.FileOutputStream -import java.text.SimpleDateFormat -import java.util.* -import com.yunqiinnovation.azure_speech.AzureAsrHelper -import com.yunqiinnovation.azure_speech.AzureTtsHelper -import com.yunqiinnovation.jl_opus.JlOpusPlugin - -/** - * BLE服务类,提供核心蓝牙功能 - */ -class BleService(private val context: Context) { - companion object { - private const val TAG = "JieliBleService" - - // 服务和特征UUID - 根据协议规范重新定义 - // private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID - private const val AUDIO_SERVICE_UUID = "0000abc0-0000-1111-2222-123456789abc" // 主服务UUID - private const val NOTIFY_CHAR_UUID = - "0000abc2-0000-1111-2222-123456789abc" // 用于接收数据的特征(notify) - private const val WRITE_CHAR_UUID = - "0000abc1-0000-1111-2222-123456789abc" // 用于发送命令的特征(write without response) - private const val CLIENT_CHAR_CONFIG_UUID = - "00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符 - - // 连接状态常量 - const val STATE_DISCONNECTED = 0 - const val STATE_CONNECTING = 1 - const val STATE_CONNECTED = 2 - const val STATE_DISCONNECTING = 3 - const val STATE_ERROR = 4 - - // 扫描结果更新间隔(毫秒) - private const val SCAN_RESULT_UPDATE_INTERVAL = 500L - - // 命令类型常量 - const val COMMAND_VOLUME = 1 - } - - /** - * 回调接口 - */ - interface Callback { - fun onScanResult(devices: List>) - fun onConnectionStateChanged(state: Int) - fun onAudioDataReceived(data: ByteArray) - fun onWakeupSignalReceived() - fun onCommandResult(commandType: Int, success: Boolean) - } - - // region 文件相关 - private var outputFile: File? = null - private var fos: FileOutputStream? = null - - // Azure服务 - private var azureAsrHelper: AzureAsrHelper? = null - private var azureTtsHelper: AzureTtsHelper? = null - - // 获取插件实例 - private val opusPlugin = JlOpusPlugin() - - // 蓝牙管理器和适配器 - private val bluetoothManager = - context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter - private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner - - // GATT连接和特征 - private var bluetoothGatt: BluetoothGatt? = null - private var notifyChar: BluetoothGattCharacteristic? = null - private var writeChar: BluetoothGattCharacteristic? = null - - // 扫描相关 - private val scanHandler = Handler(Looper.getMainLooper()) - private val scanResults = ArrayList() - private var isScanning = false - private var isPendingScanResultUpdate = false - private val scanResultUpdateRunnable = Runnable { - notifyScanResults() - isPendingScanResultUpdate = false - } - - // 连接状态LiveData - val connectionState = MutableLiveData(STATE_DISCONNECTED) - - // 统一回调接口 - var callback: Callback? = null - - /** - * 开始扫描BLE设备 - */ - fun startScan(scanTimeout: Long = 5000): Boolean { - if (bluetoothLeScanner == null || isScanning) { - return false - } - - Log.i(TAG, "开始扫描BLE设备...") - scanResults.clear() - - val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID) - val scanFilter = ScanFilter.Builder() - // .setServiceUuid(ParcelUuid(serviceUuid)) - .build() - val scanFilters = listOf(scanFilter) - - val scanSettings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build() - - bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback) - isScanning = true - - // 设置超时停止扫描 - scanHandler.postDelayed({ - Log.d( - TAG, - "定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]" - ) - stopScan() - }, scanTimeout) - - return true - } - - /** - * 停止扫描 - */ - fun stopScan(): Boolean { - Log.i(TAG, "停止扫描BLE设备") - - if (bluetoothLeScanner == null || !isScanning) { - return false - } - - bluetoothLeScanner.stopScan(scanCallback) - scanHandler.removeCallbacksAndMessages(null) - isScanning = false - - // 最后一次发送扫描结果 - notifyScanResults() - - return true - } - - /** - * 扫描回调 - */ - private val scanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult) { - val device = result.device - val deviceName = device.name ?: "未知设备" - val deviceAddress = device.address - val rssi = result.rssi - val scanRecord = result.scanRecord - if (!deviceName.contains("zxd")) { - return - } - Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi") - - // 打印所有发现的UUID以便调试 - val serviceUuids = scanRecord?.serviceUuids - if (serviceUuids != null) { - for (uuid in serviceUuids) { - Log.d(TAG, "设备UUID: ${uuid.uuid}") - } - } - - // 检查制造商数据 - for (i in 0 until 65535) { - val manufacturerData = scanRecord?.getManufacturerSpecificData(i) - if (manufacturerData != null) { - // Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}") - // 这里可以根据已知的厂商ID和格式解析数据 - } - } - - // 避免重复添加 - - val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress } - - if (existingIndex >= 0) { - - scanResults[existingIndex] = result - } else { - Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD") - scanResults.add(result) - } - - // 如果没有待处理的更新,安排一个 - if (!isPendingScanResultUpdate) { - isPendingScanResultUpdate = true - scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL) - } - } - - override fun onScanFailed(errorCode: Int) { - Log.e(TAG, "扫描失败,错误码: $errorCode") - isScanning = false - } - } - - /** - * 将扫描结果通知回调 - */ - private fun notifyScanResults() { - val devicesJson = scanResults.map { result -> - mapOf( - "id" to result.device.address, - "name" to (result.device.name ?: "未知设备"), - "rssi" to result.rssi - ) - } - - callback?.onScanResult(devicesJson) - } - - /** - * 连接到指定设备 - */ - fun connect(deviceAddress: String) { - if (bluetoothAdapter == null) { - Log.e(TAG, "蓝牙适配器不可用") - updateConnectionState(STATE_ERROR) - return - } - - // 先断开现有连接 - bluetoothGatt?.close() - bluetoothGatt = null - - try { - val device = bluetoothAdapter.getRemoteDevice(deviceAddress) - updateConnectionState(STATE_CONNECTING) - - // 连接GATT服务器 - Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]") - bluetoothGatt = - device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) - } catch (e: Exception) { - Log.e(TAG, "连接设备时发生错误: ${e.message}") - updateConnectionState(STATE_ERROR) - } - } - - /** - * 断开连接 - */ - fun disconnect() { - bluetoothGatt?.disconnect() - updateConnectionState(STATE_DISCONNECTING) - } - - /** - * 更新连接状态并通知回调 - */ - private fun updateConnectionState(state: Int) { - connectionState.postValue(state) - callback?.onConnectionStateChanged(state) - } - - /** - * GATT回调 - */ - private val gattCallback = object : BluetoothGattCallback() { - - - override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "设备已连接,开始发现服务...") - updateConnectionState(STATE_CONNECTED) - - // 开始搜索服务 - gatt.discoverServices() - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - Log.i(TAG, "设备已断开连接") - updateConnectionState(STATE_DISCONNECTED) - - // 清理资源 - gatt.close() - bluetoothGatt = null - notifyChar = null - writeChar = null - } else if (status != BluetoothGatt.GATT_SUCCESS) { - Log.e(TAG, "连接失败,status=$status") - updateConnectionState(STATE_ERROR) - - gatt.close() - bluetoothGatt = null - } - } - - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "服务发现成功") - - // 查找目标服务 - val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID)) - if (audioService != null) { - Log.i(TAG, "找到音频服务,开始遍历特征...") - audioService.characteristics.forEach { char -> - Log.i(TAG, "特征UUID: ${char.uuid}") - } - Log.i(TAG, "找到音频服务$") - - // 获取特征 - notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID)) - writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID)) - if (notifyChar != null && writeChar != null) { - Log.i(TAG, "找到所需特征") - - // 设置特征写入类型为无响应模式 - writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - - // 注册特征通知 - setupNotifications(gatt) - - opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object : - JlOpusPlugin.CallbackOpus { - override fun onAudioDataReceivedOpus(pcmData: ByteArray) { - // Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}") - callback?.onAudioDataReceived(pcmData) - // 处理解码后的 PCM 数据(如播放或写入文件) - } - }) - } else { - Log.e(TAG, "未找到所需特征") - updateConnectionState(STATE_ERROR) - } - } else { - Log.e(TAG, "未找到音频服务") - updateConnectionState(STATE_ERROR) - } - } else { - Log.e(TAG, "服务发现失败,status=$status") - updateConnectionState(STATE_ERROR) - } - } - - // 添加扩展函数(在文件顶部声明) - fun List.toByteArray(): ByteArray { - return ByteArray(this.size) { this[it] } - } - - override fun onCharacteristicChanged( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic - ) { - val characteristicUuid = characteristic.uuid.toString() - // Log.d(TAG, "收到特征变化通知: $characteristicUuid") - - when (characteristicUuid) { - // NOTIFY_CHAR_UUID -> { - // // 收到音频数据 - // val audioData = characteristic.value - // Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节") - - // callback?.onAudioDataReceived(audioData) - // } - NOTIFY_CHAR_UUID -> { - // 处理控制消息 - val value = characteristic.value - // Log.i(TAG, "收到控制通知: ${value.size}") - - // 判断是否收到音频数据 - if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) { - // 处理设备返回的协议数据 - fos?.write(value) - - opusPlugin?.handleDecodeRequest(value) - - - } - // 判断是否是协议格式响应 - if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) { - // 处理设备返回的协议数据 - processResponseData(value) - } - // 假设0x01表示唤醒信号 - else if (value.isNotEmpty() && value[0].toInt() == 0x01) { - Log.i(TAG, "收到唤醒信号") - - callback?.onWakeupSignalReceived() - - // 收到唤醒后自动发送开始录音命令 - sendStartRecordingCommand() - - - } - } - } - } - - override fun onCharacteristicWrite( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - status: Int - ) { - if (characteristic.uuid.toString() == WRITE_CHAR_UUID) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}") - } else { - Log.e(TAG, "命令发送失败,status=$status") - } - } - } - - override fun onDescriptorWrite( - gatt: BluetoothGatt, - descriptor: BluetoothGattDescriptor, - status: Int - ) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "描述符写入成功: ${descriptor.uuid}") - Log.i(TAG, "特征通知设置成功,设备就绪") - } else { - Log.e(TAG, "描述符写入失败,status=$status") - updateConnectionState(STATE_ERROR) - } - } - } - - /** - * 设置特征通知 - */ - private fun setupNotifications(gatt: BluetoothGatt) { - // 启用 notify 特征通知 - if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { - gatt.setCharacteristicNotification(notifyChar, true) - - val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID)) - descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - - if (descriptor != null) { - gatt.writeDescriptor(descriptor) - Log.i(TAG, "设置数据通知特征通知") - } - } else { - Log.e(TAG, "数据通知特征不支持通知") - } - } - - - /** - * 处理设备返回的数据 - * 协议格式: - * 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte) - */ - private fun processResponseData(data: ByteArray) { - if (data.size < 4) { - Log.e(TAG, "返回数据格式错误,长度不足") - return - } - - // 判断协议头 - if (data[0].toInt() and 0xFF != 0xA5) { - Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}") - return - } - - val fieldType = data[1].toInt() and 0xFF - val dataLen = data[2].toInt() and 0xFF - - // 验证长度 - if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和 - Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}") - return - } - - // 计算校验和 - var checksum = 0 - for (i in 0 until data.size - 1) { - checksum += data[i].toInt() and 0xFF - } - checksum = checksum and 0xFF - - val receivedChecksum = data[data.size - 1].toInt() and 0xFF - - if (checksum != receivedChecksum) { - Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum") - return - } - - // 解析字段类型 - when (fieldType) { - 0x01 -> { - // 音量调整命令的响应 - val result = data[3].toInt() and 0xFF - val success = result == 0 - Log.i(TAG, "音量调整命令响应: ${if (success) "成功" else "失败"}") - callback?.onCommandResult(COMMAND_VOLUME, success) - } - // 其他命令类型可在此处添加 - else -> { - Log.d(TAG, "未知命令类型: ${String.format("0x%02X", fieldType)}") - } - } - } - - /** - * 发送开始录音命令 - */ - fun sendStartRecordingCommand(): Boolean { - if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { - Log.e(TAG, "无法发送开始录音命令,设备未连接") - return false - } - - try { - val commandData = byteArrayOf(0x01) - writeChar?.value = commandData - // 获取外部存储目录路径 - // 生成带时间戳的文件名(格式:yyyyMMdd_HHmmss_audio.opus) - val timeStamp = - SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val fileName = "${timeStamp}_audio.opus" - val externalDir = context.getExternalFilesDir(null) - outputFile = File(externalDir, fileName) - fos = FileOutputStream(outputFile).also { - Log.i(TAG, "音频文件创建成功:${outputFile?.absolutePath}") - } - - // val subscriptionKey = call.argument("subscriptionKey") ?: "" - // val region = call.argument("region") ?: "" - // val supportedLanguages = call.argument>("supportedLanguages") ?: listOf("zh-CN") - - // val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages.toTypedArray()) - // result.success(success) - - - val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false - - if (success) { - Log.i(TAG, "开始录音命令已发送") - } else { - Log.e(TAG, "开始录音命令发送失败") - } - - return success - } catch (e: Exception) { - Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}") - return false - } - } - - /** - * 发送停止录音命令 - */ - fun sendStopRecordingCommand(): Boolean { - if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { - Log.e(TAG, "无法发送停止录音命令,设备未连接") - return false - } - - try { - val commandData = byteArrayOf(0x02) - writeChar?.value = commandData - - val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false - fos?.close() - if (success) { - Log.i(TAG, "停止录音命令已发送") - } else { - Log.e(TAG, "停止录音命令发送失败") - } - - return success - } catch (e: Exception) { - Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}") - return false - } - } - - /** - * 发送音量调整命令 - * 协议格式: - * 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和 - */ - fun adjustVolume(volume: Int): Boolean { - Log.e( - TAG, - "adjustVolume,bluetoothGatt=${bluetoothGatt},writeChar=${writeChar},connectionState=${connectionState.value}" - ) - if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { - Log.e(TAG, "无法发送音量调整命令,设备未连接") - return false - } - - // 限制音量范围为0-16 - val safeVolume = volume.coerceIn(0, 16) - - try { - // 构建命令数据 - val header: Byte = 0xAA.toByte() - val fieldType: Byte = 0x01 - val dataLen: Byte = 0x01 - val volumeData: Byte = safeVolume.toByte() - - // 计算校验和 - val checksum = ((header.toInt() and 0xFF) + - (fieldType.toInt() and 0xFF) + - (dataLen.toInt() and 0xFF) + - (volumeData.toInt() and 0xFF)) and 0xFF - - // 创建完整命令 - val commandData = - byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte()) - - // 写入特征 - writeChar?.value = commandData - - val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false - - if (success) { - Log.i(TAG, "音量调整命令已发送: $safeVolume") - } else { - Log.e(TAG, "音量调整命令发送失败") - } - - return success - } catch (e: Exception) { - Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}") - return false - } - } - - /** - * 增加音量 - */ - fun increaseVolume(): Boolean { - // 假设当前音量保存在设备中,我们只需发送增加命令 - return adjustVolume(16) // 发送最大音量命令 - } - - /** - * 减小音量 - */ - fun decreaseVolume(): Boolean { - // 假设当前音量保存在设备中,我们只需发送减小命令 - return adjustVolume(0) // 发送最小音量命令 - } - - /** - * 清理资源 - */ - fun cleanup() { - stopScan() - scanHandler.removeCallbacks(scanResultUpdateRunnable) - bluetoothGatt?.close() - bluetoothGatt = null - notifyChar = null - writeChar = null - } -} \ No newline at end of file diff --git a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt b/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt deleted file mode 100644 index f09c69f50..000000000 --- a/local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt +++ /dev/null @@ -1,294 +0,0 @@ -package com.yunqiinnovation.jieli - -import android.app.Activity -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.util.Log -import androidx.annotation.NonNull -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import kotlinx.coroutines.* - -/** JieliPlugin */ -class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel.StreamHandler { - /// 方法通道 - private lateinit var methodChannel: MethodChannel - /// 统一事件通道 - private lateinit var eventChannel: EventChannel - - private lateinit var context: Context - private var activity: Activity? = null - - /// BLE服务实例 - private lateinit var bleService: BleService - - // 协程作用域 - private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - // 事件接收器 - private var eventSink: EventChannel.EventSink? = null - - // 事件类型常量 - companion object { - private const val EVENT_TYPE_AUDIO_DATA = "audio_data" - private const val EVENT_TYPE_CONNECTION_STATE = "connection_state" - private const val EVENT_TYPE_SCAN_RESULT = "scan_result" - private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal" - private const val EVENT_TYPE_COMMAND_RESULT = "command_result" - } - - // 蓝牙相关请求码 - private val REQUEST_ENABLE_BT = 1 - private val REQUEST_PERMISSION_BLE = 2 - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - val binaryMessenger = flutterPluginBinding.binaryMessenger - - // 设置方法通道 - methodChannel = MethodChannel(binaryMessenger, "com.yunqiinnovation.jieli") - methodChannel.setMethodCallHandler(this) - - // 设置统一事件通道 - eventChannel = EventChannel(binaryMessenger, "com.yunqiinnovation.jieli/events") - eventChannel.setStreamHandler(this) - - context = flutterPluginBinding.applicationContext - - // 初始化BLE服务 - bleService = BleService(context) - - // 设置回调 - setupBleCallbacks() - } - - /** - * 设置BLE服务回调 - */ - private fun setupBleCallbacks() { - // 设置统一回调接口 - bleService.callback = object : BleService.Callback { - override fun onScanResult(devices: List>) { - pluginScope.launch(Dispatchers.Main) { - sendEvent(EVENT_TYPE_SCAN_RESULT, devices) - } - } - - override fun onConnectionStateChanged(state: Int) { - pluginScope.launch(Dispatchers.Main) { - sendEvent(EVENT_TYPE_CONNECTION_STATE, state) - } - } - - override fun onAudioDataReceived(data: ByteArray) { - pluginScope.launch(Dispatchers.Main) { - // Log.d("","数据流onAudioDataReceived${data}") - sendEvent(EVENT_TYPE_AUDIO_DATA, data) - } - } - - override fun onWakeupSignalReceived() { - pluginScope.launch(Dispatchers.Main) { - sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true) - } - } - - override fun onCommandResult(commandType: Int, success: Boolean) { - pluginScope.launch(Dispatchers.Main) { - sendEvent(EVENT_TYPE_COMMAND_RESULT, mapOf( - "commandType" to commandType, - "success" to success - )) - } - } - } - } - - /** - * 发送事件到Flutter端 - */ - private fun sendEvent(eventType: String, data: Any?) { - eventSink?.success(mapOf( - "type" to eventType, - "data" to data - )) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "startScan" -> { - val scanTimeout = call.argument("scanTimeout") ?: 10000 - - if (!hasRequiredPermissions()) { - requestBlePermissions() - result.error("PERMISSION_DENIED", "需要蓝牙权限才能扫描设备", null) - return - } - - val success = bleService.startScan(scanTimeout.toLong()) - result.success(success) - } - "stopScan" -> { - val success = bleService.stopScan() - result.success(success) - } - "connect" -> { - val deviceId = call.argument("deviceId") - if (deviceId == null) { - result.error("INVALID_ARGUMENT", "设备ID不能为空", null) - return - } - - if (!hasRequiredPermissions()) { - requestBlePermissions() - result.error("PERMISSION_DENIED", "需要蓝牙权限才能连接设备", null) - return - } - - bleService.connect(deviceId) - result.success(true) - } - "disconnect" -> { - bleService.disconnect() - result.success(true) - } - "sendStartRecordingCommand" -> { - val success = bleService.sendStartRecordingCommand() - result.success(success) - } - "sendStopRecordingCommand" -> { - val success = bleService.sendStopRecordingCommand() - result.success(success) - } - "isBluetoothEnabled" -> { - val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - val bluetoothAdapter = bluetoothManager.adapter - result.success(bluetoothAdapter?.isEnabled == true) - } - "requestBluetoothEnable" -> { - if (activity == null) { - result.error("ACTIVITY_NOT_AVAILABLE", "Activity不可用,无法请求打开蓝牙", null) - return - } - - val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - val bluetoothAdapter = bluetoothManager.adapter - - if (bluetoothAdapter == null) { - result.error("BLUETOOTH_UNAVAILABLE", "设备不支持蓝牙", null) - return - } - - if (bluetoothAdapter.isEnabled) { - result.success(true) - return - } - - // 请求打开蓝牙 - val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) - result.success(true) - } - "adjustVolume" -> { - val volume = call.argument("volume") ?: 0 - val success = bleService.adjustVolume(volume) - result.success(success) - } - "increaseVolume" -> { - val success = bleService.increaseVolume() - result.success(success) - } - "decreaseVolume" -> { - val success = bleService.decreaseVolume() - result.success(success) - } - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - methodChannel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - - bleService.cleanup() - pluginScope.cancel() - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - } - - override fun onDetachedFromActivityForConfigChanges() { - activity = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - } - - override fun onDetachedFromActivity() { - activity = null - } - - /// EventChannel.StreamHandler接口实现 - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - eventSink = events - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } - - /// 检查是否拥有所需权限 - private fun hasRequiredPermissions(): Boolean { - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - hasPermission(android.Manifest.permission.BLUETOOTH_SCAN) && - hasPermission(android.Manifest.permission.BLUETOOTH_CONNECT) - } else { - hasPermission(android.Manifest.permission.BLUETOOTH) && - hasPermission(android.Manifest.permission.BLUETOOTH_ADMIN) - } - } - - /// 检查单个权限 - private fun hasPermission(permission: String): Boolean { - return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED - } - - /// 请求BLE权限 - private fun requestBlePermissions() { - activity?.let { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - ActivityCompat.requestPermissions( - it, - arrayOf( - android.Manifest.permission.BLUETOOTH_SCAN, - android.Manifest.permission.BLUETOOTH_CONNECT - ), - REQUEST_PERMISSION_BLE - ) - } else { - ActivityCompat.requestPermissions( - it, - arrayOf( - android.Manifest.permission.BLUETOOTH, - android.Manifest.permission.BLUETOOTH_ADMIN - ), - REQUEST_PERMISSION_BLE - ) - } - } - } -} \ No newline at end of file diff --git a/local_plugins/jieli/lib/jieli.dart b/local_plugins/jieli/lib/jieli.dart deleted file mode 100644 index d0d318627..000000000 --- a/local_plugins/jieli/lib/jieli.dart +++ /dev/null @@ -1,283 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; - -/// 设备类,代表一个BLE设备 -class BleDevice { - final String id; - final String name; - final int rssi; - - BleDevice({required this.id, required this.name, required this.rssi}); - - factory BleDevice.fromJson(Map json) { - return BleDevice( - id: json['id'] as String, - name: json['name'] as String? ?? 'Unknown Device', - rssi: json['rssi'] as int? ?? 0, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'rssi': rssi, - }; - } - - @override - String toString() => 'BleDevice(id: $id, name: $name, rssi: $rssi)'; -} - -/// 连接状态 -enum ConnectionState { - disconnected, - connecting, - connected, - disconnecting, - error -} - -/// 事件类型 -class EventType { - static const String audioData = 'audio_data'; - static const String connectionState = 'connection_state'; - static const String scanResult = 'scan_result'; - static const String wakeupSignal = 'wakeup_signal'; - static const String commandResult = 'command_result'; -} - -/// 杰理BLE插件主类 -class Jieli { - static const MethodChannel _methodChannel = - MethodChannel('com.yunqiinnovation.jieli'); - static const EventChannel _eventChannel = - EventChannel('com.yunqiinnovation.jieli/events'); - - /// 扫描结果流 - static final StreamController> _scanController = - StreamController>.broadcast(); - Stream> get scanResults => _scanController.stream; - - /// 连接状态变化流 - static final StreamController _connectionController = - StreamController.broadcast(); - Stream get connectionState => _connectionController.stream; - - /// 音频数据流 - static final StreamController _audioController = - StreamController.broadcast(); - Stream get audioDataStream => _audioController.stream; - - /// 唤醒信号流 - static final StreamController _wakeupController = - StreamController.broadcast(); - Stream get wakeupStream => _wakeupController.stream; - - /// 命令结果流 - static final StreamController _commandResultController = - StreamController.broadcast(); - Stream get commandResultStream => - _commandResultController.stream; - - /// 原始事件流 - Stream>? _eventStream; - - /// 事件流订阅 - StreamSubscription? _eventSubscription; - - /// 命令类型常量 - static const int commandTypeVolume = 1; - - /// 构造函数,初始化平台通道和事件监听 - Jieli() { - _setupEventListener(); - } - - /// 设置事件监听器 - void _setupEventListener() { - _eventSubscription = - _eventChannel.receiveBroadcastStream().listen(_handleEvent); - } - - /// 处理来自原生平台的事件 - void _handleEvent(dynamic event) { - if (event is! Map) return; - - final eventData = Map.from(event); - final String eventType = eventData['type'] as String; - final dynamic data = eventData['data']; - - switch (eventType) { - case EventType.scanResult: - if (data is List) { - final deviceList = data - .map( - (item) => BleDevice.fromJson(Map.from(item))) - .toList(); - _scanController.add(deviceList); - } - break; - case EventType.connectionState: - if (data is int && data >= 0 && data < ConnectionState.values.length) { - _connectionController.add(ConnectionState.values[data]); - } - break; - case EventType.audioData: - if (data is Uint8List) { - //print('收到ble通道消息: $data'); - _audioController.add(data); - } - break; - case EventType.wakeupSignal: - if (data is bool) { - _wakeupController.add(data); - } - break; - case EventType.commandResult: - if (data is Map) { - final commandType = data['commandType'] as int; - final success = data['success'] as bool; - _commandResultController.add(CommandResult( - commandType: commandType, - success: success, - )); - } - break; - } - } - - /// 开始扫描BLE设备 - Future startScan({int scanTimeout = 10000}) async { - final result = await _methodChannel.invokeMethod( - 'startScan', - {'scanTimeout': scanTimeout}, - ); - return result ?? false; - } - - /// 停止扫描BLE设备 - Future stopScan() async { - final result = await _methodChannel.invokeMethod('stopScan'); - return result ?? false; - } - - /// 连接到指定设备 - Future connect(String deviceId) async { - final result = await _methodChannel.invokeMethod( - 'connect', - {'deviceId': deviceId}, - ); - return result ?? false; - } - - /// 断开连接 - Future disconnect() async { - final result = await _methodChannel.invokeMethod('disconnect'); - return result ?? false; - } - - /// 发送开始录音命令 - Future sendStartRecordingCommand() async { - final result = - await _methodChannel.invokeMethod('sendStartRecordingCommand'); - return result ?? false; - } - - /// 发送停止录音命令 - Future sendStopRecordingCommand() async { - final result = - await _methodChannel.invokeMethod('sendStopRecordingCommand'); - return result ?? false; - } - - /// 关闭插件,释放资源 - Future dispose() async { - await stopScan(); - // await disconnect(); - // _eventSubscription?.cancel(); - // _scanController.close(); - // _connectionController.close(); - // _audioController.close(); - // _wakeupController.close(); - // _commandResultController.close(); - } - - /// 检查蓝牙是否已启用 - Future isBluetoothEnabled() async { - final result = - await _methodChannel.invokeMethod('isBluetoothEnabled'); - return result ?? false; - } - - /// 请求打开蓝牙 - Future requestBluetoothEnable() async { - final result = - await _methodChannel.invokeMethod('requestBluetoothEnable'); - return result ?? false; - } - - /// 调整音量 - /// [volume] 音量值,范围0-16 - Future adjustVolume(int volume) async { - try { - final result = await _methodChannel.invokeMethod( - 'adjustVolume', - {'volume': volume}, - ); - return result ?? false; - } catch (e) { - print('调整音量失败: $e'); - return false; - } - } - - /// 增加音量 - Future increaseVolume() async { - try { - final result = await _methodChannel.invokeMethod('increaseVolume'); - return result ?? false; - } catch (e) { - print('增加音量失败: $e'); - return false; - } - } - - /// 减小音量 - Future decreaseVolume() async { - try { - final result = await _methodChannel.invokeMethod('decreaseVolume'); - return result ?? false; - } catch (e) { - print('减小音量失败: $e'); - return false; - } - } - - /// 命令结果回调 - Stream get onCommandResult => commandResultStream - .where((event) => event.commandType == commandTypeVolume) - .map((event) => event); -} - -/// 蓝牙设备模型 -class BluetoothDevice { - // ... existing code ... -} - -/// 命令结果 -class CommandResult { - final int commandType; - final bool success; - - CommandResult({ - required this.commandType, - required this.success, - }); - - @override - String toString() => - 'CommandResult{commandType: $commandType, success: $success}'; -} diff --git a/local_plugins/jieli/pubspec.yaml b/local_plugins/jieli/pubspec.yaml deleted file mode 100644 index ab230e685..000000000 --- a/local_plugins/jieli/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: jieli -description: A Flutter plugin for connecting with Jieli BLE earphones -version: 0.0.1 -homepage: - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.jieli - pluginClass: JieliPlugin \ No newline at end of file diff --git a/local_plugins/jl_ota/android/build.gradle b/local_plugins/jl_ota/android/build.gradle index 76e296450..dd9b88141 100644 --- a/local_plugins/jl_ota/android/build.gradle +++ b/local_plugins/jl_ota/android/build.gradle @@ -18,9 +18,6 @@ allprojects { repositories { google() mavenCentral() - flatDir { - dirs 'libs' - } } } @@ -51,8 +48,8 @@ android { } dependencies { - implementation(name: 'jl_bt_ota_V1.9.0-release', ext: 'aar') - implementation(project(":ble_service")) + // implementation project(':ble_service') + testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.mockito:mockito-core:5.0.0") } @@ -68,4 +65,4 @@ android { } } } -} +} \ No newline at end of file diff --git a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar deleted file mode 100644 index c33c59d48..000000000 Binary files a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar and /dev/null differ diff --git a/local_plugins/jl_ota/android/settings.gradle b/local_plugins/jl_ota/android/settings.gradle index d5825d78e..0ff55ddd6 100644 --- a/local_plugins/jl_ota/android/settings.gradle +++ b/local_plugins/jl_ota/android/settings.gradle @@ -1 +1 @@ -rootProject.name = 'jl_ota' +rootProject.name = 'jl_ota' \ No newline at end of file diff --git a/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt b/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt index c61dfc70d..9a76c5a11 100644 --- a/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt +++ b/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt @@ -14,32 +14,18 @@ import android.os.Handler import android.os.Looper import android.util.Log import androidx.annotation.NonNull +//import com.yunqiinnovation.ble_service.BleService -import com.jieli.jl_bt_ota.constant.ErrorCode; -import com.jieli.jl_bt_ota.constant.StateCode; -import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; -import com.jieli.jl_bt_ota.interfaces.BtEventCallback; -import com.jieli.jl_bt_ota.interfaces.IActionCallback; -import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; -import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; -import com.jieli.jl_bt_ota.model.base.BaseError; -import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; - - -import com.yunqiinnovation.ble_service.BleService /** JlOtaPlugin */ -class JlOtaPlugin(): FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - private lateinit var context: Context - private lateinit var eventChannel: EventChannel - private var streamSink: EventChannel.EventSink? = null - /// BLE服务实例 - private var otaManager: OTAManager? = null - +class JlOtaPlugin() : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private lateinit var context: Context + private lateinit var eventChannel: EventChannel + private var streamSink: EventChannel.EventSink? = null companion object { private const val TAG = "JlOtaPlugin" @@ -48,10 +34,11 @@ class JlOtaPlugin(): FlutterPlugin, MethodCallHandler { } - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") - channel.setMethodCallHandler(this) + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") + channel.setMethodCallHandler(this) eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) @@ -65,215 +52,25 @@ class JlOtaPlugin(): FlutterPlugin, MethodCallHandler { } }) - - otaManager = OTAManager(context) - -// otaManager.getBluetoothOption().setFirmwareFilePath(filePath); -// otaManager.startOTA(new IUpgradeCallback() { -// } - - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "getPlatformVersion") { - - val bluetoothOption = BluetoothOTAConfigure() - //选择通讯方式 - bluetoothOption.priority = BluetoothOTAConfigure.PREFER_BLE - //是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现) - bluetoothOption.isUseReconnect = true - //是否启用设备认证流程(与固件工程师确认) - bluetoothOption.isUseAuthDevice = true - //设置BLE的MTU - bluetoothOption.mtu = 500 - //是否需要改变BLE的MTU - bluetoothOption.isNeedChangeMtu = false - //是否启用杰理服务器(暂时不支持) - bluetoothOption.isUseJLServer = false - Log.d(TAG, "bluetoothOTAManager初始化成功") - //配置OTA参数 - otaManager?.configure(bluetoothOption) - - - var filePath= "/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files/downloads/ota_upgrades/1.ufw" -otaManager?.bluetoothOption?.firmwareFilePath = filePath -Log.d(TAG, "OpusManager路径成功${filePath}") -// Add ?. after bluetoothOption ^ - otaManager?.startOTA( - CustomUpdateCallback( - context, null, this - ) - ) - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() } - } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - -} -class OTAManager(context: Context) : BluetoothOTAManager(context) { - override fun getConnectedDevice(): BluetoothDevice? { - // 这里可以添加空实现的注释说明 - return null -} - override fun getConnectedBluetoothGatt(): BluetoothGatt? { - // 这里可以添加空实现的注释说明 - return null -} - override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { - // 这里可以添加空实现的注释说明 + override fun onMethodCall(call: MethodCall, result: Result) { -} -override fun disconnectBluetoothDevice -(bluetoothDevice: BluetoothDevice?) { - // 这里可以添加空实现的注释说明 + if (call.method == "getPlatformVersion") { -} - /** 数据发送方法(协议无关) */ - override fun sendDataToDevice( - bluetoothDevice: BluetoothDevice?, - bytes: ByteArray? - ): Boolean {return true} -} + // BleService.otaManager?.configureOTA() -private class CustomUpdateCallback( - val context: Context, - val device: BluetoothDevice?, - val viewModel: JlOtaPlugin - ) : IUpgradeCallback{ - companion object { - private const val TAG = "JlOtaPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" - - } - override fun onStartOTA() { - Log.d(TAG, "onStartOTA开始升级") - // viewModel.otaStateMLD.value = OTAStart(device) - } - - override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { - Log.d(TAG, "onNeedReconnect需要重新链接") - // viewModel.otaStateMLD.value = OTAReconnect(device, addr, isNewReconnectWay) - // if (viewModel.isUseReconnectWay()) { - // viewModel.reconnectDev(addr, isNewReconnectWay) - // } - } - - override fun onProgress(type: Int, progress: Float) { - Log.d(TAG, "onProgress正在进展") - //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) - } - - override fun onStopOTA() { - Log.d(TAG, "onStartOTA停止升级") -// viewModel.otaStateMLD.value = -// OTAEnd(device, ErrorCode.ERR_NONE, context.getString(R.string.ota_complete)) -// val handler = Handler(Looper.getMainLooper()) -// handler.postDelayed({ -// // Log.d("ZHM", "onStopOTA: disconnectDevice connected : "+viewModel.bluetoothHelper.isConnected()+ " getConnectedDevice: "+viewModel.bluetoothHelper.getConnectedDevice()) -// //变地址导致无法判断device是否一致 -// if (viewModel.bluetoothHelper.isConnected()/* && BluetoothUtil.deviceEquals( -// device, -// viewModel.bluetoothHelper.getConnectedDevice() -// )*/ -// ) { -// Log.d("ZHM", "onStopOTA: disconnectDevice") -// viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice) -// } -// }, 500) - } - - override fun onCancelOTA() { - Log.d(TAG, "onCancelOTA取消升级") - // viewModel.otaStateMLD.value = - // OTAEnd( - // device, - // ErrorCode.ERR_UNKNOWN, - // context.getString(R.string.ota_upgrade_cancel) - // ) - } - - override fun onError(error: BaseError?) { - Log.d(TAG, "onError升级错误") - // error?.let { - // viewModel.otaStateMLD.value = OTAEnd(device, it.subCode, it.message) - // } - // viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice)//升级失败,断开设备,方便自动化继续升级 + // BleService.otaManager?.startOTA() + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() } } -// class OtaDemo { -// private val otaManager = OTAManager(MainApplication.getInstance()) - // fun initOTA(context: Context, firmwarePath: String) { - // val otaManager = JlOtaPlugin() - // val bluetoothOption = BluetoothOTAConfigure.createDefault().apply { - // priority = BluetoothOTAConfigure.PREFER_BLE - // useAuthDevice = true - // bleIntervalMs = 500 - // timeoutMs = 3000 - // mtu = 500 - // needChangeMtu = false - // useReconnect = false - // setFirmwareFilePath(firmwarePath) - // } - // otaManager.configure(bluetoothOption) - // } - // fun otaFirmware(filePath: String) { // 方法名改为小写开头 - // val otaManager = JlOtaPlugin() - - // otaManager.registerBluetoothCallback(object : BtEventCallback { - // override fun onConnection(device: BluetoothDevice, status: Int) { - // if (status == StateCode.CONNECTION_OK) { - // if (otaManager.isOTA) return - - // otaManager.queryMandatoryUpdate(object : IActionCallback { - // override fun onSuccess(deviceInfo: TargetInfoResponse) { - // // 处理设备信息 - // deviceInfo.versionCode - // deviceInfo.versionName - // deviceInfo.projectCode - // deviceInfo.uid - // deviceInfo.pid - // } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } - // override fun onError(baseError: BaseError) { - // if (baseError.code == ErrorCode.ERR_NONE && - // baseError.subCode == ErrorCode.ERR_NONE) { - // val deviceInfo = otaManager.deviceInfo - // deviceInfo?.let { - // it.versionCode - // it.versionName - // it.projectCode - // it.uid - // it.pid - // } - // } - // } - // }) - // } - // } - // }) - // otaManager.bluetoothOption?.setFirmwareFilePath(filePath) - - // otaManager.startOTA(object : IUpgradeCallback { - // override fun onStartOTA() = Unit - // override fun onNeedReconnect(addr: String, isNewReconnectWay: Boolean) { - // if (otaManager.bluetoothOption?.useReconnect == true) { - // // 处理自定义回连 - // } - // } - // override fun onProgress(type: Int, progress: Float) = Unit - // override fun onStopOTA() = Unit - // override fun onCancelOTA() = Unit - // override fun onError(error: BaseError) = Unit - // }) +} - // otaManager.release() - // } -//} \ No newline at end of file diff --git a/local_plugins/music_service/android/build.gradle b/local_plugins/music_service/android/build.gradle index 3a98cc976..1f0ad0cf4 100644 --- a/local_plugins/music_service/android/build.gradle +++ b/local_plugins/music_service/android/build.gradle @@ -66,6 +66,5 @@ android { } dependencies { - implementation project(':deepvoice_utils') implementation("androidx.media:media:1.6.0") } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 7843697d1..b5db17325 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -17,7 +17,7 @@ import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.session.MediaButtonReceiver -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.atomic.AtomicBoolean @@ -46,16 +46,15 @@ class MusicService : MediaBrowserServiceCompat() { override fun onCreate() { super.onCreate() - FileLogger.init(this) isRunning.set(true) setupMediaSession() startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - FileLogger.i(TAG, "MediaService 启动完成") + Log.i(TAG, "MediaService 启动完成") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - FileLogger.i(TAG, "onStartCommand: $intent") + Log.i(TAG, "onStartCommand: $intent") intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { // 分发给 MediaSession 处理媒体按钮 MediaButtonReceiver.handleIntent(mediaSession, intent) @@ -107,10 +106,10 @@ class MusicService : MediaBrowserServiceCompat() { ) // 回调中处理具体操作 setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()} - override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()} - override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()} - override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()} + override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()} + override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()} + override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()} + override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()} }) // 初始播放状态 setPlaybackState( @@ -221,7 +220,7 @@ class MusicService : MediaBrowserServiceCompat() { private fun play(song: Map) { val url = song["url"] ?: return - FileLogger.i(TAG, "开始播放: $url") + Log.i(TAG, "开始播放: $url") val title = song["title"] ?: "未知标题" val artist = song["artist"] ?: "未知歌手" val coverUrl = song["coverUrl"] @@ -234,11 +233,11 @@ class MusicService : MediaBrowserServiceCompat() { setOnPreparedListener { start() updateNotification(title,artist,coverUrl) - FileLogger.i(TAG, "开始播放: $url") + Log.i(TAG, "开始播放: $url") } setOnCompletionListener { playNextOrStop() } setOnErrorListener { _, what, extra -> - FileLogger.e(TAG, "播放错误 what=$what extra=$extra") + Log.e(TAG, "播放错误 what=$what extra=$extra") true } prepareAsync() @@ -261,7 +260,7 @@ class MusicService : MediaBrowserServiceCompat() { val coverUrl = song["coverUrl"] updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); - FileLogger.i(TAG, "已暂停") + Log.i(TAG, "已暂停") } } @@ -281,7 +280,7 @@ class MusicService : MediaBrowserServiceCompat() { val coverUrl = song["coverUrl"] updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); -// FileLogger.i(TAG, "继续播放") +// Log.i(TAG, "继续播放") } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 89bf0edd8..9756b25f8 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.music_service import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import com.yunqiinnovation.music_service.MusicServiceStarter import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - FileLogger.d(TAG, "Method call: ${call.method}") + Log.d(TAG, "Method call: ${call.method}") when (call.method) { "play" -> { val song = call.argument>("song") diff --git a/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml b/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml index e38cdeb21..69c9202a9 100644 --- a/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 6dd0bd660..9759eacc7 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -586,11 +586,17 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { return } - try { - handleStreamJson(JSONObject(data), toolCalls, callback, messages) - } catch (e: Exception) { - // 忽略解析错误,但记录日志 - Log.e(TAG, "解析JSON出错: ${e.message}", e) + // 验证JSON是否完整(至少检查大括号匹配) + if (isValidJson(data)) { + try { + handleStreamJson(JSONObject(data), toolCalls, callback, messages) + } catch (e: Exception) { + // 忽略解析错误,但记录日志 + Log.e(TAG, "解析JSON出错: ${e.message}", e) + } + } else { + // 记录不完整的JSON,但不尝试解析它 + Log.d(TAG, "收到不完整的JSON数据,跳过处理, data=$data") } } } @@ -879,6 +885,39 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { fun onFunctionCall(functionCall: JSONObject) fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) } + + /** + * 检查字符串是否为有效的JSON + * 使用简单的括号匹配和引号检查来确定JSON是否完整 + */ + private fun isValidJson(jsonStr: String): Boolean { + if (jsonStr.isEmpty()) return false + if (!jsonStr.startsWith("{") || !jsonStr.endsWith("}")) return false + + var braceCount = 0 + var insideQuotes = false + var escapeNext = false + + for (char in jsonStr) { + if (escapeNext) { + escapeNext = false + continue + } + + when (char) { + '\\' -> escapeNext = true + '"' -> if (!escapeNext) insideQuotes = !insideQuotes + '{' -> if (!insideQuotes) braceCount++ + '}' -> if (!insideQuotes) braceCount-- + } + + // 如果括号不匹配(闭合太多),提前返回 + if (braceCount < 0) return false + } + + // 所有引号都应该闭合,所有括号也应该匹配 + return braceCount == 0 && !insideQuotes + } } /** diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt index 5010c6d70..b491eb1bd 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.provider.ContactsContract import android.provider.AlarmClock import android.provider.CalendarContract +import android.provider.MediaStore import android.provider.Settings import android.telephony.SmsManager import android.util.Log @@ -18,6 +19,17 @@ import org.json.JSONObject import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import android.view.KeyEvent +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import android.Manifest +import android.content.ContentValues +import android.content.ContentUris +import androidx.core.content.ContextCompat +import android.app.ActivityManager +import android.app.Application +import android.os.Process /** * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 */ @@ -63,6 +75,9 @@ class SystemFunctionHandler(private val context: Context) { registerMediaPreviousFunction(client) registerMediaNextFunction(client) + // 注册打开录音机功能 + registerOpenRecorderFunction(client) + Log.d(TAG, "所有系统功能注册完成") } catch (e: Exception) { Log.e(TAG, "注册系统功能失败: ${e.message}", e) @@ -405,6 +420,30 @@ class SystemFunctionHandler(private val context: Context) { Log.d(TAG, "已注册媒体下一首功能") } + /** + * 注册打开录音机功能 + */ + private fun registerOpenRecorderFunction(client: MCPClient) { + client.registerLocalFunction( + "open_recorder", + "打开系统录音机并开始录音", + JSONObject(""" + { + "type": "object", + "properties": {}, + "required": [] + } + """), + object : FunctionHandler { + override suspend fun handle(arguments: Map): String { + val result = openRecorder() + return "{\"result\": \"${result.message}\"}" + } + } + ) + Log.d(TAG, "已注册打开录音机功能") + } + /** * 控制媒体播放 * @param action 操作类型:"play", "pause", "previous", "next" @@ -551,6 +590,93 @@ class SystemFunctionHandler(private val context: Context) { ) } + /** + * 检查权限并根据应用状态采取不同行为 + * @param permission 需要检查的权限 + * @param permissionName 权限名称(用于显示给用户) + * @param functionName 功能名称(用于日志和提示) + * @return 是否拥有权限 + */ + private fun checkPermissionAndGuide(permission: String, permissionName: String, functionName: String): Boolean { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "缺少${permissionName}权限,无法执行${functionName}") + + // 检查应用是否在前台 + if (isAppInForeground()) { + // 应用在前台,尝试直接弹出权限申请框 + try { + // 首先尝试发送广播通知DeepSound应用弹出权限申请对话框 + val intent = Intent("com.yunqiinnovation.deepsound.action.REQUEST_PERMISSION").apply { + setPackage(context.packageName) + putExtra("permission", permission) + putExtra("permissionName", permissionName) + putExtra("functionName", functionName) + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + context.sendBroadcast(intent) + + // 由于插件无法直接请求权限(需要Activity),上面的广播会让主应用来处理权限请求 + // 这里设置一个短暂延迟,让用户有时间响应权限请求 + Thread.sleep(500) + + // 再次检查权限是否已授予(以防用户立即授予) + if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) { + return true + } + } catch (e: Exception) { + Log.e(TAG, "请求权限失败: ${e.message}", e) + } + + // 如果权限仍未授予,引导用户到权限管理页面 + try { + // 尝试跳转到应用内的权限管理页面 + val intent = Intent("com.yunqiinnovation.deepsound.action.PERMISSIONS_SETTINGS").apply { + setPackage(context.packageName) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + // 如果应用内权限页面不可用,则跳转到系统应用设置页面 + val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(settingsIntent) + } + } catch (e: Exception) { + Log.e(TAG, "打开权限设置页面失败: ${e.message}", e) + } + } + // 应用在后台或权限请求失败,只返回文字提示 + return false + } + return true + } + + /** + * 检查应用是否在前台运行 + * @return 应用是否在前台 + */ + private fun isAppInForeground(): Boolean { + try { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName = context.packageName + + for (appProcess in appProcesses) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcess.processName == packageName) { + return true + } + } + } catch (e: Exception) { + Log.e(TAG, "检查应用状态失败: ${e.message}", e) + } + return false + } + /** * 发送短信 * @param contact 联系人(名称或电话号码) @@ -561,6 +687,14 @@ class SystemFunctionHandler(private val context: Context) { Log.d(TAG, "发送短信给 $contact: $message") try { + // 检查发送短信权限 + if (!checkPermissionAndGuide(Manifest.permission.SEND_SMS, "发送短信", "发送短信")) { + return Result( + success = false, + message = "需要短信权限才能发送短信,请在权限管理中开启短信权限" + ) + } + val phoneNumber = getPhoneNumberForContact(contact) if (phoneNumber.isEmpty()) { return Result( @@ -595,6 +729,14 @@ class SystemFunctionHandler(private val context: Context) { Log.d(TAG, "拨打电话给 $contact") try { + // 检查拨打电话权限 + if (!checkPermissionAndGuide(Manifest.permission.CALL_PHONE, "拨打电话", "拨打电话")) { + return Result( + success = false, + message = "需要电话权限才能拨打电话,请在权限管理中开启电话权限" + ) + } + val phoneNumber = getPhoneNumberForContact(contact) if (phoneNumber.isEmpty()) { return Result( @@ -684,6 +826,11 @@ class SystemFunctionHandler(private val context: Context) { return contact.replace(Regex("[^+\\d]"), "") } + // 检查读取联系人权限 + if (!checkPermissionAndGuide(Manifest.permission.READ_CONTACTS, "读取联系人", "访问联系人")) { + return "" + } + // 从联系人数据库查询 try { val contentResolver = context.contentResolver @@ -747,55 +894,62 @@ class SystemFunctionHandler(private val context: Context) { ) } - // 使用系统日历应用创建提醒 - val intent = Intent(Intent.ACTION_INSERT).apply { - data = CalendarContract.Events.CONTENT_URI - putExtra(CalendarContract.Events.TITLE, title) - putExtra(CalendarContract.Events.DESCRIPTION, content) - putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time) - putExtra(CalendarContract.EXTRA_EVENT_END_TIME, time + 3600000) // 默认1小时 - putExtra(CalendarContract.Events.HAS_ALARM, 1) - putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY) - flags = Intent.FLAG_ACTIVITY_NEW_TASK + // 检查日历权限 + if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) { + return Result( + success = false, + message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限" + ) + } + + // 直接使用ContentProvider写入日历 + val endTime = time + 3600000 // 默认1小时后结束 + + // 获取默认日历ID + val calendarId = getDefaultCalendarId() + if (calendarId == -1L) { + Log.w(TAG, "未找到默认日历") + return tryCreateAlarm(title, content, timeStr) + } + + // 创建事件 + val eventValues = ContentValues().apply { + put(CalendarContract.Events.CALENDAR_ID, calendarId) + put(CalendarContract.Events.TITLE, title) + put(CalendarContract.Events.DESCRIPTION, content) + put(CalendarContract.Events.DTSTART, time) + put(CalendarContract.Events.DTEND, endTime) + put(CalendarContract.Events.HAS_ALARM, 1) + put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) + } + + val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues) + if (eventUri == null) { + Log.e(TAG, "插入日历事件失败") + return tryCreateAlarm(title, content, timeStr) + } + + // 获取事件ID + val eventId = ContentUris.parseId(eventUri) + + // 添加提醒(15分钟前提醒) + val reminderValues = ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒 } - // 检查是否有处理该Intent的应用 - val calendarActivity = intent.resolveActivity(context.packageManager) - if (calendarActivity != null) { - context.startActivity(intent) + val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues) + if (reminderUri != null) { return Result( success = true, - message = "已创建提醒: $title, 时间: $timeStr" + message = "已成功创建提醒: $title, 时间: $timeStr" ) } else { - // 备选方案:如果没有日历应用,尝试创建闹钟 - try { - // 解析小时和分钟 - val timeParts = timeStr.split(" ")[1].split(":") - val hour = timeParts[0].toInt() - val minute = timeParts[1].toInt() - - val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM) - alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content") - alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour) - alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute) - alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, false) - alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - if (alarmIntent.resolveActivity(context.packageManager) != null) { - context.startActivity(alarmIntent) - return Result( - success = true, - message = "已创建闹钟提醒: $title, 时间: $timeStr" - ) - } - } catch (e: Exception) { - Log.e(TAG, "创建闹钟失败: ${e.message}", e) - } - + // 仍然创建了事件,但没有提醒 return Result( - success = false, - message = "未找到可用的日历或闹钟应用" + success = true, + message = "已创建日历事件,但未设置提醒: $title" ) } } catch (e: Exception) { @@ -807,6 +961,69 @@ class SystemFunctionHandler(private val context: Context) { } } + /** + * 获取默认日历ID + */ + private fun getDefaultCalendarId(): Long { + val projection = arrayOf(CalendarContract.Calendars._ID) + var calendarId = -1L + + try { + val uri = CalendarContract.Calendars.CONTENT_URI + val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)" + context.contentResolver.query(uri, projection, selection, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + calendarId = cursor.getLong(0) + } else { + // 如果没有找到主日历,尝试找任何可见的日历 + context.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c -> + if (c.moveToFirst()) { + calendarId = c.getLong(0) + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "获取默认日历ID失败: ${e.message}", e) + } + + return calendarId + } + + /** + * 尝试创建闹钟提醒(备选方案) + */ + private fun tryCreateAlarm(title: String, content: String, timeStr: String): Result { + try { + // 解析小时和分钟 + val timeParts = timeStr.split(" ")[1].split(":") + val hour = timeParts[0].toInt() + val minute = timeParts[1].toInt() + + val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM) + alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content") + alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour) + alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute) + alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true) // 尝试跳过UI + alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + if (alarmIntent.resolveActivity(context.packageManager) != null) { + context.startActivity(alarmIntent) + return Result( + success = true, + message = "已创建闹钟提醒: $title, 时间: $timeStr" + ) + } + } catch (e: Exception) { + Log.e(TAG, "创建闹钟失败: ${e.message}", e) + } + + return Result( + success = false, + message = "未找到可用的日历或闹钟应用" + ) + } + /** * 获取当前时间 * @param format 时间格式(可选) @@ -878,36 +1095,21 @@ class SystemFunctionHandler(private val context: Context) { } /** - * 获取当前位置 + * 获取当前位置(简化版,只获取最后已知位置) * @return 操作结果 */ - suspend fun getCurrentLocation(): Result = suspendCancellableCoroutine { continuation -> + suspend fun getCurrentLocation(): Result = withContext(Dispatchers.Main) { Log.d(TAG, "开始获取当前位置") try { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager // 检查位置权限 - val fineLocationPermission = android.Manifest.permission.ACCESS_FINE_LOCATION - val coarseLocationPermission = android.Manifest.permission.ACCESS_COARSE_LOCATION - - val hasFineLocationPermission = context.checkPermission( - fineLocationPermission, android.os.Process.myPid(), android.os.Process.myUid() - ) == PackageManager.PERMISSION_GRANTED - - val hasCoarseLocationPermission = context.checkPermission( - coarseLocationPermission, android.os.Process.myPid(), android.os.Process.myUid() - ) == PackageManager.PERMISSION_GRANTED - - if (!hasFineLocationPermission && !hasCoarseLocationPermission) { - Log.d(TAG, "没有位置权限") - - // 如果没有权限,返回需要授权的结果 - continuation.resume(Result( + if (!checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) { + return@withContext Result( success = false, - message = "需要位置权限才能获取当前位置" - )) - return@suspendCancellableCoroutine + message = "需要位置权限才能获取当前位置,请在权限管理中开启位置权限" + ) } // 检查位置服务是否开启 @@ -915,153 +1117,157 @@ class SystemFunctionHandler(private val context: Context) { !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { Log.d(TAG, "位置服务未启用") - - // 提醒用户打开位置服务 + // 引导用户打开位置服务 try { - val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - context.startActivity(settingsIntent) + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) } catch (e: Exception) { - Log.e(TAG, "无法打开位置设置: ${e.message}") + Log.e(TAG, "打开位置设置失败: ${e.message}", e) } - continuation.resume(Result( + return@withContext Result( success = false, message = "位置服务未启用,请打开GPS或网络定位" - )) - return@suspendCancellableCoroutine + ) } - // 尝试先获取最后已知位置 + // 获取最后已知位置 var bestLocation: Location? = null - try { - // 检查网络提供者最后已知位置 - if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) - if (networkLocation != null) { - bestLocation = networkLocation - Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}") - } + // 尝试获取网络位置 + if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) + if (networkLocation != null) { + bestLocation = networkLocation + Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}") } - - // 检查GPS提供者最后已知位置 - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) { - bestLocation = gpsLocation - Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}") - } + } + + // 尝试获取GPS位置(如果更精确) + if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) { + bestLocation = gpsLocation + Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}") } - } catch (e: Exception) { - Log.e(TAG, "获取最后已知位置失败: ${e.message}") } - // 如果有最后已知位置,直接返回结果 - if (bestLocation != null) { + // 返回结果 + return@withContext if (bestLocation != null) { val message = "当前位置: 经度 ${bestLocation.longitude}, 纬度 ${bestLocation.latitude}" Log.d(TAG, message) - continuation.resume(Result( - success = true, - message = message - )) - return@suspendCancellableCoroutine + Result(success = true, message = message) + } else { + Log.d(TAG, "无法获取位置信息") + Result(success = false, message = "无法获取位置信息,请稍后重试") } - // 如果没有最后已知位置,尝试请求更新位置(设置10秒超时) - val locationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - // 收到位置更新 - val message = "当前位置: 经度 ${location.longitude}, 纬度 ${location.latitude}" - Log.d(TAG, "位置更新: $message") - locationManager.removeUpdates(this) - - if (continuation.isActive) { - continuation.resume(Result( - success = true, - message = message - )) - } - } - - @Deprecated("Deprecated in Java") - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { - // 不需要处理 - } - - override fun onProviderEnabled(provider: String) { - // 不需要处理 - } + } catch (e: Exception) { + Log.e(TAG, "获取位置过程中发生异常: ${e.message}") + return@withContext Result( + success = false, + message = "获取位置失败: ${e.message}" + ) + } + } + + /** + * 打开系统录音机并尝试自动开始录音 + * @return 操作结果 + */ + fun openRecorder(): Result { + Log.d(TAG, "打开系统录音机并尝试自动开始录音") + + try { + // 创建用于启动录音机应用的Intent + val recordIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION) + recordIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + // 添加额外参数尝试自动开始录音(不是所有应用都支持) + recordIntent.putExtra("android.intent.extra.quickCapture", true) + + // 检查是否有应用能处理此Intent + if (recordIntent.resolveActivity(context.packageManager) != null) { + context.startActivity(recordIntent) + return Result( + success = true, + message = "已打开录音机,尝试自动开始录音" + ) + } else { + // 尝试使用备用方法,针对不同厂商应用添加特定参数 + val commonRecorderIntents = mapOf( + "com.android.soundrecorder" to Intent().setClassName("com.android.soundrecorder", "com.android.soundrecorder.SoundRecorder").setAction("android.intent.action.RECORD_SOUND"), + "com.sec.android.app.voicenote" to Intent().setClassName("com.sec.android.app.voicenote", "com.sec.android.app.voicenote.Main").putExtra("launch_mode", "record"), + "com.miui.soundrecorder" to Intent().setClassName("com.miui.soundrecorder", "com.miui.soundrecorder.SoundRecorderActivity").putExtra("startOnBoot", true), + "com.huawei.recorder" to Intent().setClassName("com.huawei.recorder", "com.huawei.recorder.activity.MainActivity").putExtra("autoRecord", true), + "com.google.android.apps.recorder" to Intent().setClassName("com.google.android.apps.recorder", "com.google.android.apps.recorder.ui.RecorderActivity").putExtra("start_recording", true) + ) - override fun onProviderDisabled(provider: String) { - // 如果提供者被禁用,检查是否还有其他可用提供者 - if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && - !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - - locationManager.removeUpdates(this) - - if (continuation.isActive) { - continuation.resume(Result( - success = false, - message = "位置服务已被禁用" - )) + for ((packageName, intent) in commonRecorderIntents) { + try { + if (isPackageInstalled(packageName, context.packageManager)) { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + return Result( + success = true, + message = "已打开录音应用并尝试自动开始录音" + ) } + } catch (e: Exception) { + Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}") + // 继续尝试下一个应用 } } - } - - // 注册位置更新请求 - if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - locationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, - 0L, - 0f, - locationListener - ) - } - - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - 0L, - 0f, - locationListener + + // 如果特定Intent都失败,回退到原来的方法 + val commonRecorderPackages = listOf( + "com.android.soundrecorder", + "com.sec.android.app.voicenote", + "com.miui.soundrecorder", + "com.huawei.recorder", + "com.google.android.apps.recorder" ) - } - - // 设置超时取消 - continuation.invokeOnCancellation { - try { - locationManager.removeUpdates(locationListener) - } catch (e: Exception) { - Log.e(TAG, "取消位置更新失败: ${e.message}") - } - } - - // 启动超时处理 - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ - if (continuation.isActive) { + + for (packageName in commonRecorderPackages) { try { - locationManager.removeUpdates(locationListener) - continuation.resume(Result( - success = false, - message = "获取位置超时,请稍后重试" - )) + val packageIntent = context.packageManager.getLaunchIntentForPackage(packageName) + if (packageIntent != null) { + packageIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + // 尝试添加通用的自动开始录音参数 + packageIntent.putExtra("android.intent.extra.quickCapture", true) + context.startActivity(packageIntent) + return Result( + success = true, + message = "已打开录音应用,请手动开始录音" + ) + } } catch (e: Exception) { - Log.e(TAG, "超时处理失败: ${e.message}") + Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}") } } - }, 10000L) // 10秒超时 - - } catch (e: Exception) { - Log.e(TAG, "获取位置过程中发生异常: ${e.message}") - if (continuation.isActive) { - continuation.resume(Result( + + return Result( success = false, - message = "获取位置失败: ${e.message}" - )) + message = "未找到录音应用" + ) } + } catch (e: Exception) { + Log.e(TAG, "打开录音机失败: ${e.message}", e) + return Result( + success = false, + message = "打开录音机失败: ${e.message}" + ) + } + } + + // 检查应用是否已安装 + private fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean { + return try { + packageManager.getPackageInfo(packageName, 0) + true + } catch (e: Exception) { + false } } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt index c8363dba8..247fd6cc0 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt @@ -165,7 +165,7 @@ class CustomSseClientTransport( // 解析JSON-RPC消息 val data = event.data if (data != null) { - Log.d(TAG, "收到事件数据: $data") + // Log.d(TAG, "收到事件数据: $data") try { // 尝试安全地解析JSON消息 safeParseMessage(data) diff --git a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift index 99fd86008..a9cb2e17b 100644 --- a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift +++ b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift @@ -178,7 +178,7 @@ public class MCPClient { /// 检查是否已连接 /// - Returns: 是否已连接 - func isConnected() -> Bool { + func checkIsConnected() -> Bool { return isConnected } diff --git a/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift b/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift index 1eb798ef9..54276fcc8 100644 --- a/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift +++ b/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift @@ -21,14 +21,28 @@ private class ToolCallInfo { } } -/// OpenAI服务流回调类型定义 -public typealias StreamCallback = ( - onToken: (String) -> Void, - onComplete: () -> Void, - onError: (Error) -> Void, - onFunctionCall: ([String: Any]) -> Void, - onFunctionCallResult: ([String: Any], [String: Any]) -> Void -) +/// OpenAI服务流回调结构体定义 +struct StreamCallback { + let onToken: (String) -> Void + let onComplete: () -> Void + let onError: (Error) -> Void + let onFunctionCall: ([String: Any]) -> Void + let onFunctionCallResult: ([String: Any], [String: Any]) -> Void + + init( + onToken: @escaping (String) -> Void, + onComplete: @escaping () -> Void, + onError: @escaping (Error) -> Void, + onFunctionCall: @escaping ([String: Any]) -> Void, + onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void + ) { + self.onToken = onToken + self.onComplete = onComplete + self.onError = onError + self.onFunctionCall = onFunctionCall + self.onFunctionCallResult = onFunctionCallResult + } +} /// OpenAI服务iOS原生实现 public class OpenAIService { @@ -88,8 +102,8 @@ public class OpenAIService { /// 检查MCP客户端是否已初始化 /// - Returns: 是否已初始化 - public func isMcpInitialized() -> Bool { - return isMcpInitialized && mcpClient?.isConnected() == true + public func checkMcpInitialized() -> Bool { + return isMcpInitialized && mcpClient?.checkIsConnected() == true } /// 关闭MCP客户端 @@ -292,7 +306,7 @@ public class OpenAIService { functionCall: [String: Any], messages: [[String: Any]], systemPrompt: String, - callback: @escaping StreamCallback + callback: StreamCallback ) { Task { do { @@ -498,10 +512,10 @@ public class OpenAIService { } /// 发送消息(流式输出) - public func sendMessageStream( + internal func sendMessageStream( messages: [[String: Any]], systemPrompt: String = "", - callback: @escaping StreamCallback + callback: StreamCallback ) { guard isInitialized, !apiKey.isEmpty else { callback.onError(OpenAIError("OpenAI服务未初始化")) @@ -597,12 +611,12 @@ public class OpenAIService { } /// 发送函数调用结果 - public func sendFunctionCallResult( + internal func sendFunctionCallResult( messages: [[String: Any]], systemPrompt: String, functionCall: [String: Any], functionResult: String, - callback: @escaping StreamCallback + callback: StreamCallback ) { do { // 构建完整消息数组 @@ -666,7 +680,7 @@ public class OpenAIService { private var finalToolCalls: [Int: ToolCallInfo] = [:] init( - callback: @escaping StreamCallback, + callback: StreamCallback, autoHandleMcpTools: Bool = true, autoToolHandler: @escaping ([String: Any]) -> Void ) { diff --git a/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift b/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift index 0284055a8..ec703d0d3 100644 --- a/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift +++ b/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift @@ -1,6 +1,37 @@ import Flutter import UIKit +// 修改OpenAIService类,添加转发方法 +extension OpenAIService { + // 为插件提供的公共发送消息方法 + public func publicSendMessageStream(messages: [[String: Any]], systemPrompt: String, onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void, onFunctionCall: @escaping ([String: Any]) -> Void, onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void) { + + let callback = StreamCallback( + onToken: onToken, + onComplete: onComplete, + onError: onError, + onFunctionCall: onFunctionCall, + onFunctionCallResult: onFunctionCallResult + ) + + sendMessageStream(messages: messages, systemPrompt: systemPrompt, callback: callback) + } + + // 为插件提供的公共发送函数调用结果方法 + public func publicSendFunctionCallResult(messages: [[String: Any]], systemPrompt: String, functionCall: [String: Any], functionResult: String, onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void, onFunctionCall: @escaping ([String: Any]) -> Void, onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void) { + + let callback = StreamCallback( + onToken: onToken, + onComplete: onComplete, + onError: onError, + onFunctionCall: onFunctionCall, + onFunctionCallResult: onFunctionCallResult + ) + + sendFunctionCallResult(messages: messages, systemPrompt: systemPrompt, functionCall: functionCall, functionResult: functionResult, callback: callback) + } +} + public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { // OpenAI服务实例 private let openAIService = OpenAIService() @@ -87,29 +118,26 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler // 在后台线程执行 DispatchQueue.global(qos: .userInitiated).async { - let callback: OpenAIService.StreamCallback = ( + self.openAIService.publicSendMessageStream( + messages: messagesRaw, + systemPrompt: systemPrompt, onToken: { token in - // 发送token事件 DispatchQueue.main.async { eventSink(["type": "token", "content": token]) } }, onComplete: { - // 发送完成事件 DispatchQueue.main.async { eventSink(["type": "complete"]) } }, onError: { error in - // 发送错误事件 DispatchQueue.main.async { eventSink(["type": "error", "content": error.localizedDescription]) } }, onFunctionCall: { functionCall in - // 发送函数调用事件 DispatchQueue.main.async { - // 转换为字符串 do { let data = try JSONSerialization.data(withJSONObject: functionCall) if let jsonString = String(data: data, encoding: .utf8) { @@ -123,7 +151,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } }, onFunctionCallResult: { functionCall, functionCallResult in - // 发送函数调用结果事件 DispatchQueue.main.async { do { let functionCallData = try JSONSerialization.data(withJSONObject: functionCall) @@ -142,8 +169,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } ) - self.openAIService.sendMessageStream(messages: messagesRaw, systemPrompt: systemPrompt, callback: callback) - // 请求已开始 DispatchQueue.main.async { result(true) @@ -169,29 +194,28 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler // 在后台线程执行 DispatchQueue.global(qos: .userInitiated).async { - let callback: OpenAIService.StreamCallback = ( + self.openAIService.publicSendFunctionCallResult( + messages: messagesRaw, + systemPrompt: systemPrompt, + functionCall: functionCallRaw, + functionResult: functionResult, onToken: { token in - // 发送token事件 DispatchQueue.main.async { eventSink(["type": "token", "content": token]) } }, onComplete: { - // 发送完成事件 DispatchQueue.main.async { eventSink(["type": "complete"]) } }, onError: { error in - // 发送错误事件 DispatchQueue.main.async { eventSink(["type": "error", "content": error.localizedDescription]) } }, onFunctionCall: { functionCall in - // 发送函数调用事件 DispatchQueue.main.async { - // 转换为字符串 do { let data = try JSONSerialization.data(withJSONObject: functionCall) if let jsonString = String(data: data, encoding: .utf8) { @@ -205,7 +229,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } }, onFunctionCallResult: { functionCall, functionCallResult in - // 发送函数调用结果事件 DispatchQueue.main.async { do { let functionCallData = try JSONSerialization.data(withJSONObject: functionCall) @@ -224,14 +247,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } ) - self.openAIService.sendFunctionCallResult( - messages: messagesRaw, - systemPrompt: systemPrompt, - functionCall: functionCallRaw, - functionResult: functionResult, - callback: callback - ) - // 请求已开始 DispatchQueue.main.async { result(true) @@ -290,7 +305,7 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } case "isMcpInitialized": - result(openAIService.isMcpInitialized()) + result(openAIService.checkMcpInitialized()) case "closeMcpClient": result(openAIService.closeMcpClient()) diff --git a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift index 65c8a8add..3e638830c 100644 --- a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift +++ b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift @@ -442,7 +442,7 @@ class GetCurrentTimeHandler: FunctionHandler { } /// 获取当前位置处理器 -class GetCurrentLocationHandler: FunctionHandler { +class GetCurrentLocationHandler: NSObject, FunctionHandler { private let locationManager = CLLocationManager() private var locationCompletionHandler: ((String, Bool) -> Void)? diff --git a/local_plugins/open_ai_service/ios/open_ai_service.podspec b/local_plugins/open_ai_service/ios/open_ai_service.podspec index d06d07fed..ecf9c5383 100644 --- a/local_plugins/open_ai_service/ios/open_ai_service.podspec +++ b/local_plugins/open_ai_service/ios/open_ai_service.podspec @@ -15,7 +15,7 @@ A Flutter plugin for OpenAI services. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index 4e477b3ee..b21882f77 100644 Binary files a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock and b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index 8aeb8c861..9139b185a 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Tue May 20 09:53:38 CST 2025 -gradle.version=8.9 +#Sun May 18 21:39:38 IST 2025 +gradle.version=8.10 diff --git a/local_plugins/jieli/android/build.gradle.kts b/local_plugins/speech/android/build.gradle.kts similarity index 58% rename from local_plugins/jieli/android/build.gradle.kts rename to local_plugins/speech/android/build.gradle.kts index a8a6e7ac4..4e5186a27 100644 --- a/local_plugins/jieli/android/build.gradle.kts +++ b/local_plugins/speech/android/build.gradle.kts @@ -4,11 +4,12 @@ plugins { // Kotlin Android 插件 id("org.jetbrains.kotlin.android") id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("plugin.serialization") version "1.9.24" } android { // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.jieli" + namespace = "com.deep_voice.speech" // 目标 SDK 版本 compileSdk = 35 @@ -46,12 +47,18 @@ android { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") - implementation("androidx.core:core-ktx:1.7.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") - implementation("androidx.lifecycle:lifecycle-service:2.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") - implementation(project(":azure_speech")) - implementation(project(":jl_opus")) -} \ No newline at end of file + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("com.squareup.okhttp3:okhttp:4.9.3") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.3") + implementation("com.anthropic:anthropic-java:0.8.0") + implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") + implementation("io.ktor:ktor-client-core:3.1.2") + implementation("io.ktor:ktor-client-cio:3.1.2") + implementation("io.ktor:ktor-client-okhttp:3.1.2") + implementation("io.ktor:ktor-client-logging:3.1.2") + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("androidx.media:media:1.6.0") +} \ No newline at end of file diff --git a/local_plugins/speech/android/settings.gradle.kts b/local_plugins/speech/android/settings.gradle.kts new file mode 100644 index 000000000..efad09063 --- /dev/null +++ b/local_plugins/speech/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "speech" \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/AndroidManifest.xml b/local_plugins/speech/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d3f8e2308 --- /dev/null +++ b/local_plugins/speech/android/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt new file mode 100644 index 000000000..d805a757e --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt @@ -0,0 +1,52 @@ +package com.deep_voice.speech.asr + +/** + * ASR事件类型枚举 + */ +enum class AsrEventType { + /** 会话开始 */ + SESSION_STARTED, + + /** 会话结束 */ + SESSION_STOPPED, + + + /** 识别中(中间结果) */ + RECOGNIZING, + + /** 识别完成(最终结果) */ + RECOGNIZED, + + /** 识别取消 */ + CANCELED, + + /** 发生错误 */ + ERROR, + +} + +/** + * ASR事件数据类 + * + * @param type 事件类型 + * @param params 事件参数,根据不同的事件类型包含不同的内容: + * - ERROR: 包含 "errorCode" 和 "errorMessage" + * - CANCELED: 包含 "reason" 和 "errorDetails" + * - RECOGNIZING/RECOGNIZED: 包含 "text", "languageCode" + */ +data class AsrEvent( + val type: AsrEventType, + val params: Map = emptyMap() +) + +/** + * ASR事件监听接口 + */ +interface AsrEventListener { + /** + * 当发生ASR事件时调用 + * + * @param event ASR事件 + */ + fun onEvent(event: AsrEvent) +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt new file mode 100644 index 000000000..94da1b435 --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt @@ -0,0 +1,90 @@ +package com.deep_voice.speech.asr + +/** + * ASR服务通用接口 + * + * 定义了语音识别服务的标准接口,可用于不同提供商的实现,如火山引擎、Azure等 + */ +interface IAsrService { + /** + * 初始化语音识别服务 + * + * @param subscriptionKey 服务订阅密钥/访问令牌 + * @param region 服务区域/应用ID + * @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"] + * @return 是否初始化成功 + */ + suspend fun initialize( + subscriptionKey: String, + region: String, + supportedLanguages: List? = null + ): Boolean + + /** + * 单次识别(非流式) + * + * @param audioData 要识别的音频数据字节数组 + * @param callback 识别结果回调 + * @return 是否成功开始识别 + */ + suspend fun recognizeOnce(audioData: ByteArray, callback: (String) -> Unit): Boolean + + /** + * 启动连续识别 + * 开始持续监听音频输入并识别 + * + * @return 是否成功启动 + */ + suspend fun startContinuousRecognition(): Boolean + + /** + * 停止连续识别 + * + * @return 是否成功停止 + */ + suspend fun stopContinuousRecognition(): Boolean + + /** + * 推送音频数据 + * 在连续识别模式下推送新的音频数据 + * + * @param audioData 要识别的音频数据字节数组 + * @return 是否成功处理 + */ + suspend fun pushAudioData(audioData: ByteArray): Boolean + + /** + * 添加ASR事件监听器 + * + * @param listener 事件监听器 + */ + fun addListener(listener: AsrEventListener) + + /** + * 移除ASR事件监听器 + * + * @param listener 要移除的事件监听器 + */ + fun removeListener(listener: AsrEventListener) + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + fun release() + + /** + * 判断当前是否正在识别 + * + * @return 是否正在进行语音识别 + */ + fun isRecognizing(): Boolean + + /** + * 设置识别参数 + * + * @param params 参数键值对 + * @return 是否设置成功 + */ + fun setParameters(params: Map): Boolean +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt new file mode 100644 index 000000000..91c8945d6 --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt @@ -0,0 +1,92 @@ +package com.deep_voice.speech.tts + +/** + * TTS服务通用接口 + * + * 定义了语音合成服务的标准接口,可用于不同提供商的实现,如微软Azure、火山语音等 + */ +interface ITtsService { + /** + * 初始化语音合成服务 + * + * @param ttsAppId 服务应用ID + * @param ttsAppToken 服务访问令牌/订阅密钥 + * @param ttsResource 服务资源ID/区域(可选) + * @param language 语言代码,如"zh-CN" + * @return 是否初始化成功 + */ + fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String = "", language: String = "zh-CN"): Boolean + + /** + * 设置语音角色 + * + * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同) + * @return 是否设置成功 + */ + fun setVoice(voiceName: String): Boolean + + /** + * 单次播放文本(非流式) + * + * @param text 要合成的文本 + * @return 是否成功开始合成 + */ + fun speakOnce(text: String): Boolean + + /** + * 流式合成 + * 在流式场景下合成并播放文本,适用于连续的文本输入 + * + * @param text 要合成的文本片段 + * @return 是否成功处理 + */ + fun speakStream(text: String): Boolean + + /** + * 刷新并播放流式文本缓冲区中的剩余内容 + * + * @return 是否成功处理 + */ + fun flushStream(): Boolean + + /** + * 停止当前合成和播放 + * + * @return 是否成功停止 + */ + fun stop(): Boolean + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + fun release() + + /** + * 添加TTS事件监听器 + * + * @param listener 事件监听器 + */ + fun addListener(listener: TtsEventListener) + + /** + * 移除TTS事件监听器 + * + * @param listener 要移除的事件监听器 + */ + fun removeListener(listener: TtsEventListener) + + /** + * 添加音频数据监听器 + * + * @param listener 音频数据监听器 + */ + fun addAudioDataListener(listener: AudioDataListener) + + /** + * 移除音频数据监听器 + * + * @param listener 要移除的音频数据监听器 + */ + fun removeAudioDataListener(listener: AudioDataListener) +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt new file mode 100644 index 000000000..cb69a849d --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt @@ -0,0 +1,55 @@ +package com.deep_voice.speech.tts + +/** + * TTS事件类型枚举 + */ +enum class TtsEventType { + /** 开始合成 */ + SYNTHESIS_STARTED, + + /** 合成完成 */ + SYNTHESIS_COMPLETED, + + /** 合成取消 */ + SYNTHESIS_CANCELED, + + /** 发生错误 */ + ERROR +} + +/** + * TTS事件数据类 + * + * @param type 事件类型 + * @param params 事件参数,根据不同的事件类型包含不同的内容 + * - ERROR: 包含 "errorCode" 和 "errorMessage" + * - CANCELED: 包含 "reason" 和 "errorDetails" + */ +data class TtsEvent( + val type: TtsEventType, + val params: Map = emptyMap() +) + +/** + * TTS事件监听接口 + */ +interface TtsEventListener { + /** + * 当发生TTS事件时调用 + * + * @param event TTS事件 + */ + fun onEvent(event: TtsEvent) +} + +/** + * 音频数据监听接口 + */ +interface AudioDataListener { + /** + * 当接收到音频数据时调用 + * + * @param data 音频数据字节数组 + */ + fun onAudioData(data: ByteArray) +} \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/build.gradle.kts b/local_plugins/volcano_speech/android/build.gradle.kts index cf924302d..54987bb97 100644 --- a/local_plugins/volcano_speech/android/build.gradle.kts +++ b/local_plugins/volcano_speech/android/build.gradle.kts @@ -1,21 +1,11 @@ -import com.android.build.gradle.LibraryExtension - buildscript { repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.3.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") - } -} - -allprojects { - repositories { - google() - mavenCentral() - + classpath("com.android.tools.build:gradle:7.3.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21") } } @@ -24,10 +14,9 @@ plugins { id("org.jetbrains.kotlin.android") } -// 配置android扩展 -configure { +android { namespace = "com.yunqiinnovation.volcano_speech" - compileSdkVersion(35) + compileSdk = 35 defaultConfig { minSdk = 21 @@ -38,6 +27,10 @@ configure { targetCompatibility = JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = "11" + } + sourceSets { getByName("main") { manifest.srcFile("src/main/AndroidManifest.xml") @@ -49,17 +42,23 @@ configure { lintOptions { isCheckReleaseBuilds = false } -} - -// 显式设置Kotlin JVM目标版本 -tasks.withType { - kotlinOptions { - jvmTarget = "11" - } + } dependencies { + // Flutter插件依赖 + // compileOnly("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681") + debugImplementation("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681") + releaseImplementation("io.flutter:flutter_embedding_release:1.0.0-a794cf2681") + // 添加火山引擎语音合成SDK - // implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") - implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") + implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") + // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") + // implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") + implementation(project(":speech")) +} + +repositories { + google() + mavenCentral() } \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt index 92073c7b4..20f65258a 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt @@ -6,7 +6,7 @@ import android.os.Looper import com.bytedance.speech.speechengine.SpeechEngine import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineGenerator -import com.yunqiinnovation.volcano_speech.utils.FileLogger +import android.util.Log import org.json.JSONException import org.json.JSONObject import org.json.JSONArray @@ -84,7 +84,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun initialize(appId: String, token: String, resourceId: String = "volc.bigasr.sauc.duration"): Boolean { if (isInitialized) { - FileLogger.i(TAG, "引擎已经初始化") + Log.i(TAG, "引擎已经初始化") return true } @@ -94,7 +94,7 @@ class VolcanoAsrHelper(private val context: Context) { try { SpeechEngineGenerator.PrepareEnvironment(context, null) } catch (e: Exception) { - FileLogger.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化") + Log.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化") } // 创建引擎 @@ -102,7 +102,7 @@ class VolcanoAsrHelper(private val context: Context) { engineHandler = engine?.createEngine() ?: -1 if (engineHandler == -1L) { - FileLogger.e(TAG, "创建引擎失败") + Log.e(TAG, "创建引擎失败") state = AsrState.ERROR return false } @@ -125,10 +125,10 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token) // 设置资源ID - 大模型必需 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId) + // engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId) // 设置协议类型为Seed - 大模型必需 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) + // engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) // 设置网络配置 engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com") @@ -176,7 +176,7 @@ class VolcanoAsrHelper(private val context: Context) { isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR if (isInitialized) { - FileLogger.i(TAG, "引擎初始化成功") + Log.i(TAG, "引擎初始化成功") state = AsrState.READY // 设置回调监听 @@ -189,12 +189,12 @@ class VolcanoAsrHelper(private val context: Context) { return true } else { - FileLogger.e(TAG, "引擎初始化失败: $result") + Log.e(TAG, "引擎初始化失败: $result") state = AsrState.ERROR return false } } catch (e: Exception) { - FileLogger.e(TAG, "初始化异常: ${e.message}", e) + Log.e(TAG, "初始化异常: ${e.message}", e) state = AsrState.ERROR return false } @@ -205,12 +205,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setAudioInputType(type: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isEngineStarted) { - FileLogger.e(TAG, "引擎已启动,无法更改配置") + Log.e(TAG, "引擎已启动,无法更改配置") return false } @@ -219,7 +219,7 @@ class VolcanoAsrHelper(private val context: Context) { "mic" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER "stream" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_STREAM else -> { - FileLogger.e(TAG, "不支持的音频输入类型: $type") + Log.e(TAG, "不支持的音频输入类型: $type") return false } } @@ -227,7 +227,7 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType) return true } catch (e: Exception) { - FileLogger.e(TAG, "设置音频输入类型失败: ${e.message}", e) + Log.e(TAG, "设置音频输入类型失败: ${e.message}", e) return false } } @@ -237,12 +237,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setAudioParams(sampleRate: Int, channels: Int): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isEngineStarted) { - FileLogger.e(TAG, "引擎已启动,无法更改配置") + Log.e(TAG, "引擎已启动,无法更改配置") return false } @@ -267,7 +267,7 @@ class VolcanoAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "设置音频参数失败: ${e.message}", e) + Log.e(TAG, "设置音频参数失败: ${e.message}", e) return false } } @@ -277,7 +277,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setLanguage(language: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -286,7 +286,7 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language) return true } catch (e: Exception) { - FileLogger.e(TAG, "设置语言失败: ${e.message}", e) + Log.e(TAG, "设置语言失败: ${e.message}", e) return false } } @@ -296,7 +296,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setHotWords(hotWordsId: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -308,7 +308,7 @@ class VolcanoAsrHelper(private val context: Context) { } return true } catch (e: Exception) { - FileLogger.e(TAG, "设置热词失败: ${e.message}", e) + Log.e(TAG, "设置热词失败: ${e.message}", e) return false } } @@ -317,14 +317,14 @@ class VolcanoAsrHelper(private val context: Context) { * 一次性识别(异步) */ fun recognizeOnce(callback: RecognizeCallback): Boolean { - FileLogger.i(TAG, "开始一次性识别") + Log.i(TAG, "开始一次性识别") if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isRecognizing) { - FileLogger.e(TAG, "当前已在识别中") + Log.e(TAG, "当前已在识别中") return false } @@ -345,7 +345,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "启动识别失败: $ret") + Log.e(TAG, "启动识别失败: $ret") return false } @@ -354,7 +354,7 @@ class VolcanoAsrHelper(private val context: Context) { state = AsrState.RECOGNIZING return true } catch (e: Exception) { - FileLogger.e(TAG, "识别异常: ${e.message}", e) + Log.e(TAG, "识别异常: ${e.message}", e) return false } } @@ -364,12 +364,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun stopRecognize(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } @@ -379,7 +379,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止识别失败: $ret") + Log.e(TAG, "停止识别失败: $ret") return false } } else { @@ -387,7 +387,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止识别失败: $ret") + Log.e(TAG, "停止识别失败: $ret") return false } @@ -398,7 +398,7 @@ class VolcanoAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "停止识别异常: ${e.message}", e) + Log.e(TAG, "停止识别异常: ${e.message}", e) return false } } @@ -407,14 +407,14 @@ class VolcanoAsrHelper(private val context: Context) { * 开始连续识别 */ fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - FileLogger.i(TAG, "开始连续识别") + Log.i(TAG, "开始连续识别") if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isRecognizing) { - FileLogger.e(TAG, "当前已在识别中") + Log.e(TAG, "当前已在识别中") return false } @@ -436,17 +436,17 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "启动连续识别失败: $ret") + Log.e(TAG, "启动连续识别失败: $ret") return false } isRecognizing = true isEngineStarted = true state = AsrState.RECOGNIZING - FileLogger.i(TAG, "开始连续识别") + Log.i(TAG, "开始连续识别") return true } catch (e: Exception) { - FileLogger.e(TAG, "连续识别异常: ${e.message}", e) + Log.e(TAG, "连续识别异常: ${e.message}", e) return false } } @@ -456,7 +456,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun stopContinuousRecognition(): Boolean { if (!isInitialized || !isRecognizing) { - FileLogger.e(TAG, "引擎未初始化或未在识别中") + Log.e(TAG, "引擎未初始化或未在识别中") return false } @@ -465,7 +465,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止连续识别失败: $ret") + Log.e(TAG, "停止连续识别失败: $ret") return false } @@ -474,7 +474,7 @@ class VolcanoAsrHelper(private val context: Context) { state = AsrState.READY return true } catch (e: Exception) { - FileLogger.e(TAG, "停止连续识别异常: ${e.message}", e) + Log.e(TAG, "停止连续识别异常: ${e.message}", e) return false } } @@ -485,17 +485,17 @@ class VolcanoAsrHelper(private val context: Context) { */ fun feedAudio(data: ByteArray): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") + Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") return false } @@ -504,13 +504,13 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.feedAudio(engineHandler, data, data.size) if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "发送音频数据失败: $ret") + Log.e(TAG, "发送音频数据失败: $ret") return false } return true } catch (e: Exception) { - FileLogger.e(TAG, "发送音频数据异常: ${e.message}", e) + Log.e(TAG, "发送音频数据异常: ${e.message}", e) return false } } @@ -521,17 +521,17 @@ class VolcanoAsrHelper(private val context: Context) { */ fun feedAudioEnd(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") + Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") return false } @@ -540,13 +540,13 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.feedAudio(engineHandler, ByteArray(0), 0) if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "发送音频结束标记失败: $ret") + Log.e(TAG, "发送音频结束标记失败: $ret") return false } return true } catch (e: Exception) { - FileLogger.e(TAG, "发送音频结束标记异常: ${e.message}", e) + Log.e(TAG, "发送音频结束标记异常: ${e.message}", e) return false } } @@ -604,9 +604,9 @@ class VolcanoAsrHelper(private val context: Context) { isInitialized = false state = AsrState.IDLE - FileLogger.i(TAG, "引擎已释放") + Log.i(TAG, "引擎已释放") } catch (e: Exception) { - FileLogger.e(TAG, "释放引擎异常: ${e.message}", e) + Log.e(TAG, "释放引擎异常: ${e.message}", e) } } @@ -617,7 +617,7 @@ class VolcanoAsrHelper(private val context: Context) { when (type) { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { // 引擎启动成功 - FileLogger.i(TAG, "引擎启动成功") + Log.i(TAG, "引擎启动成功") mainHandler.post { continuousCallback?.onSessionStarted() } @@ -625,7 +625,7 @@ class VolcanoAsrHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { // 引擎停止 - FileLogger.i(TAG, "引擎停止") + Log.i(TAG, "引擎停止") isRecognizing = false isEngineStarted = false state = AsrState.READY @@ -637,15 +637,15 @@ class VolcanoAsrHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> { // 中间识别结果 - FileLogger.i(TAG, "中间识别结果: $data") + Log.i(TAG, "中间识别结果: $data") try { val json = JSONObject(data) if (!json.has("result")) { - FileLogger.w(TAG, "中间结果没有 result 字段: $data") + Log.w(TAG, "中间结果没有 result 字段: $data") return } // if (json.has("definite")) { - // FileLogger.i(TAG, "utterances: ${json.getString("utterances")}") + // Log.i(TAG, "utterances: ${json.getString("utterances")}") // } // 从多种可能的格式中获取文本 val text = extractTextFromResult(json) @@ -655,23 +655,23 @@ class VolcanoAsrHelper(private val context: Context) { if (text.isNotEmpty()) { mainHandler.post { - FileLogger.i(TAG, "提取到中间文本: $text") + Log.i(TAG, "提取到中间文本: $text") // 中间结果 continuousCallback?.onRecognizing(text, detectedLanguage) } } } catch (e: Exception) { - FileLogger.e(TAG, "解析中间识别结果异常: ${e}") + Log.e(TAG, "解析中间识别结果异常: ${e}") } } SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> { - FileLogger.i(TAG, "最终识别结果: $data") + Log.i(TAG, "最终识别结果: $data") // 最终识别结果 try { val json = JSONObject(data) if (!json.has("result")) { - FileLogger.w(TAG, "最终结果没有 result 字段: $data") + Log.w(TAG, "最终结果没有 result 字段: $data") return } @@ -685,22 +685,22 @@ class VolcanoAsrHelper(private val context: Context) { if (text.isNotEmpty()) { mainHandler.post { if (isContinuousMode) { - FileLogger.i(TAG, "发送最终识别结果: $text (连续模式)") + Log.i(TAG, "发送最终识别结果: $text (连续模式)") continuousCallback?.onResult(text, detectedLanguage) // 在连续模式下,不需要手动触发下一轮识别,引擎会自动继续监听 // 但如果需要明确标记新一轮开始,可以在此处添加代码 } else { - FileLogger.i(TAG, "发送最终识别结果: $text (单次模式)") + Log.i(TAG, "发送最终识别结果: $text (单次模式)") recognizeCallback?.onResult(text, detectedLanguage) } } } } catch (e: Exception) { val errMsg = "解析最终识别结果异常: ${e.message}" - FileLogger.e(TAG, errMsg) - FileLogger.e(TAG, "原始数据: $data") - FileLogger.e(TAG, "详细异常信息", e) + Log.e(TAG, errMsg) + Log.e(TAG, "原始数据: $data") + Log.e(TAG, "详细异常信息", e) } } @@ -715,7 +715,7 @@ class VolcanoAsrHelper(private val context: Context) { val errMsg = json.optString("err_msg", "未知错误") val errCode = json.optInt("err_code", -1) - FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") + Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") state = AsrState.ERROR mainHandler.post { @@ -728,7 +728,7 @@ class VolcanoAsrHelper(private val context: Context) { isEngineStarted = false } } catch (e: JSONException) { - FileLogger.e(TAG, "解析错误信息异常: ${e.message}", e) + Log.e(TAG, "解析错误信息异常: ${e.message}", e) } } } @@ -775,8 +775,8 @@ class VolcanoAsrHelper(private val context: Context) { return text } catch (e: Exception) { - FileLogger.e(TAG, "提取文本异常: ${e.message}", e) - FileLogger.e(TAG, "详细异常信息", e) + Log.e(TAG, "提取文本异常: ${e.message}", e) + Log.e(TAG, "详细异常信息", e) return "" } } diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt index 303f92072..a53567cd7 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt @@ -4,14 +4,20 @@ import android.content.Context import android.os.Handler import android.os.Looper import androidx.annotation.NonNull -import com.yunqiinnovation.volcano_speech.utils.FileLogger - import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel +import android.util.Log +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.AudioDataListener /** VolcanoSpeechPlugin */ class VolcanoSpeechPlugin: FlutterPlugin { @@ -89,23 +95,25 @@ class VolcanoSpeechPlugin: FlutterPlugin { // 初始化TTS帮助类 volcanoTtsHelper = VolcanoTtsHelper(context) // 添加TTS事件监听 - volcanoTtsHelper.addListener(object : VolcanoTtsHelper.TtsEventListener { - override fun onEvent(event: VolcanoTtsHelper.TtsEvent) { + volcanoTtsHelper.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { when (event.type) { - VolcanoTtsHelper.TtsEventType.STARTED -> { + TtsEventType.SYNTHESIS_STARTED -> { sendTtsEvent(mapOf("type" to "started")) } - VolcanoTtsHelper.TtsEventType.COMPLETED -> { + TtsEventType.SYNTHESIS_COMPLETED -> { sendTtsEvent(mapOf("type" to "completed")) } - VolcanoTtsHelper.TtsEventType.CANCELED -> { + TtsEventType.SYNTHESIS_CANCELED -> { sendTtsEvent(mapOf("type" to "canceled")) } - VolcanoTtsHelper.TtsEventType.ERROR -> { + TtsEventType.ERROR -> { + val errorCode = event.params["errorCode"] as? String ?: "-1" + val errorMessage = event.params["errorMessage"] as? String ?: "未知错误" sendTtsEvent(mapOf( "type" to "error", - "errorCode" to event.errorCode, - "errorMessage" to event.errorMessage + "errorCode" to errorCode, + "errorMessage" to errorMessage )) } } @@ -113,7 +121,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { }) // 添加TTS音频数据监听 - volcanoTtsHelper.addAudioDataListener(object : VolcanoTtsHelper.AudioDataListener { + volcanoTtsHelper.addAudioDataListener(object : AudioDataListener { override fun onAudioData(data: ByteArray) { sendTtsAudioData(data) } @@ -126,7 +134,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { // 发送ASR事件 private fun sendAsrEvent(event: Map) { if (asrEventSink == null) { - FileLogger.w(TAG, "无法发送ASR事件:事件通道未准备好") + Log.w(TAG, "无法发送ASR事件:事件通道未准备好") return } @@ -134,25 +142,25 @@ class VolcanoSpeechPlugin: FlutterPlugin { try { asrEventSink?.success(event) } catch (e: Exception) { - FileLogger.e(TAG, "发送ASR事件失败: ${e.message}") + Log.e(TAG, "发送ASR事件失败: ${e.message}") } } } // 发送TTS事件 private fun sendTtsEvent(event: Map) { - FileLogger.d(TAG, "发送TTS事件: $event") + Log.d(TAG, "发送TTS事件: $event") if (ttsEventSink == null) { - FileLogger.w(TAG, "无法发送TTS事件:事件通道未准备好") + Log.w(TAG, "无法发送TTS事件:事件通道未准备好") return } mainHandler.post { try { ttsEventSink?.success(event) - FileLogger.d(TAG, "TTS事件发送成功") + Log.d(TAG, "TTS事件发送成功") } catch (e: Exception) { - FileLogger.e(TAG, "发送TTS事件失败: ${e.message}") + Log.e(TAG, "发送TTS事件失败: ${e.message}") } } } @@ -167,7 +175,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { try { ttsAudioDataSink?.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "发送TTS音频数据失败: ${e.message}") + Log.e(TAG, "发送TTS音频数据失败: ${e.message}") } } } @@ -391,7 +399,8 @@ class VolcanoSpeechPlugin: FlutterPlugin { result.success(volcanoTtsHelper.stop()) } "isSpeaking" -> { - result.success(volcanoTtsHelper.isSpeaking()) + // 由于ITtsService不再定义isSpeaking方法,我们可以通过检查是否有正在合成的文本来判断 + result.success(false) // 简化处理,直接返回false } "getCurrentVoice" -> { result.success(volcanoTtsHelper.getCurrentVoice()) @@ -402,7 +411,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { result.success(true) } "release" -> { - volcanoTtsHelper.dispose() + volcanoTtsHelper.release() result.success(true) } else -> { @@ -419,7 +428,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { ttsEventChannel.setStreamHandler(null) ttsAudioDataChannel.setStreamHandler(null) - volcanoTtsHelper.dispose() + volcanoTtsHelper.release() volcanoAsrHelper.release() } } \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt index 46fd10269..1802d5d24 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt @@ -6,14 +6,20 @@ import android.os.Looper import com.bytedance.speech.speechengine.SpeechEngine import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineGenerator -import com.yunqiinnovation.volcano_speech.utils.FileLogger +import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType import org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue +import java.util.UUID /** - * 火山语音合成服务 - 符合TtsService接口要求 + * 火山语音合成服务 - 实现ITtsService接口 */ -class VolcanoTtsHelper(private val context: Context) { +class VolcanoTtsHelper(private val context: Context) : ITtsService { private val TAG = "VolcanoTtsHelper" private val mainHandler = Handler(Looper.getMainLooper()) @@ -24,19 +30,18 @@ class VolcanoTtsHelper(private val context: Context) { // 语音合成状态 private var isEngineStarted = false - private var isPlaying = false private var currentReqId = "" // 配置参数 - private var voice = "zh_male_M392_conversation_wvae_bigtts" + private var voice = "zh_female_linjianvhai_moon_bigtts" // 文本队列管理 private val pendingTexts = ConcurrentLinkedQueue() private var isProcessingQueue = false + private val MAX_PENDING_SENTENCES = 20 // 最大等待句子数,避免内存占用过大 - // 连续合成相关 + // 合成相关 private var currentText = "" - private var isContinuousMode = false private var synthesisFromPlayer = false private var isSynthesisInProgress = false @@ -49,61 +54,47 @@ class VolcanoTtsHelper(private val context: Context) { private val audioDataListeners = mutableListOf() // 事件监听 - private val listeners = mutableListOf() + private val eventListeners = mutableListOf() - /** - * TTS事件类型 - */ - enum class TtsEventType { - STARTED, - COMPLETED, - CANCELED, - ERROR - } + // 流式文本处理 + private val streamBuffer = StringBuffer() + private var lastSpeakTime = 0L - /** - * TTS事件数据类 - */ - data class TtsEvent( - val type: TtsEventType, - val errorCode: Int = 0, - val errorMessage: String = "" - ) + // 跟踪当前合成的播放数 + private var pendingPlayCount = 0 /** - * TTS事件监听接口 + * 当前选择的语音 */ - interface TtsEventListener { - fun onEvent(event: TtsEvent) - } + fun getCurrentVoice(): String = voice /** - * 音频数据监听接口 + * 设置是否启用音频数据回调 */ - interface AudioDataListener { - fun onAudioData(data: ByteArray) + fun setEnableAudioCallback(enable: Boolean) { + this.enableAudioCallback = enable } /** * 添加事件监听器 */ - fun addListener(listener: TtsEventListener) { - if (!listeners.contains(listener)) { - listeners.add(listener) + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) } } /** * 移除事件监听器 */ - fun removeListener(listener: TtsEventListener) { - listeners.remove(listener) + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) } /** * 添加音频数据监听器 */ - fun addAudioDataListener(listener: AudioDataListener) { + override fun addAudioDataListener(listener: AudioDataListener) { if (!audioDataListeners.contains(listener)) { audioDataListeners.add(listener) // 如果添加了音频监听器,自动开启音频回调 @@ -114,7 +105,7 @@ class VolcanoTtsHelper(private val context: Context) { /** * 移除音频数据监听器 */ - fun removeAudioDataListener(listener: AudioDataListener) { + override fun removeAudioDataListener(listener: AudioDataListener) { audioDataListeners.remove(listener) // 如果没有监听器了,可以关闭音频回调 if (audioDataListeners.isEmpty()) { @@ -125,9 +116,10 @@ class VolcanoTtsHelper(private val context: Context) { /** * 分发事件到所有监听器 */ - private fun dispatchEvent(event: TtsEvent) { + private fun dispatchEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) mainHandler.post { - listeners.forEach { it.onEvent(event) } + eventListeners.forEach { it.onEvent(event) } } } @@ -142,27 +134,10 @@ class VolcanoTtsHelper(private val context: Context) { } } - /** - * 当前选择的语音 - */ - fun getCurrentVoice(): String = voice - - /** - * 当前是否正在播放 - */ - fun isSpeaking(): Boolean = isPlaying - - /** - * 设置是否启用音频数据回调 - */ - fun setEnableAudioCallback(enable: Boolean) { - this.enableAudioCallback = enable - } - /** * 初始化语音合成引擎 */ - fun initialize(appId: String, token: String): Boolean { + override fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String): Boolean { if (isInitialized) { return true } @@ -172,7 +147,7 @@ class VolcanoTtsHelper(private val context: Context) { try { SpeechEngineGenerator.PrepareEnvironment(context, null) } catch (e: Exception) { - FileLogger.w(TAG, "准备环境时发生异常: ${e.message}") + Log.w(TAG, "准备环境时发生异常: ${e.message}") } // 创建引擎 @@ -192,8 +167,8 @@ class VolcanoTtsHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id") // 设置授权信息 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;${token}") + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, ttsAppId) + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;${ttsAppToken}") // 设置协议和工作模式 engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) @@ -209,7 +184,11 @@ class VolcanoTtsHelper(private val context: Context) { engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_FRONTEND_INT, 1) engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT, SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA) engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL, true) - + + // engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_INTENT_BOOL, true) + + engine?.setOptionString(SpeechEngineDefines.PARAMS_KEY_TTS_EMOTION_STRING, "happy"); + // 初始化引擎 val result = engine?.initEngine(engineHandler) isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR @@ -238,19 +217,22 @@ class VolcanoTtsHelper(private val context: Context) { */ private fun handleError(code: Int, message: String, e: Exception? = null) { if (e != null) { - FileLogger.e(TAG, message, e) + Log.e(TAG, message, e) } else { - FileLogger.e(TAG, message) + Log.e(TAG, message) } - dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message)) + dispatchEvent(TtsEventType.ERROR, mapOf( + "errorCode" to code.toString(), + "errorMessage" to message + )) } /** * 设置语音 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -264,135 +246,118 @@ class VolcanoTtsHelper(private val context: Context) { } } + + + /** - * 单次播放文本 + * 单次播放文本(未实现,推荐使用speakStream) */ - fun speakOnce(text: String): Boolean { - if (text.isEmpty()) { - return false - } - - // 停止当前播放 - stop() - - // 清空队列 - pendingTexts.clear() - isProcessingQueue = false - isContinuousMode = false - synthesisFromPlayer = false - isSynthesisInProgress = false - - // 设置单次播放模式 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NORMAL) - - // 启用音频数据回调(如果需要) - configAudioCallback() - - // 预备要合成的文本 - currentText = text - - // 单次播放模式:直接启动引擎并设置文本,自动开始合成 - return startEngineWithText(text, false) + override fun speakOnce(text: String): Boolean { + return false } /** * 将文本加入播放队列,支持连续播放多段文本 */ - fun speakStream(text: String): Boolean { + override fun speakStream(text: String): Boolean { if (text.isEmpty()) { - return false - } - - // 设置连续播放模式 - if (!isContinuousMode) { - isContinuousMode = true - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) - - // 启用音频数据回调(如果需要) - configAudioCallback() - } - - // 加入队列 - pendingTexts.add(text) - - // 如果当前引擎未启动,启动引擎 - if (!isEngineStarted) { - return startEngine() - } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { - // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 - return triggerNextSynthesis() - } - - return true - } - - /** - * 配置音频数据回调 - */ - private fun configAudioCallback() { - // 如果有音频数据监听器或明确要求启用回调,则设置回调模式 - if (enableAudioCallback) { - // 启用音频数据回调,使用流式模式(2) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 2) - } else { - // 禁用音频数据回调 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 0) - } - } - - /** - * 启动引擎,但不自动开始合成(适用于连续模式) - */ - private fun startEngine(): Boolean { - if (!isInitialized) { - return false + return true } try { - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") + if (!isInitialized) { + Log.e(TAG, "引擎未初始化") + return false + } - // 启动引擎 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") + // 处理文本,移除特殊字符 + val processedText = text.replace(Regex("[#*]"), " ") - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - handleError(ret ?: -1, "启动引擎失败") - isProcessingQueue = false - return false + // 添加新文本到缓冲区 + streamBuffer.append(text) + + // 增加防抖逻辑 + val currentTime = System.currentTimeMillis() + if (currentTime - lastSpeakTime < 600) { + return true } + lastSpeakTime = currentTime - isEngineStarted = true - retryCount = MAX_RETRY_COUNT + val currentText = streamBuffer.toString() + if (currentText.isEmpty()) { + return true + } + + // 定义标点符号列表 + val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') + + // 查找最后一个标点符号的位置 + var lastPunctuationIndex = -1 + for (i in currentText.indices.reversed()) { + if (currentText[i] in punctuationMarks) { + lastPunctuationIndex = i + break + } + } + + // 如果找到标点符号,则播放到该标点符号 + if (lastPunctuationIndex >= 0) { + // 提取要播放的文本(包含标点符号) + val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) + + // 剩余的文本保存在缓冲区中 + streamBuffer.delete(0, lastPunctuationIndex + 1) + + // 只有实际有文本时才处理 + if (textToSpeak.isNotEmpty()) { + // 将文本添加到队列 + Log.d(TAG, "textToSpeak: ${textToSpeak}") + pendingTexts.add(textToSpeak) + + // 根据引擎状态决定下一步操作 + if (!isEngineStarted) { + // 引擎未启动,启动它 + startEngine() + } else if (!isSynthesisInProgress && !synthesisFromPlayer) { + // 引擎已启动但当前空闲,触发下一个合成 + triggerNextSynthesis() + } + // 其他情况下引擎正忙,会在当前任务完成后处理队列 + } + } + // 如果没找到标点符号,保留全部文本在缓冲区,等待更多文本 return true } catch (e: Exception) { - handleError(-1, "启动引擎异常: ${e.message}", e) - isProcessingQueue = false + handleError(-1, "流式语音合成失败: ${e.message}", e) return false } } /** - * 启动引擎并设置文本(适用于单次模式) + * 启动语音引擎 */ - private fun startEngineWithText(text: String, isStream: Boolean): Boolean { + private fun startEngine(): Boolean { if (!isInitialized) { return false } try { - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") + // 设置连续播放场景模式 + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) - // 设置要合成的文本 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, text) + // 启用音频数据回调(如果需要) + if (enableAudioCallback) { + engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 2) + } else { + engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 0) + } - // 启动引擎开始合成 + // 发送启动引擎指令 val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - handleError(ret ?: -1, "启动合成失败") - isProcessingQueue = false + handleError(ret ?: -1, "启动引擎失败") return false } @@ -401,59 +366,13 @@ class VolcanoTtsHelper(private val context: Context) { return true } catch (e: Exception) { - handleError(-1, "合成异常: ${e.message}", e) - isProcessingQueue = false - return false - } - } - - /** - * 播放队列中剩余文本并标记流式输入结束 - */ - fun flushStream(): Boolean { - if (pendingTexts.isEmpty() && !isEngineStarted) { - isProcessingQueue = false - return true - } - - // 如果引擎未启动,则启动引擎 - if (!isEngineStarted) { - val result = startEngine() - if (result && !pendingTexts.isEmpty()) { - // 引擎启动成功后,在引擎启动回调中会自动触发第一句合成 - } - return result - } - - // 如果队列为空且当前没有播放,则关闭引擎 - if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) { - closeEngine() - } - - return true - } - - /** - * 关闭引擎 - */ - private fun closeEngine(): Boolean { - if (!isEngineStarted) { - return true - } - - try { - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isEngineStarted = false - isSynthesisInProgress = false - return ret == SpeechEngineDefines.ERR_NO_ERROR - } catch (e: Exception) { - FileLogger.e(TAG, "关闭引擎异常", e) + handleError(-1, "启动引擎异常: ${e.message}", e) return false } } /** - * 在回调中触发下一句合成 + * 触发下一句合成 */ private fun triggerNextSynthesis(): Boolean { if (!isEngineStarted || pendingTexts.isEmpty() || isSynthesisInProgress) { @@ -464,13 +383,15 @@ class VolcanoTtsHelper(private val context: Context) { currentText = nextText try { + Log.d(TAG, "开始合成: ${nextText}") // 设置要合成的文本 engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, nextText) // 标记合成开始 isSynthesisInProgress = true + pendingPlayCount++ - // 发送合成指令 + // 发送合成指令,不再使用请求ID val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { @@ -480,9 +401,11 @@ class VolcanoTtsHelper(private val context: Context) { pendingTexts.add(nextText) synthesisFromPlayer = true isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 return true } else { isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 handleError(ret ?: -1, "合成失败") return false } @@ -491,42 +414,87 @@ class VolcanoTtsHelper(private val context: Context) { return true } catch (e: Exception) { isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 handleError(-1, "触发合成异常: ${e.message}", e) return false } } - + + /** + * 播放队列中剩余文本并标记流式输入结束 + */ + override fun flushStream(): Boolean { + if (!isInitialized) { + return false + } + + try { + // 获取缓冲区中剩余的文本 + val remainingText = streamBuffer.toString() + + // 清空缓冲区 + streamBuffer.delete(0, streamBuffer.length) + + // 如果缓冲区有未播放的文本,直接加入队列 + if (remainingText.isNotEmpty()) { + pendingTexts.add(remainingText) + } + + // 如果队列为空且引擎未启动,直接返回 + if (pendingTexts.isEmpty() && !isEngineStarted) { + return true + } + + // 如果引擎未启动但队列有内容,则启动引擎 + if (!isEngineStarted && !pendingTexts.isEmpty()) { + return startEngine() + } + + // 如果引擎已启动但没有正在进行的合成,触发下一个合成 + if (isEngineStarted && !isSynthesisInProgress && !synthesisFromPlayer && !pendingTexts.isEmpty()) { + return triggerNextSynthesis() + } + + return true + } catch (e: Exception) { + handleError(-1, "刷新流式文本失败: ${e.message}", e) + return false + } + } + /** * 停止播放 */ - fun stop(): Boolean { + override fun stop(): Boolean { if (!isInitialized) { return false } // 已经不在播放,无需操作 - if (!isPlaying && !isEngineStarted) { + if (!isEngineStarted) { return true } try { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isPlaying = false isEngineStarted = false isSynthesisInProgress = false - // 清空队列 + // 清空队列和状态 pendingTexts.clear() isProcessingQueue = false - isContinuousMode = false synthesisFromPlayer = false + pendingPlayCount = 0 + + // 清空流缓冲区 + streamBuffer.delete(0, streamBuffer.length) // 发送取消事件 - dispatchEvent(TtsEvent(TtsEventType.CANCELED)) + dispatchEvent(TtsEventType.SYNTHESIS_CANCELED) return ret == SpeechEngineDefines.ERR_NO_ERROR } catch (e: Exception) { - FileLogger.e(TAG, "停止播放异常", e) + Log.e(TAG, "停止播放异常", e) return false } } @@ -534,7 +502,7 @@ class VolcanoTtsHelper(private val context: Context) { /** * 释放资源 */ - fun dispose() { + override fun release() { if (!isInitialized) { return } @@ -549,10 +517,11 @@ class VolcanoTtsHelper(private val context: Context) { engine = null isInitialized = false pendingTexts.clear() - listeners.clear() + streamBuffer.delete(0, streamBuffer.length) + eventListeners.clear() audioDataListeners.clear() } catch (e: Exception) { - FileLogger.e(TAG, "释放引擎异常", e) + Log.e(TAG, "释放引擎异常", e) } } @@ -566,17 +535,17 @@ class VolcanoTtsHelper(private val context: Context) { currentReqId = stringData isEngineStarted = true - // 如果是连续模式且队列不为空,在引擎启动后立即触发第一句合成 - if (isContinuousMode && !pendingTexts.isEmpty() && !isSynthesisInProgress) { + // 如果队列不为空,在引擎启动后立即触发第一句合成 + if (!pendingTexts.isEmpty() && !isSynthesisInProgress) { triggerNextSynthesis() } } SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { // 引擎停止 - isPlaying = false isEngineStarted = false isSynthesisInProgress = false + pendingPlayCount = 0 } SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> { @@ -591,8 +560,8 @@ class VolcanoTtsHelper(private val context: Context) { // 重置重试计数 retryCount = MAX_RETRY_COUNT - // 如果是连续模式且不是从播放器触发的合成,尝试处理下一个文本 - if (isContinuousMode && !synthesisFromPlayer && !pendingTexts.isEmpty()) { + // 如果不是从播放器触发的合成,尝试处理下一个文本 + if (!synthesisFromPlayer && !pendingTexts.isEmpty()) { // 在合成结束回调中触发下一句合成 triggerNextSynthesis() } @@ -600,28 +569,33 @@ class VolcanoTtsHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> { // 开始播放 - isPlaying = true - dispatchEvent(TtsEvent(TtsEventType.STARTED)) + // Log.d(TAG, "开始播放MESSAGE_TYPE_TTS_START") + dispatchEvent(TtsEventType.SYNTHESIS_STARTED) } SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> { // 播放结束 - isPlaying = false + // Log.d(TAG, "结束播放MESSAGE_TYPE_TTS_FINISH, pendingTexts.size: ${pendingTexts.size}, pendingPlayCount: ${pendingPlayCount}") + + // 减少待播放计数 + if (pendingPlayCount > 0) { + pendingPlayCount-- + } // 如果是从播放器触发的合成,现在可以继续处理队列了 if (synthesisFromPlayer) { synthesisFromPlayer = false // 播放器空闲,尝试触发下一句合成 triggerNextSynthesis() - } else if (pendingTexts.isEmpty()) { - // 如果队列为空,发送播放完成事件并关闭引擎 - dispatchEvent(TtsEvent(TtsEventType.COMPLETED)) + } else if (pendingTexts.isEmpty() && pendingPlayCount == 0) { + // 如果队列为空,发送播放完成事件 + // Log.d(TAG, "所有文本播放完成") + dispatchEvent(TtsEventType.SYNTHESIS_COMPLETED) isProcessingQueue = false - // 如果不是连续模式,关闭引擎 - if (!isContinuousMode) { - closeEngine() - } + // 如果没有待播放内容,关闭引擎 + engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") + isEngineStarted = false } else if (!isSynthesisInProgress) { // 如果队列不为空且当前没有正在进行的合成,触发下一句合成 triggerNextSynthesis() @@ -642,13 +616,21 @@ class VolcanoTtsHelper(private val context: Context) { val errCode = json.optInt("err_code", -1) val errMsg = json.optString("err_msg", "未知错误") - FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") + Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") // 标记当前合成结束 isSynthesisInProgress = false // 通知错误 - dispatchEvent(TtsEvent(TtsEventType.ERROR, errCode, errMsg)) + // dispatchEvent(TtsEventType.ERROR, mapOf( + // "errorCode" to errCode.toString(), + // "errorMessage" to errMsg + // )) + + // 检查是否需要更新计数 + if (pendingPlayCount > 0) { + pendingPlayCount-- + } // 尝试恢复或停止 when (errCode) { @@ -662,11 +644,13 @@ class VolcanoTtsHelper(private val context: Context) { SpeechEngineDefines.CODE_TTS_SYNTHESIS_ERROR, SpeechEngineDefines.CODE_TTS_SYNTHESIS_WAITING_TIMEOUT, SpeechEngineDefines.CODE_TTS_ERROR_UNKNOWN -> { - if (isContinuousMode && !pendingTexts.isEmpty()) { + if (!pendingTexts.isEmpty()) { // 轻微错误,直接尝试下一句 + // Log.d(TAG, "轻微错误,直接尝试下一句, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") triggerNextSynthesis() } else { - stop() + + // stop() } } @@ -679,15 +663,10 @@ class VolcanoTtsHelper(private val context: Context) { // 1秒后重试当前文本 mainHandler.postDelayed({ if (isEngineStarted) { - if (isContinuousMode) { - // 设置当前文本并触发合成 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, currentText) - isSynthesisInProgress = true - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") - } else { - // 非连续模式,重新启动引擎 - startEngineWithText(currentText, false) - } + // 设置当前文本并触发合成 + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, currentText) + isSynthesisInProgress = true + engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") } }, 1000) } else { @@ -697,7 +676,8 @@ class VolcanoTtsHelper(private val context: Context) { // 其他错误,停止引擎 else -> { - stop() + Log.d(TAG, "其他错误,停止合成") + // stop() } } } catch (e: Exception) { diff --git a/pubspec.yaml b/pubspec.yaml index 2875aaed1..7a8bbb5c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: logger: ^2.0.2+1 flutter_blue_plus: ^1.31.16 just_audio: ^0.9.36 - webview_flutter: ^4.10.0 + webview_flutter: ^4.12.0 flutter_easyloading: ^3.0.5 package_info_plus: ^8.3.0 file_picker: ^10.1.5 @@ -69,22 +69,16 @@ dependencies: path: local_plugins/azure_speech open_ai_service: path: local_plugins/open_ai_service - jieli: - path: local_plugins/jieli chat_storage: path: local_plugins/chat_storage spotify_sdk: path: local_plugins/spotify_sdk - amap_service: - path: local_plugins/amap_service jl_opus: path: local_plugins/jl_opus jl_ota: path: local_plugins/jl_ota ble_service: path: local_plugins/ble_service - deepvoice_utils: - path: local_plugins/deepvoice_utils classic_bluetooth: path: local_plugins/classic_bluetooth agent_service: diff --git a/test.json b/test.json index 8b7c60bb7..178eae9f1 100644 --- a/test.json +++ b/test.json @@ -15,4 +15,21 @@ adb shell am broadcast \ adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver - adb logcat -v time | grep -i BleForegroundService \ No newline at end of file + adb logcat -v time | grep -i BleForegroundService + + + + + mvn install:install-file \ + -Dfile= + -DgroupId=com.jieli.audio \ + -DartifactId=jl_audio_decode \ + -Dversion=2.0.0 \ + -Dpackaging=aar + + mvn install:install-file \ + -Dfile=/Users/wolfplus/Developer/Doc/Jieli_OTA_SDK_Android_1.9.2/libs/jl_bt_ota_V1.9.2-release.aar \ + -DgroupId=com.jieli.jl_bt_ota \ + -DartifactId=jl_bt_ota \ + -Dversion=1.9.2 \ + -Dpackaging=aar \ No newline at end of file diff --git a/test.md b/test.md new file mode 100644 index 000000000..91ff40e92 --- /dev/null +++ b/test.md @@ -0,0 +1,45 @@ + +请帮我生成一个基于 GetX 最佳实践的 Flutter 应用项目骨架。要求如下: + +1. 目录结构 + - lib/ + - main.dart + - app/ + - data/ + - providers/ # 网络请求、API 客户端(使用 GetConnect) + - repositories/ # Repository 层,封装业务数据源 + - services/ # 全局服务(如 ThemeService、AuthService) + - modules/ + - home/ # 示例模块 + - bindings/ # HomeBinding,注册 Controller、Service + - controllers/ # HomeController + - models/ # HomeModel 数据类 + - views/ # HomePage 页面 + - routes/ + - app_routes.dart # 路由常量 + - app_pages.dart # GetPage 列表,包括中间件示例 + - translations/ # 多语言文件(en_US、zh_CN)和 Translations 实现 + - widgets/ # 公共 Widget(Loading、ErrorView) + - utils/ # 日志 Logger、网络拦截器等工具函数 + +2. main.dart + - 使用 GetMaterialApp + - 配置 initialRoute、getPages、theme、darkTheme、locale、translations + +3. 依赖注入(Bindings) + - 在 app/modules/**/bindings 中,用 Get.lazyPut、Get.putAsync、Get.create 分别示例单例、异步初始化和工厂模式 + +4. 状态管理 + - 在 Controller 中使用 Rx + .obs + - 在视图中用 Obx、GetX、GetBuilder + - 在 Controller 的 onInit、onReady、onClose 中写注释说明生命周期 + +5. 路由管理 + - 在 app/routes/app_routes.dart 定义路由名称常量 + - 在 app/routes/app_pages.dart 用 GetPage 配置路由、binding + +8. 架构模式 + - 强调 “Repository → Service → Controller → View” 分层 + - 各层代码注释职责 + +- 代码风格统一、注释清晰 \ No newline at end of file