From 9fcec5f32d56547f13b1743c2aa9be84a6c2e449 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 21 Jun 2025 10:22:21 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(=E9=9F=B3=E4=B9=90=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=88=97=E8=A1=A8):=20=E6=B7=BB=E5=8A=A0=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在AppBar中添加iOS风格的返回按钮以改善导航体验,同时移除冗余的标题显示 --- lib/modules/music/views/music_playlist_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 37a35fbf2..022566802 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -14,7 +14,11 @@ class MusicPlaylistView extends StatelessWidget { return Scaffold( appBar: AppBar( - title: const Text('音乐播放列表'), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 + onPressed: () => Navigator.pop(context), + ), + //title: const Text('音乐播放列表'), backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black, elevation: 0, From 78ac7e2dd42918ecfb0f9ded53b2e2728a4b599f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 21 Jun 2025 10:23:31 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(=E9=9F=B3=E4=B9=90):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=92=AD=E6=94=BE=E9=9F=B3=E4=B9=90=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 MusicManager 和 MusicService 中添加 playPlaylist 方法 - 在 AgentController 中实现播放列表逻辑,包括加载状态管理 - 修复音乐播放视图中的当前播放歌曲ID比较问题 --- lib/data/services/music_manager.dart | 5 ++ .../agent/controllers/agent_controller.dart | 80 ++++++++++++++++++- .../music/views/music_playlist_view.dart | 2 +- .../music_service/lib/music_service.dart | 5 ++ 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 90324c290..26e7aef91 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -80,4 +80,9 @@ class MusicManager extends GetxService { Future seekTo(int positionMs) async { await _musicService.seekTo(positionMs); } + + /// 播放音乐列表 + Future playPlaylist(List> playlist) async { + await _musicService.playMusicPlaylist(playlist); + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index add1f53b4..c6c58d704 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -85,6 +85,16 @@ class AgentController extends GetxController { // 是否已加载聊天记录 final isHistoryLoaded = false.obs; + // 添加加载状态跟踪 + final RxBool isLoadingMusic = false.obs; + final RxString loadingMusicId = ''.obs; + + // 设置加载状态的方法 + void setLoadingMusic(String id, bool loading) { + loadingMusicId.value = loading ? id : ''; + isLoadingMusic.value = loading; + } + @override void onInit() { super.onInit(); @@ -926,12 +936,14 @@ class AgentController extends GetxController { // 恢复音乐播放 Future musicResume() async { try { - Logger.d('AgentController', '恢复音乐播放'); + Logger.d('AgentController', '恢复播放音乐'); await _musiceManager.resume(); isPlaying.value = true; _startProgressTimer(); + Logger.d('AgentController', '音乐恢复播放成功'); } catch (e) { - Logger.e('AgentController', '恢复音乐失败: $e'); + Logger.e('AgentController', '恢复播放音乐失败: $e'); + isPlaying.value = false; } } @@ -1037,4 +1049,68 @@ class AgentController extends GetxController { ), ); } + + /// 播放音乐列表 + Future playPlaylist(List> playlist) async { + try { + if (playlist.isEmpty) { + Logger.e(TAG, '播放列表为空,无法播放'); + return; + } + + Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); + + // 设置加载状态 + isLoadingMusic.value = true; + loadingMusicId.value = playlist[0]['id']?.toString() ?? ''; + + // 重置播放状态 + isPlaying.value = true; + currentPosition.value = 0; + duration.value = 0; + progress.value = 0.0; + + // 格式化歌曲列表数据 + final formattedPlaylist = playlist.map((song) { + return { + 'id': song['id']?.toString() ?? '', + 'url': song['url']?.toString() ?? '', + 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', + 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', + 'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', + }; + }).toList(); + + // 调用音乐管理器播放列表 + await _musiceManager.playPlaylist(formattedPlaylist); + + // 更新当前播放歌曲ID + if (formattedPlaylist.isNotEmpty) { + currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0; + } + + // 启动进度定时器 + _startProgressTimer(); + + Logger.d(TAG, '音乐列表播放启动成功'); + } catch (e) { + Logger.e(TAG, '播放音乐列表失败: $e'); + isPlaying.value = false; + currentId.value = 0; + + // 显示错误提示 + Get.snackbar( + '播放失败', + '无法播放音乐列表,请稍后重试', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } finally { + // 清除加载状态 + isLoadingMusic.value = false; + loadingMusicId.value = ''; + } + } } diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 022566802..a66d228a3 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -166,7 +166,7 @@ class MusicPlaylistView extends StatelessWidget { // 播放按钮 Obx(() { final isThisPlaying = - controller.isPlaying.value && controller.currentId.value == url; + controller.isPlaying.value && controller.currentId.value == id; // 只有当前播放的歌曲才显示进度 final currentProgress = diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index da5516b43..ac091afb0 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -74,4 +74,9 @@ class MusicService { Future seekTo(int positionMs) async { await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); } + + /// 播放音乐列表 + Future playMusicPlaylist(List> songs) async { + await _methodChannel.invokeMethod('playlist', {'songs': songs}); + } } From de40cc1dbf079181f5eac62f9ffc7b7982d7b318 Mon Sep 17 00:00:00 2001 From: wolfplus2048 <30993207+wolfplus2048@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:26:43 +0100 Subject: [PATCH 3/5] add --- .../bytedance_speech/BytedanceAudioPlayer.kt | 110 +++++++++++++++++- .../bytedance_speech/BytedanceTTS.kt | 2 +- 2 files changed, 106 insertions(+), 6 deletions(-) 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 index a761c015c..f1c0ec79a 100644 --- 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 @@ -15,12 +15,13 @@ import com.deep_voice.speech.tts.AudioDataListener import com.deep_voice.speech.tts.AudioOutputDevice /** - * 高性能音频播放器 - 16kHz采样率 + * 语音通信音频播放器 - 16kHz采样率 * 设计要点: - * • 永远使用 USAGE_MEDIA + MODE_NORMAL 媒体播放通路 + * • 使用 USAGE_VOICE_COMMUNICATION + MODE_IN_COMMUNICATION 通信通路 * • 利用 AudioDeviceCallback 监听设备热插拔 * • API 31+ 使用 setPreferredDevice() 精准路由 * • 有耳机自动从耳机播放,无耳机自动切到扬声器 + * • 内置回声消除和噪音抑制功能 */ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() { companion object { @@ -35,6 +36,35 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au private var isFirstData = true private var totalBytesWritten = 0 private var sessionActive = true + private var audioFocusRequest: Any? = null // 兼容不同API版本 + + // 音频焦点变化监听器 + private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + // 永久失去焦点,停止播放 + Log.d(TAG, "音频焦点丢失") + stop() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // 暂时失去焦点,暂停播放 + Log.d(TAG, "音频焦点暂时丢失") + audioTrack?.pause() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // 暂时失去焦点但可降低音量,这里选择暂停 + Log.d(TAG, "音频焦点暂时丢失(可降低音量)") + audioTrack?.pause() + } + AudioManager.AUDIOFOCUS_GAIN -> { + // 重新获得焦点,恢复播放 + Log.d(TAG, "音频焦点恢复") + if (sessionActive) { + audioTrack?.play() + } + } + } + } // 回调 private var onPlayStarted: (() -> Unit)? = null @@ -62,6 +92,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au init { // 注册设备热插拔监听 audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper())) + // 请求音频焦点 + requestAudioFocus() buildTrack(selectOutputDevice()) // 首次创建 } @@ -126,7 +158,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au audioTrack = AudioTrack.Builder() .setAudioAttributes( AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) // 统一媒体通路 + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 语音通信通路 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() ) @@ -159,8 +191,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au track.play() } - // 配置音频管理器 - 统一使用 MODE_NORMAL - audioManager.mode = AudioManager.MODE_NORMAL + // 配置音频管理器 - 使用通信模式以启用回声消除和噪音抑制 + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION // 切换扬声器开关仅在无耳机时打开 audioManager.isSpeakerphoneOn = (device == null) @@ -168,6 +200,71 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz") } + /** + * 请求音频焦点 + */ + private fun requestAudioFocus() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android O及以上使用新的API + val focusRequest = android.media.AudioFocusRequest.Builder( + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + + val result = audioManager.requestAudioFocus(focusRequest) + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + audioFocusRequest = focusRequest + Log.d(TAG, "音频焦点请求成功") + } else { + Log.w(TAG, "音频焦点请求失败") + } + } else { + // Android O以下使用旧的API + @Suppress("DEPRECATION") + val result = audioManager.requestAudioFocus( + audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "音频焦点请求成功(旧API)") + } else { + Log.w(TAG, "音频焦点请求失败(旧API)") + } + } + } catch (e: Exception) { + Log.e(TAG, "请求音频焦点异常: ${e.message}") + } + } + + /** + * 释放音频焦点 + */ + private fun releaseAudioFocus() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audioFocusRequest?.let { + audioManager.abandonAudioFocusRequest(it as android.media.AudioFocusRequest) + } + } else { + @Suppress("DEPRECATION") + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + Log.d(TAG, "音频焦点已释放") + } catch (e: Exception) { + Log.e(TAG, "释放音频焦点异常: ${e.message}") + } + } + /** * 开始新的播放会话 */ @@ -262,7 +359,10 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au stop() audioTrack?.release() audioTrack = null + releaseAudioFocus() audioManager.unregisterAudioDeviceCallback(this) + // 恢复音频模式 + audioManager.mode = AudioManager.MODE_NORMAL } /** 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 bb446a994..9db3c0e93 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 @@ -116,7 +116,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { // TTS配置参数 private var appId: String = "" private var accessToken: String = "" - private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 + private var speaker: String = "zh_female_yuanqinvyou_moon_bigtts" // 默认语音,使用Java示例的音色 private var format: String = "pcm" // pcm 格式 private var sampleRate: Int = 16000 // 使用16kHz采样率 private var enableTimestamp: Boolean = false From f26f3e6e487676762062ca042b435642a63414e3 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 23 Jun 2025 20:33:23 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BC=98=E5=8C=96ui=E7=AD=89=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/meeting_controller.dart | 31 +++--- .../meeting_record_controller.dart | 17 +++- .../controllers/translation_controller.dart | 62 ++++++++---- .../translation/views/translation_view.dart | 95 +++++++++++++------ 4 files changed, 140 insertions(+), 65 deletions(-) diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 0058b31d8..fc7f46938 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -182,27 +182,34 @@ class MeetingController extends GetxController { void addMeetingAudio() async { EasyLoading.show(status: '正在导入本地录音...'); try { + int addUp = 0; final appDir = await appDocDir; - final sourceDir = Directory("${appDir.path}/MeetingAudio/"); - // 检查源目录是否存在 - if (!await sourceDir.exists()) { - EasyLoading.dismiss(); - return; - } +// 定义源目录列表 + final sourceDirs = [ + Directory("${appDir.path}/MeetingAudio/"), + Directory("${appDir.path}/DestAudio/"), + ]; - // 创建目标目录(如果不存在) +// 创建目标目录(如果不存在) final destDir = Directory("${appDir.path}/LocalAudio/"); if (!await destDir.exists()) { await destDir.create(recursive: true); } - // 获取目录下所有文件 - final files = - await sourceDir.list().where((entity) => entity is File).toList(); + List allFiles = []; - int addUp = 0; - for (final fileEntity in files) { + for (final dir in sourceDirs) { + if (await dir.exists()) { + final files = + await dir.list().where((entity) => entity is File).toList(); + allFiles.addAll(files); + } + } + +// 此时 allFiles 包含 MeetingAudio 和 DestAudio 目录下所有的文件 + + for (final fileEntity in allFiles) { try { final file = File(fileEntity.path); final filePath = file.path; diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index ee4640b18..f9f66fc7e 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -335,10 +335,19 @@ class MeetingRecordController extends GetxController final appDir = await appDocDir; final dir = Directory("${appDir.path}/MeetingAudio/"); if (!await dir.exists()) await dir.create(recursive: true); - await _asrService.renameFile( - "${dir.path}/${fileName.value}.wav", - newName.value, - ); + if (isMarked.value) { + final Newdir = Directory("${appDir.path}/DestAudio/"); + if (!await Newdir.exists()) await Newdir.create(recursive: true); + await _asrService.moveFile( + "${dir.path}/${fileName.value}.wav", + "${Newdir.path}/${fileName.value}.wav", + ); + } else { + await _asrService.renameFile( + "${dir.path}/${fileName.value}.wav", + newName.value, + ); + } } await _asrService.stopContinuousRecognition(); diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index dd307a50f..0fb6d465d 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -36,6 +36,11 @@ class TranslationController extends GetxController { final bleManager = Get.find(); // 音频输入源 bool _audioSourceType = false; + // 倒计时器 + + final RxString recordDurationText = '00:00'.obs; + Timer? _recordTimer; + int _seconds = 0; // 存储相关 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; @@ -198,7 +203,7 @@ class TranslationController extends GetxController { return true; } else { Logger.d("Permission", "用户未授予存储权限"); - return false; + return true; } } catch (e) { Logger.e("Permission", "请求存储权限时出错: $e"); @@ -267,16 +272,45 @@ class TranslationController extends GetxController { Future toggleisRecord() async { isRecording.toggle(); if (isRecording.value && isRecognizing.value) { - // 使用可读性更好的时间格式 - final formattedTime = - DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - await _asrService.enableRecord( - "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + startRecording(); } else if (!isRecording.value) { - await _asrService.stopRecord(true); + stopRecording(); } else {} } + Future startRecording() async { + final bool storage = await _requestStoragePermission(); + if (!storage) { + Logger.d("Permission", "未授予存储权限,无法提供录音功能"); + return; + } + //isRecording.value = true; + _seconds = 0; + recordDurationText.value = '00:00'; + _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { + _seconds++; + final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (_seconds % 60).toString().padLeft(2, '0'); + recordDurationText.value = '$minutes:$seconds'; + }); + + // 启动实际录音逻辑 + // ... + final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); + await _asrService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + } + + Future stopRecording() async { + _recordTimer?.cancel(); + _recordTimer = null; + isRecording.value = false; + recordDurationText.value = '00:00'; + await _asrService.stopRecord(true); + // 停止实际录音逻辑 + // ... + } + // 开始语音识别 Future startRecognition() async { Logger.info('开始语音识别'); @@ -343,15 +377,7 @@ class TranslationController extends GetxController { await _asrService.startContinuousRecognition(_audioSourceType); isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { - final bool storage = await _requestStoragePermission(); - if (!storage) { - Logger.d("Permission", "未授予存储权限,无法提供录音功能"); - return; - } - final formattedTime = - DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - await _asrService.enableRecord( - "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); + startRecording(); } // 监听识别结果 _recognitionSubscription?.cancel(); @@ -411,7 +437,7 @@ class TranslationController extends GetxController { saveTranslationHistory(); } - + stopRecording(); currentSessionId = null; } catch (e) { Logger.error('停止语音识别失败: ${e.toString()}'); @@ -607,7 +633,7 @@ class TranslationController extends GetxController { if (isRecognizing.value) { await stopRecognition(); } - await _asrService.stopRecord(true); + stopRecording(); _translationDebounceTimer?.cancel(); await _ttsService.stop(); } catch (e) { diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 3a9df2239..221bab5a7 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -107,37 +107,70 @@ class TranslationView extends GetView { )), centerTitle: true, actions: [ - // TTS toggle button - Obx(() => IconButton( - icon: Icon( - controller.isRecording.value - ? Icons.mic - : Icons.fiber_manual_record, - color: isDarkMode ? Colors.white : Colors.black, - size: 24.sp, + Obx(() { + if (controller.isRecording.value) { + // 录音中:显示红色计时器样式,点击可停止录音 + return Padding( + padding: EdgeInsets.only(right: 16.w), + child: GestureDetector( + onTap: controller.toggleisRecord, // 点击结束录音 + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(20.r), + ), + child: Row( + children: [ + Icon( + Icons.fiber_manual_record, + color: Colors.white, + size: 18.sp, + ), + SizedBox(width: 4.w), + Text( + controller.recordDurationText.value, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), ), - // onPressed: controller.toggleTts,默认播放 - onPressed: controller.toggleisRecord, - tooltip: '是否开启录音', - )), - - // 历史记录按钮 - IconButton( - icon: Icon( - Icons.folder, - color: isDarkMode ? Colors.white : Colors.black, - size: 24.sp, - ), - onPressed: () async { - // 停止语音识别和TTS,释放资源 - await controller.stopAll(); - Get.toNamed(Routes.translationList); - - // 返回时不自动开始翻译,只需重新设置滚动监听器 - controller.setupScrollListener(); - }, - tooltip: '查看录音文件', - ), + ); + } else { + // 未录音:显示红色录音按钮 + 文件夹按钮 + return Row( + children: [ + IconButton( + icon: Icon( + Icons.radio_button_checked, + color: Colors.red, + size: 24.sp, + ), + onPressed: controller.toggleisRecord, + tooltip: '是否开启录音', + ), + IconButton( + icon: Icon( + Icons.folder, + color: isDarkMode ? Colors.white : Colors.black, + size: 24.sp, + ), + onPressed: () async { + await controller.stopAll(); + Get.toNamed(Routes.translationList); + controller.setupScrollListener(); + }, + tooltip: '查看录音文件', + ), + ], + ); + } + }), ], bottom: PreferredSize( preferredSize: Size.fromHeight(50.h), @@ -328,7 +361,7 @@ class TranslationView extends GetView { onTap: () => controller.playTranslationItem(item), child: Icon( Icons.volume_up, - size: 18, + size: 20, color: Theme.of(context).colorScheme.primary, ), ), From 5a25a60b43ca806e03af8cb9f28ac56571ec4480 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 24 Jun 2025 11:25:55 +0800 Subject: [PATCH 5/5] add --- lib/data/services/ble_manager.dart | 27 ++- .../speech_impl/volcano_tts_service.dart | 146 ++++++------ .../controllers/ota_upgrade_controller.dart | 4 +- lib/modules/settings/views/settings_view.dart | 210 +++++++++--------- .../main/kotlin/com/example/ota/OtaPlugin.kt | 23 +- 5 files changed, 221 insertions(+), 189 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index acf36ac6b..f379b53d8 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -184,6 +184,19 @@ class BleManager extends GetxService { state == BleConnectionState.error) { Logger.info('BLE设备已断开或发生错误'); _isConnected.value = false; + // 更新设备信息Add commentMore actions + _updateDeviceInfoIfNeeded( + batteryLevel: 0, + leftEarBattery: 0, + rightEarBattery: 0, + caseBattery: 0, + leftEarCharging: false, + rightEarCharging: false, + caseCharging: false, + firmwareVersion: '', + firmwareProductId: '', + firmwareProductColor: '', + ); // 断开连接时不清除设备信息,保留最后的电量信息 // 只有在需要完全清除设备时才重置信息 } @@ -405,7 +418,7 @@ class BleManager extends GetxService { // 使用保存的设备信息创建新的DeviceInfo对象 String deviceAddress = _currentDeviceAddress ?? ""; String deviceName = _currentDeviceName ?? ""; - + Logger.info('创建新的设备信息对象: address=$deviceAddress, name=$deviceName'); deviceInfo.value = DeviceInfo( address: deviceAddress, @@ -442,7 +455,7 @@ class BleManager extends GetxService { // 创建初始设备信息对象 String deviceAddress = _currentDeviceAddress ?? ""; String deviceName = _currentDeviceName ?? ""; - + Logger.info('初始化设备信息对象: address=$deviceAddress, name=$deviceName'); deviceInfo.value = DeviceInfo( address: deviceAddress, @@ -467,8 +480,9 @@ class BleManager extends GetxService { _bleService.getProductId(), ]); - Logger.info('设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); - + Logger.info( + '设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); + if (!results[0]) { Logger.warning('电池信息获取指令发送失败'); } @@ -788,9 +802,10 @@ class BleManager extends GetxService { // 从蓝牙管理器获取设备名称 final bluetoothManager = Get.find(); _currentDeviceName = bluetoothManager.currentDevice?.name ?? ""; - Logger.info('保存设备信息: address=$_currentDeviceAddress, name=$_currentDeviceName'); + Logger.info( + '保存设备信息: address=$_currentDeviceAddress, name=$_currentDeviceName'); } - + return await _bleService.registerBackgroundScan(macAddress: macAddress); } catch (e) { Logger.error('注册后台扫描失败: ${e.toString()}'); diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index 402aac5b8..f84ed6b0a 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_service.dart @@ -9,82 +9,84 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class VolcanoTtsService extends GetxService implements TtsService { /// 方法通道 static const MethodChannel _channel = MethodChannel('volcano_speech/tts'); - + /// 事件通道 - static const EventChannel _eventChannel = EventChannel('volcano_speech/tts_events'); - + static const EventChannel _eventChannel = + EventChannel('volcano_speech/tts_events'); + /// 音频数据通道 - static const EventChannel _audioDataChannel = EventChannel('volcano_speech/tts_audio_data'); - + static const EventChannel _audioDataChannel = + EventChannel('volcano_speech/tts_audio_data'); + /// 事件流控制器 - final StreamController _eventStreamController = + final StreamController _eventStreamController = StreamController.broadcast(); - + /// 音频数据流控制器 - final StreamController _audioDataStreamController = + final StreamController _audioDataStreamController = StreamController.broadcast(); - + /// 事件处理器 StreamSubscription? _eventSubscription; - + /// 音频数据处理器 StreamSubscription? _audioDataSubscription; - + /// 是否初始化完成 bool _isInitialized = false; - + /// 当前语音名称 String _currentVoice = "zh_female_qingxin"; - + /// 是否启用音频回调 bool _isAudioCallbackEnabled = false; - + @override String get currentVoice => _currentVoice; - + @override bool get isSpeaking => _isSpeaking; - + /// 是否正在播放 bool _isSpeaking = false; - + @override Stream get onEvent => _eventStreamController.stream; - + /// 音频数据流 Stream get onAudioData => _audioDataStreamController.stream; - + /// 构造函数 VolcanoTtsService() { _setupEventListeners(); _setupAudioDataListeners(); } - + /// 设置事件监听 void _setupEventListeners() { _eventSubscription = _eventChannel.receiveBroadcastStream().listen( (dynamic event) { if (event is! Map) return; - + final Map eventMap = event; final String? type = eventMap['type'] as String?; - + switch (type) { case 'started': _isSpeaking = true; _eventStreamController.add(TtsEvent(type: TtsEventType.started)); break; - + case 'completed': _isSpeaking = false; _eventStreamController.add(TtsEvent(type: TtsEventType.completed)); break; - + case 'canceled': _isSpeaking = false; _eventStreamController.add(TtsEvent(type: TtsEventType.canceled)); break; - + case 'error': _isSpeaking = false; final errorCode = eventMap['errorCode'] as String? ?? "-1"; @@ -105,7 +107,7 @@ class VolcanoTtsService extends GetxService implements TtsService { }, ); } - + /// 设置音频数据监听 void _setupAudioDataListeners() { _audioDataSubscription = _audioDataChannel.receiveBroadcastStream().listen( @@ -119,32 +121,34 @@ class VolcanoTtsService extends GetxService implements TtsService { }, ); } - + @override Future initialize({List? supportedLanguages}) async { if (_isInitialized) return true; - + try { // 从.env文件读取配置 final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; - + if (appId.isEmpty || appToken.isEmpty) { - debugPrint('火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量'); + debugPrint( + '火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量'); return false; } - + // 初始化火山语音TTS final result = await _channel.invokeMethod('initialize', { - 'appId': appId, - 'appToken': appToken, - }) ?? false; - + 'appId': appId, + 'appToken': appToken, + }) ?? + false; + if (!result) { debugPrint('火山语音TTS初始化失败'); return false; } - + _isInitialized = true; return true; } catch (e) { @@ -160,16 +164,17 @@ class VolcanoTtsService extends GetxService implements TtsService { } return true; } - + @override Future setVoice(String voiceName) async { if (!await _ensureInitialized()) return false; - + try { final result = await _channel.invokeMethod('setVoice', { - 'voice': voiceName, - }) ?? false; - + 'voice': voiceName, + }) ?? + false; + if (result) { _currentVoice = voiceName; } @@ -179,16 +184,17 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + /// 设置是否启用音频数据回调 Future setEnableAudioCallback(bool enable) async { if (!await _ensureInitialized()) return false; - + try { final result = await _channel.invokeMethod('enableAudioCallback', { - 'enable': enable, - }) ?? false; - + 'enable': enable, + }) ?? + false; + if (result) { _isAudioCallbackEnabled = enable; } @@ -198,17 +204,18 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + @override Future speakOnce(String text) async { if (!await _ensureInitialized()) return false; - + try { // 单次播放 final result = await _channel.invokeMethod('speakOnce', { - 'text': text, - }) ?? false; - + 'text': text, + }) ?? + false; + return result; } catch (e) { debugPrint('单次播放失败:$e'); @@ -216,17 +223,18 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + @override Future speakStream(String text) async { if (!await _ensureInitialized()) return false; - + try { // 流式添加文本 final result = await _channel.invokeMethod('speakStream', { - 'text': text, - }) ?? false; - + 'text': text, + }) ?? + false; + return result; } catch (e) { debugPrint('流式播放失败:$e'); @@ -234,11 +242,11 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + @override Future flushStream() async { if (!await _ensureInitialized()) return false; - + try { // 刷新流式文本 return await _channel.invokeMethod('flushStream') ?? false; @@ -247,11 +255,11 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + @override Future stop() async { if (!_isInitialized) return false; - + try { return await _channel.invokeMethod('stop') ?? false; } catch (e) { @@ -259,27 +267,33 @@ class VolcanoTtsService extends GetxService implements TtsService { return false; } } - + @override Future dispose() async { if (_isSpeaking) { await stop(); } - + await _eventSubscription?.cancel(); await _audioDataSubscription?.cancel(); - + if (!_eventStreamController.isClosed) { await _eventStreamController.close(); } - + if (!_audioDataStreamController.isClosed) { await _audioDataStreamController.close(); } - + if (_isInitialized) { await _channel.invokeMethod('release'); _isInitialized = false; } } -} \ No newline at end of file + + @override + Future setAudioOutputDevice(int type) { + // TODO: implement setAudioOutputDevice + throw UnimplementedError(); + } +} diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index c175e332d..d98324272 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -327,7 +327,7 @@ class OtaUpgradeController extends GetxController { } if (existingSameFile) { - Get.snackbar('文件已存在', '跳过下载,直接更新'); + //Get.snackbar('文件已存在', '跳过下载,直接更新'); startUpgrading(savePath); return; @@ -367,7 +367,7 @@ class OtaUpgradeController extends GetxController { ); File file = File(savePath); if (await file.exists()) { - Get.snackbar('下载完成', '文件已保存至: $savePath'); + // Get.snackbar('下载完成', '文件已保存至: $savePath'); } else { Get.snackbar('错误', '文件未找到,请检查写入权限'); } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 4cc3e7360..6c33ea86e 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -525,114 +525,114 @@ class SettingsView extends GetView { ? Colors.white.withOpacity(0.1) : Colors.grey[200]), - // 杰理测试 - _buildSimpleNavigationSetting( - title: '杰理蓝牙测试', - subtitle: '测试杰理蓝牙设备扫描、连接和功能', - icon: Icons.bluetooth, - iconBgColor: isDarkMode - ? Colors.blue[900]!.withOpacity(0.3) - : Colors.blue[100]!, - iconColor: - isDarkMode ? Colors.blue[300]! : Colors.blue[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.jieliTest); - }, - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), - // 面对面翻译测试 - _buildSimpleNavigationSetting( - title: '面对面翻译测试', - subtitle: '测试面对面翻译', - icon: Icons.bluetooth_searching, - iconBgColor: isDarkMode - ? Colors.green[900]!.withOpacity(0.3) - : Colors.green[100]!, - iconColor: - isDarkMode ? Colors.green[300]! : Colors.green[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.ftftranslation); - }, - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), - // BLE测试 - _buildSimpleNavigationSetting( - title: 'opus解码测试', - subtitle: '测试opus解码', - icon: Icons.bluetooth_searching, - iconBgColor: isDarkMode - ? Colors.green[900]!.withOpacity(0.3) - : Colors.green[100]!, - iconColor: - isDarkMode ? Colors.green[300]! : Colors.green[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.opusTest); - }, - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // // 杰理测试 + // _buildSimpleNavigationSetting( + // title: '杰理蓝牙测试', + // subtitle: '测试杰理蓝牙设备扫描、连接和功能', + // icon: Icons.bluetooth, + // iconBgColor: isDarkMode + // ? Colors.blue[900]!.withOpacity(0.3) + // : Colors.blue[100]!, + // iconColor: + // isDarkMode ? Colors.blue[300]! : Colors.blue[600]!, + // titleColor: isDarkMode ? Colors.white : null, + // subtitleColor: isDarkMode ? Colors.white70 : null, + // onTap: () { + // Get.toNamed(Routes.jieliTest); + // }, + // isDarkMode: isDarkMode, + // ), + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), + // // 面对面翻译测试 + // _buildSimpleNavigationSetting( + // title: '面对面翻译测试', + // subtitle: '测试面对面翻译', + // icon: Icons.bluetooth_searching, + // iconBgColor: isDarkMode + // ? Colors.green[900]!.withOpacity(0.3) + // : Colors.green[100]!, + // iconColor: + // isDarkMode ? Colors.green[300]! : Colors.green[600]!, + // titleColor: isDarkMode ? Colors.white : null, + // subtitleColor: isDarkMode ? Colors.white70 : null, + // onTap: () { + // Get.toNamed(Routes.ftftranslation); + // }, + // isDarkMode: isDarkMode, + // ), + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), + // // BLE测试 + // _buildSimpleNavigationSetting( + // title: 'opus解码测试', + // subtitle: '测试opus解码', + // icon: Icons.bluetooth_searching, + // iconBgColor: isDarkMode + // ? Colors.green[900]!.withOpacity(0.3) + // : Colors.green[100]!, + // iconColor: + // isDarkMode ? Colors.green[300]! : Colors.green[600]!, + // titleColor: isDarkMode ? Colors.white : null, + // subtitleColor: isDarkMode ? Colors.white70 : null, + // onTap: () { + // Get.toNamed(Routes.opusTest); + // }, + // isDarkMode: isDarkMode, + // ), + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), // BLE测试 - _buildSimpleNavigationSetting( - title: 'BLE蓝牙测试', - subtitle: '测试BLE蓝牙设备扫描、连接和功能', - icon: Icons.bluetooth_searching, - iconBgColor: isDarkMode - ? Colors.green[900]!.withOpacity(0.3) - : Colors.green[100]!, - iconColor: - isDarkMode ? Colors.green[300]! : Colors.green[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.bleTest); - }, - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // _buildSimpleNavigationSetting( + // title: 'BLE蓝牙测试', + // subtitle: '测试BLE蓝牙设备扫描、连接和功能', + // icon: Icons.bluetooth_searching, + // iconBgColor: isDarkMode + // ? Colors.green[900]!.withOpacity(0.3) + // : Colors.green[100]!, + // iconColor: + // isDarkMode ? Colors.green[300]! : Colors.green[600]!, + // titleColor: isDarkMode ? Colors.white : null, + // subtitleColor: isDarkMode ? Colors.white70 : null, + // onTap: () { + // Get.toNamed(Routes.bleTest); + // }, + // isDarkMode: isDarkMode, + // ), + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), - // 火山语音测试 - _buildSimpleNavigationSetting( - title: '语音测试', - subtitle: '测试语音识别和合成功能,支持多种服务实现', - icon: Icons.record_voice_over, - iconBgColor: isDarkMode - ? Colors.orange[900]!.withOpacity(0.3) - : Colors.orange[100]!, - iconColor: - isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, - onTap: () => Get.toNamed(Routes.speechTest), - isDarkMode: isDarkMode, - ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // // 火山语音测试 + // _buildSimpleNavigationSetting( + // title: '语音测试', + // subtitle: '测试语音识别和合成功能,支持多种服务实现', + // icon: Icons.record_voice_over, + // iconBgColor: isDarkMode + // ? Colors.orange[900]!.withOpacity(0.3) + // : Colors.orange[100]!, + // iconColor: + // isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + // onTap: () => Get.toNamed(Routes.speechTest), + // isDarkMode: isDarkMode, + // ), + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), // 测试ble耳机固件版本升级 _buildSimpleNavigationSetting( title: 'ota升级', diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index c2ddcfe1f..8f68906de 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -146,9 +146,10 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { private lateinit var channel: MethodChannel private lateinit var context: Context private lateinit var otaDevice: BluetoothDevice - private lateinit var bluetoothHelper: BluetoothHelper + private var bluetoothHelper: BluetoothHelper? = null + + private var otaManager: OTAManager? = null - private lateinit var otaManager: OTAManager private lateinit var filePath: String//升级文件路径 private var isDisconnect = false//是否主动断开 private var streamSink: EventChannel.EventSink? = null @@ -216,8 +217,8 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { private val bluetoothCallback = object : BtEventCallback() { override fun onConnection(device: BluetoothDevice?, status: Int) { - if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper.getConnectedDevice() != null) { - bluetoothHelper.disconnectDevice(bluetoothHelper.getConnectedDevice()) + if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper?.getConnectedDevice() != null) { + bluetoothHelper?.disconnectDevice(bluetoothHelper?.getConnectedDevice()) } otaDevice = device!! @@ -238,10 +239,10 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { ) ) Log.d("TAG", "startUpgrade,filePath=${filePath}") - otaManager.bluetoothOption.firmwareFilePath = filePath - otaManager.startOTA( + otaManager?.bluetoothOption?.firmwareFilePath = filePath + otaManager?.startOTA( CustomUpdateCallback( - otaManager.bluetoothHelper.getConnectedDevice(), streamSink + otaManager?.bluetoothHelper?.getConnectedDevice(), streamSink ) ) } @@ -280,7 +281,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { otaManager = OTAManager(context) bluetoothHelper = BluetoothHelper(context) // 如果需要额外的初始化逻辑,请在这里添加 - otaManager.registerBluetoothCallback(bluetoothCallback) + otaManager!!.registerBluetoothCallback(bluetoothCallback) // 初始化BleService // 2. 初始化BleService并注册回调 if (BleService.initialize(context)) { @@ -335,10 +336,12 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { } else if (call.method == "onOtaClose") { BleService.isEnterOta = false isDisconnect = false + if(otaManager!= null) { + otaManager?.bluetoothHelper?.stopScan() otaManager?.bluetoothHelper?.disconnectDevice(otaDevice) - + } result.success("") } else { result.notImplemented() @@ -370,7 +373,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { if (state == BleConst.STATE_DISCONNECTED && isDisconnect) { Log.d("TAG", "Ble设备已断开,onConnectionStateChanged") Handler(Looper.getMainLooper()).postDelayed({ - otaManager.bluetoothHelper.startScan(otaDevice) + otaManager?.bluetoothHelper?.startScan(otaDevice) }, 2000) // 2000 毫秒 = 2 秒 } }