Browse Source

Merge commit 'c0239721724d328ef0cee162b544a03b81f8d69f' into 重写ble

weicu
lxm 1 year ago
parent
commit
a43dc64b88
  1. 17
      android/app/build.gradle.kts
  2. 10
      android/app/src/main/AndroidManifest.xml
  3. 133
      android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt
  4. 376
      android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
  5. 245
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  6. 150
      android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt
  7. 66
      android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt
  8. 116
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  9. 3
      ios/Runner/Info.plist
  10. 8
      lib/core/bindings/initial_binding.dart
  11. 199
      lib/core/services/floating_window_manager.dart
  12. 4
      lib/core/translations/app_translations.dart
  13. 2
      lib/core/translations/language/bg_bg.dart
  14. 2
      lib/core/translations/language/cs_cz.dart
  15. 2
      lib/core/translations/language/da_dk.dart
  16. 2
      lib/core/translations/language/de_de.dart
  17. 2
      lib/core/translations/language/el_gr.dart
  18. 2
      lib/core/translations/language/en_us.dart
  19. 2
      lib/core/translations/language/es_es.dart
  20. 2
      lib/core/translations/language/et_ee.dart
  21. 2
      lib/core/translations/language/fi_fi.dart
  22. 2
      lib/core/translations/language/fr_fr.dart
  23. 2
      lib/core/translations/language/hi_in.dart
  24. 2
      lib/core/translations/language/hr_hr.dart
  25. 2
      lib/core/translations/language/hu_hu.dart
  26. 2
      lib/core/translations/language/id_id.dart
  27. 2
      lib/core/translations/language/is_is.dart
  28. 2
      lib/core/translations/language/it_it.dart
  29. 2
      lib/core/translations/language/ja_jp.dart
  30. 2
      lib/core/translations/language/ko_kr.dart
  31. 2
      lib/core/translations/language/lt_lt.dart
  32. 2
      lib/core/translations/language/lv_lv.dart
  33. 2
      lib/core/translations/language/ms_my.dart
  34. 2
      lib/core/translations/language/nb_no.dart
  35. 2
      lib/core/translations/language/nl_nl.dart
  36. 2
      lib/core/translations/language/pl_pl.dart
  37. 2
      lib/core/translations/language/pt_pt.dart
  38. 2
      lib/core/translations/language/ro_ro.dart
  39. 2
      lib/core/translations/language/ru_ru.dart
  40. 2
      lib/core/translations/language/sk_sk.dart
  41. 2
      lib/core/translations/language/sl_si.dart
  42. 2
      lib/core/translations/language/sr_rs.dart
  43. 2
      lib/core/translations/language/sv_se.dart
  44. 2
      lib/core/translations/language/th_th.dart
  45. 2
      lib/core/translations/language/tl_ph.dart
  46. 2
      lib/core/translations/language/tr_tr.dart
  47. 2
      lib/core/translations/language/uk_ua.dart
  48. 2
      lib/core/translations/language/vi_vn.dart
  49. 2
      lib/core/translations/language/zh_cn.dart
  50. 2
      lib/core/translations/language/zh_hk.dart
  51. 70
      lib/core/utils/overlay_permission_util.dart
  52. 4
      lib/data/services/bluetooth_manager.dart
  53. 9
      lib/data/services/network/api.dart
  54. 89
      lib/data/services/ota_check_service.dart
  55. 362
      lib/data/services/pay_service.dart
  56. 199
      lib/main.dart
  57. 33
      lib/modules/agent/controllers/agent_controller.dart
  58. 4
      lib/modules/agent/views/message_bubble.dart
  59. 9
      lib/modules/login/controllers/weixinLogin.dart
  60. 6
      lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
  61. 2
      lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart
  62. 2
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  63. 50
      lib/modules/pairing/controllers/pairing_controller.dart
  64. 84
      lib/modules/pairing/views/pairing_view.dart
  65. 9
      lib/modules/payment/bindings/payment_binding.dart
  66. 143
      lib/modules/payment/controllers/payment_controller.dart
  67. 506
      lib/modules/payment/views/payment_view.dart
  68. 2
      lib/modules/permissions/controllers/permissions_controller.dart
  69. 19
      lib/modules/profile/views/profile_view.dart
  70. 8
      lib/modules/settings/controllers/settings_controller.dart
  71. 38
      lib/modules/settings/views/settings_view.dart
  72. 31
      lib/modules/splash/controllers/splash_controller.dart
  73. 115
      lib/modules/splash/views/splash_view.dart
  74. 148
      lib/modules/translation/controllers/translation_controller.dart
  75. 162
      lib/modules/translation/controllers/translation_detail_controller.dart
  76. 46
      lib/modules/translation/controllers/translation_list_controller.dart
  77. 172
      lib/modules/translation/views/dialogs/delete_confirm_dialog.dart
  78. 187
      lib/modules/translation/views/dialogs/translation_long_press_menu.dart
  79. 81
      lib/modules/translation/views/dialogs/translation_mode_selector.dart
  80. 352
      lib/modules/translation/views/translation_detail_view.dart
  81. 358
      lib/modules/translation/views/translation_view.dart
  82. 37
      lib/modules/translation/views/widgets/draggable_floating_button.dart
  83. 9
      lib/routes/app_pages.dart
  84. 3
      lib/routes/app_routes.dart
  85. 413
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  86. 340
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  87. 17
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  88. 391
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  89. 26
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift
  90. 95
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  91. 50
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  92. 119
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  93. 190
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  94. 10
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  95. 1
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift
  96. 1
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift
  97. 14
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  98. 2
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift
  99. 72
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt
  100. 62
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

17
android/app/build.gradle.kts

@ -61,7 +61,8 @@ android {
manifestPlaceholders.putAll(
mapOf(
"redirectSchemeName" to "spotify-sdk",
"redirectHostName" to "auth"
"redirectHostName" to "auth",
"applicationName" to "com.saitong.voitrans.MyApp" // 新增:声明自定义 Application
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
@ -105,7 +106,11 @@ android {
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
// signingConfig = signingConfigs.getByName("debug")
// Release 版本只保留 arm64-v8a 和 armeabi-v7a(移除 x86/x86_64)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
debug {
isMinifyEnabled = false
@ -115,6 +120,11 @@ android {
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
// Debug 版本保留所有 ABI(包括 x86/x86_64,用于模拟器)
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
}
}
@ -122,6 +132,9 @@ android {
dependencies {
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加 CardView 依赖
implementation("androidx.cardview:cardview:1.0.0")
}
flutter {

10
android/app/src/main/AndroidManifest.xml

@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools">
<!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 -->
@ -84,8 +85,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
@ -121,6 +121,12 @@
android:value="78e7946dd602778df82c6ae143a88184"/>
<service android:name="com.amap.api.location.APSService"></service>
<!-- 悬浮窗服务 -->
<service
android:name="com.saitong.voitrans.FloatingWindowService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

133
android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowPlugin.kt

@ -0,0 +1,133 @@
package com.saitong.voitrans
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
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
class FloatingWindowPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "floating_window")
channel.setMethodCallHandler(this)
// 设置回调通道
FloatingWindowService.methodChannel = channel
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"enableFloatingWindow" -> {
enableFloatingWindow(result)
}
"disableFloatingWindow" -> {
disableFloatingWindow(result)
}
"showFloatingWindow" -> {
showFloatingWindow(result)
}
"hideFloatingWindow" -> {
hideFloatingWindow(result)
}
"updateContent" -> {
updateContent(call, result)
}
"setPosition" -> {
setPosition(call, result)
}
else -> {
result.notImplemented()
}
}
}
private fun enableFloatingWindow(result: Result) {
try {
val intent = Intent(context, FloatingWindowService::class.java)
context.startService(intent)
result.success(true)
} catch (e: Exception) {
result.error("ENABLE_ERROR", "Failed to enable floating window: ${e.message}", null)
}
}
private fun disableFloatingWindow(result: Result) {
try {
val intent = Intent(context, FloatingWindowService::class.java)
context.stopService(intent)
result.success(true)
} catch (e: Exception) {
result.error("DISABLE_ERROR", "Failed to disable floating window: ${e.message}", null)
}
}
private fun showFloatingWindow(result: Result) {
try {
// 服务已经在运行,不需要额外操作
result.success(true)
} catch (e: Exception) {
result.error("SHOW_ERROR", "Failed to show floating window: ${e.message}", null)
}
}
private fun hideFloatingWindow(result: Result) {
try {
FloatingWindowService.serviceInstance?.stopSelf()
result.success(true)
} catch (e: Exception) {
result.error("HIDE_ERROR", "Failed to hide floating window: ${e.message}", null)
}
}
private fun updateContent(call: MethodCall, result: Result) {
try {
val args = call.arguments as? Map<String, Any>
if (args != null) {
val intent = Intent(context, FloatingWindowService::class.java).apply {
action = FloatingWindowService.ACTION_UPDATE_CONTENT
putExtra(FloatingWindowService.EXTRA_SOURCE_TEXT, args["sourceText"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TRANSLATED_TEXT, args["translatedText"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_SOURCE_LANGUAGE, args["sourceLanguage"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TARGET_LANGUAGE, args["targetLanguage"] as? String ?: "")
putExtra(FloatingWindowService.EXTRA_TIMESTAMP, (args["timestamp"] as? Number)?.toLong() ?: 0L)
putExtra(FloatingWindowService.EXTRA_IS_INTERMEDIATE, args["isIntermediate"] as? Boolean ?: false)
}
context.startService(intent)
}
result.success(true)
} catch (e: Exception) {
result.error("UPDATE_ERROR", "Failed to update content: ${e.message}", null)
}
}
private fun setPosition(call: MethodCall, result: Result) {
try {
val args = call.arguments as? Map<String, Any>
if (args != null) {
val intent = Intent(context, FloatingWindowService::class.java).apply {
action = FloatingWindowService.ACTION_SET_POSITION
putExtra(FloatingWindowService.EXTRA_X, (args["x"] as? Number)?.toInt() ?: 0)
putExtra(FloatingWindowService.EXTRA_Y, (args["y"] as? Number)?.toInt() ?: 0)
}
context.startService(intent)
}
result.success(true)
} catch (e: Exception) {
result.error("POSITION_ERROR", "Failed to set position: ${e.message}", null)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
FloatingWindowService.methodChannel = null
}
}

376
android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt

@ -0,0 +1,376 @@
package com.saitong.voitrans
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.*
import android.widget.TextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.FrameLayout
import androidx.cardview.widget.CardView
import io.flutter.plugin.common.MethodChannel
import java.text.SimpleDateFormat
import java.util.*
class FloatingWindowService : Service() {
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var isMinimized = true
private lateinit var sourceTextView: TextView
private lateinit var translatedTextView: TextView
private lateinit var languageInfoView: TextView
private lateinit var timestampView: TextView
private lateinit var minimizedView: CardView
private lateinit var expandedView: CardView
companion object {
var methodChannel: MethodChannel? = null
var serviceInstance: FloatingWindowService? = null
const val ACTION_UPDATE_CONTENT = "UPDATE_CONTENT"
const val ACTION_SET_POSITION = "SET_POSITION"
const val EXTRA_SOURCE_TEXT = "source_text"
const val EXTRA_TRANSLATED_TEXT = "translated_text"
const val EXTRA_SOURCE_LANGUAGE = "source_language"
const val EXTRA_TARGET_LANGUAGE = "target_language"
const val EXTRA_TIMESTAMP = "timestamp"
const val EXTRA_IS_INTERMEDIATE = "is_intermediate"
const val EXTRA_X = "x"
const val EXTRA_Y = "y"
}
override fun onCreate() {
super.onCreate()
serviceInstance = this
createFloatingWindow()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_UPDATE_CONTENT -> updateContent(intent)
ACTION_SET_POSITION -> setPosition(intent)
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createFloatingWindow() {
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
// 创建布局
floatingView = createFloatingLayout()
// 初始化视图组件
initViews()
// 设置点击事件
setupClickListeners()
// 设置窗口参数
val layoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
format = PixelFormat.TRANSLUCENT
gravity = Gravity.TOP or Gravity.START
x = 100
y = 100
}
windowManager?.addView(floatingView, layoutParams)
// 默认显示最小化状态
updateViewState(true)
}
private fun createFloatingLayout(): View {
val context = this
val density = resources.displayMetrics.density
// 创建根布局
val rootLayout = FrameLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// 创建最小化视图
val minimizedCard = CardView(context).apply {
id = View.generateViewId()
layoutParams = FrameLayout.LayoutParams(
(60 * density).toInt(),
(60 * density).toInt()
).apply {
setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
radius = 30 * density
cardElevation = 8 * density
setCardBackgroundColor(0xFFE3F2FD.toInt())
}
val minimizedContent = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
val icon = TextView(context).apply {
text = "译"
textSize = 14f
setTextColor(0xFF1976D2.toInt())
gravity = Gravity.CENTER
}
minimizedContent.addView(icon)
minimizedCard.addView(minimizedContent)
// 创建展开视图
val expandedCard = CardView(context).apply {
id = View.generateViewId()
layoutParams = FrameLayout.LayoutParams(
(320 * density).toInt(),
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
radius = 16 * density
cardElevation = 12 * density
setCardBackgroundColor(0xFFFFFFFF.toInt())
visibility = View.GONE
}
val expandedContent = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
(16 * density).toInt(),
(16 * density).toInt(),
(16 * density).toInt(),
(16 * density).toInt()
)
}
// 标题栏
val titleLayout = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
val titleText = TextView(context).apply {
text = "实时翻译"
textSize = 16f
setTextColor(0xFF333333.toInt())
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
}
val closeButton = TextView(context).apply {
id = View.generateViewId()
text = "✕"
textSize = 18f
setTextColor(0xFF666666.toInt())
gravity = Gravity.CENTER
setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
}
titleLayout.addView(titleText)
titleLayout.addView(closeButton)
// 语言信息
val languageInfo = TextView(context).apply {
id = View.generateViewId()
text = "中文 → 英文"
textSize = 12f
setTextColor(0xFF666666.toInt())
setPadding(0, (8 * density).toInt(), 0, (8 * density).toInt())
}
// 源文本
val sourceText = TextView(context).apply {
id = View.generateViewId()
text = "正在识别..."
textSize = 14f
setTextColor(0xFF333333.toInt())
setPadding(0, (8 * density).toInt(), 0, (4 * density).toInt())
maxLines = 3
}
// 翻译文本
val translatedText = TextView(context).apply {
id = View.generateViewId()
text = "翻译中..."
textSize = 14f
setTextColor(0xFF1976D2.toInt())
setPadding(0, (4 * density).toInt(), 0, (8 * density).toInt())
maxLines = 3
}
// 时间戳
val timestamp = TextView(context).apply {
id = View.generateViewId()
text = ""
textSize = 10f
setTextColor(0xFF999999.toInt())
gravity = Gravity.END
}
expandedContent.addView(titleLayout)
expandedContent.addView(languageInfo)
expandedContent.addView(sourceText)
expandedContent.addView(translatedText)
expandedContent.addView(timestamp)
expandedCard.addView(expandedContent)
rootLayout.addView(minimizedCard)
rootLayout.addView(expandedCard)
// 保存引用
minimizedView = minimizedCard
expandedView = expandedCard
sourceTextView = sourceText
translatedTextView = translatedText
languageInfoView = languageInfo
timestampView = timestamp
return rootLayout
}
private fun initViews() {
// Views are already initialized in createFloatingLayout
}
private fun setupClickListeners() {
// 最小化状态点击展开
minimizedView.setOnClickListener {
isMinimized = false
updateViewState(false)
notifyStateChange("expanded")
}
// 展开状态点击最小化
expandedView.setOnClickListener {
isMinimized = true
updateViewState(true)
notifyStateChange("minimized")
}
// 长按关闭
floatingView!!.setOnLongClickListener {
closeFloatingWindow()
true
}
// 拖拽功能
setupDragListener()
}
private fun setupDragListener() {
var initialX = 0
var initialY = 0
var initialTouchX = 0f
var initialTouchY = 0f
floatingView!!.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
initialX = layoutParams.x
initialY = layoutParams.y
initialTouchX = event.rawX
initialTouchY = event.rawY
true
}
MotionEvent.ACTION_MOVE -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
layoutParams.x = initialX + (event.rawX - initialTouchX).toInt()
layoutParams.y = initialY + (event.rawY - initialTouchY).toInt()
windowManager?.updateViewLayout(floatingView, layoutParams)
true
}
else -> false
}
}
}
private fun updateViewState(minimized: Boolean) {
if (minimized) {
minimizedView.visibility = View.VISIBLE
expandedView.visibility = View.GONE
} else {
minimizedView.visibility = View.GONE
expandedView.visibility = View.VISIBLE
}
}
private fun updateContent(intent: Intent) {
val sourceText = intent.getStringExtra(EXTRA_SOURCE_TEXT) ?: ""
val translatedText = intent.getStringExtra(EXTRA_TRANSLATED_TEXT) ?: ""
val sourceLanguage = intent.getStringExtra(EXTRA_SOURCE_LANGUAGE) ?: ""
val targetLanguage = intent.getStringExtra(EXTRA_TARGET_LANGUAGE) ?: ""
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, 0)
val isIntermediate = intent.getBooleanExtra(EXTRA_IS_INTERMEDIATE, false)
// 更新UI
sourceTextView.text = if (sourceText.isNotEmpty()) sourceText else "正在识别..."
translatedTextView.text = if (translatedText.isNotEmpty()) translatedText else "翻译中..."
languageInfoView.text = "$sourceLanguage → $targetLanguage"
if (timestamp > 0) {
val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timestampView.text = dateFormat.format(Date(timestamp))
}
// 如果是中间结果,可以添加一些视觉提示
if (isIntermediate) {
translatedTextView.alpha = 0.7f
} else {
translatedTextView.alpha = 1.0f
}
}
private fun setPosition(intent: Intent) {
val x = intent.getIntExtra(EXTRA_X, 0)
val y = intent.getIntExtra(EXTRA_Y, 0)
floatingView?.let { view ->
val layoutParams = view.layoutParams as WindowManager.LayoutParams
layoutParams.x = x
layoutParams.y = y
windowManager?.updateViewLayout(view, layoutParams)
}
}
private fun closeFloatingWindow() {
stopSelf()
notifyFloatingWindowClosed()
}
private fun notifyStateChange(state: String) {
methodChannel?.invokeMethod("onFloatingWindowStateChanged", mapOf("state" to state))
}
private fun notifyFloatingWindowClosed() {
methodChannel?.invokeMethod("onFloatingWindowClosed", null)
}
override fun onDestroy() {
super.onDestroy()
floatingView?.let { windowManager?.removeView(it) }
serviceInstance = null
}
}

245
android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt

@ -0,0 +1,245 @@
package com.saitong.voitrans
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel // 添加这行导入
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
private var pendingDeepLink: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "🚀 MainActivity onCreate 开始")
super.onCreate(savedInstanceState)
// 检查 Intent 信息
intent?.let {
Log.d(TAG, "Intent action: ${it.action}")
Log.d(TAG, "Intent data: ${it.data}")
Log.d(TAG, "Intent extras: ${it.extras}")
}
// 暂存深链接,等Flutter引擎完全准备好后处理
if (intent?.action == Intent.ACTION_VIEW && intent?.data != null) {
pendingDeepLink = intent
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成")
}
// 设置日志级别,减少系统级日志
suppressSystemLogs()
Log.d(TAG, "✅ MainActivity onCreate 完成")
}
// 添加 onNewIntent 方法处理深链接
override fun onNewIntent(intent: Intent) {
Log.d(TAG, "🔗 onNewIntent 被调用")
super.onNewIntent(intent)
setIntent(intent) // 更新当前 Intent
// 检查新的 Intent 信息
Log.d(TAG, "New Intent action: ${intent.action}")
Log.d(TAG, "New Intent data: ${intent.data}")
Log.d(TAG, "New Intent extras: ${intent.extras}")
// 处理深链接
handleDeepLink(intent)
}
override fun provideFlutterEngine(context: Context): FlutterEngine? {
Log.d(TAG, "🔍 provideFlutterEngine 被调用")
val engine = FlutterEngineCache.getInstance().get("main_engine")
if (engine == null) {
Log.w(TAG, "❌ FlutterEngine 缓存为空,这可能导致问题")
// 如果缓存为空,让 Flutter 框架创建默认 Engine
return null
}
// 检查 Engine 状态 - 使用更兼容的方式
try {
// 检查 Dart 执行器是否正在运行
val isExecutorRunning = engine.dartExecutor.isExecutingDart
Log.d(TAG, "FlutterEngine Dart 执行状态: $isExecutorRunning")
// 如果需要,可以检查其他状态
if (!isExecutorRunning) {
Log.w(TAG, "⚠️ FlutterEngine Dart 执行器未运行,可能需要重新初始化")
}
} catch (e: Exception) {
Log.w(TAG, "❌ 检查 FlutterEngine 状态时出错", e)
}
Log.d(TAG, "✅ 使用缓存的 FlutterEngine")
return engine
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
Log.d(TAG, "🔧 configureFlutterEngine 开始")
// 检查插件是否已经注册,避免重复注册
val pluginRegistry = flutterEngine.plugins
// 只有在插件未注册时才调用 GeneratedPluginRegistrant
if (!pluginRegistry.has(io.flutter.plugins.pathprovider.PathProviderPlugin::class.java)) {
Log.d(TAG, "📦 注册 Flutter 插件")
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
} else {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
}
// Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({
pendingDeepLink?.let { intent ->
Log.d(TAG, "🔗 处理待处理的深链接")
handleDeepLink(intent)
pendingDeepLink = null
}
}, 1000) // 延迟1秒确保Flutter完全准备好
Log.d(TAG, "✅ configureFlutterEngine 完成")
}
// 处理深链接的统一方法
private fun handleDeepLink(intent: Intent?) {
intent?.let {
if (it.action == Intent.ACTION_VIEW && it.data != null) {
val uri = it.data
Log.d(TAG, "🔗 处理深链接: $uri")
try {
Log.d(TAG, "🔗 开始发送深链接到 Flutter")
// 通过 MethodChannel 将深链接信息传递给 Flutter
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
Log.d(TAG, "🔗 获取到 BinaryMessenger")
val channel = MethodChannel(
messenger,
"com.saitong.voitrans/deeplink"
)
val arguments = mapOf(
"url" to uri.toString(),
"scheme" to uri?.scheme,
"host" to uri?.host,
"path" to uri?.path,
"query" to uri?.query,
"queryParameters" to uri?.queryParameterNames?.associateWith {
uri.getQueryParameter(it)
}
)
Log.d(TAG, "🔗 准备发送参数: $arguments")
channel.invokeMethod("handleDeepLink", arguments)
Log.d(TAG, "✅ 深链接信息已发送到 Flutter")
} ?: run {
Log.w(TAG, "❌ BinaryMessenger 为空,无法发送深链接")
}
} catch (e: Exception) {
Log.e(TAG, "❌ 处理深链接时出错", e)
}
} else {
Log.d(TAG, "🔗 Intent 不是深链接或数据为空")
}
}
}
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
)
// 尝试多种方式设置日志级别
systemTags.forEach { tag ->
try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
}
}
}

150
android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt

@ -0,0 +1,150 @@
package com.saitong.voitrans
import android.app.ActivityManager
import android.content.Context
import android.util.Log
import io.flutter.app.FlutterApplication
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.GeneratedPluginRegistrant
import com.saitong.voitrans.FloatingWindowPlugin
import com.saitong.voitrans.OverlayPermissionPlugin
// Application.kt
class MyApp : FlutterApplication() {
companion object {
private const val TAG = "MyApp"
private const val ENGINE_ID = "main_engine"
// 标记 Engine 是否已经执行过 Dart 入口点
@Volatile
private var isDartEntrypointExecuted = false
// 记录应用启动时间,用于判断是否为新启动
@Volatile
private var appStartTime = 0L
}
override fun onCreate() {
super.onCreate()
appStartTime = System.currentTimeMillis()
Log.d(TAG, "MyApp onCreate - 进程启动")
val cache = FlutterEngineCache.getInstance()
val existingEngine = cache.get(ENGINE_ID)
if (existingEngine == null) {
Log.d(TAG, "创建新的 FlutterEngine")
createAndCacheEngine(cache)
} else {
Log.d(TAG, "FlutterEngine 已存在,检查状态")
// 检查现有 Engine 的状态
try {
val isExecuting = existingEngine.dartExecutor.isExecutingDart
Log.d(TAG, "Engine Dart执行状态: $isExecuting")
if (!isExecuting && isMainProcess()) {
Log.d(TAG, "Engine 存在但Dart未执行,重新执行")
existingEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
}
} catch (e: Exception) {
Log.e(TAG, "检查或重新执行 Dart 入口点失败", e)
// 如果失败,重新创建 Engine
cache.remove(ENGINE_ID)
createAndCacheEngine(cache)
}
}
}
private fun createAndCacheEngine(cache: FlutterEngineCache) {
// 在主进程中总是创建并执行Dart入口点
if (isMainProcess()) {
Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart")
val flutterEngine = FlutterEngine(this)
// 注册插件(只在这里注册一次)
Log.d(TAG, "📦 注册 Flutter 插件")
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 注册悬浮窗相关插件
flutterEngine.plugins.add(FloatingWindowPlugin())
flutterEngine.plugins.add(OverlayPermissionPlugin())
try {
Log.d(TAG, "🚀 开始执行 Dart 入口点")
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
isDartEntrypointExecuted = true
cache.put(ENGINE_ID, flutterEngine)
Log.d(TAG, "✅ FlutterEngine 创建并缓存成功")
} catch (e: Exception) {
Log.e(TAG, "❌ 执行 Dart 入口点失败", e)
// 如果执行失败,仍然缓存 Engine,但标记为未执行
isDartEntrypointExecuted = false
cache.put(ENGINE_ID, flutterEngine)
}
} else {
Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine")
// 非主进程只创建 Engine,不执行 Dart 入口点
val flutterEngine = FlutterEngine(this)
cache.put(ENGINE_ID, flutterEngine)
}
}
private fun isMainProcess(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return true
val mainProcessName = packageName
val myPid = android.os.Process.myPid()
for (process in processes) {
if (process.pid == myPid) {
return process.processName == mainProcessName
}
}
return true
}
private fun isAppInForeground(): Boolean {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses ?: return false
val pkg = packageName
for (p in processes) {
if (p.processName == pkg &&
p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true
}
}
return false
}
private fun isAppBeingLaunched(): Boolean {
// 改进的启动检测逻辑
val currentTime = System.currentTimeMillis()
val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动
if (isRecentStart) {
Log.d(TAG, "检测到应用新启动")
return true
}
// 备用检测方法(兼容旧版本)
return try {
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.getRunningTasks(1)
val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName
Log.d(TAG, "通过 RunningTasks 检测启动状态: $result")
result
} catch (e: Exception) {
Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e)
// 如果无法获取任务信息,默认认为是启动状态
true
}
}
}

66
android/app/src/main/kotlin/com/saitong/voitrans/OverlayPermissionPlugin.kt

@ -0,0 +1,66 @@
package com.saitong.voitrans
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
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
class OverlayPermissionPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "overlay_permission")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"hasOverlayPermission" -> {
result.success(hasOverlayPermission())
}
"requestOverlayPermission" -> {
requestOverlayPermission(result)
}
else -> {
result.notImplemented()
}
}
}
private fun hasOverlayPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(context)
} else {
true
}
}
private fun requestOverlayPermission(result: Result) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = Uri.parse("package:${context.packageName}")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
result.success(true)
} else {
result.success(true)
}
} catch (e: Exception) {
result.error("REQUEST_ERROR", "Failed to request overlay permission: ${e.message}", null)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

116
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -1,116 +0,0 @@
package com.saitong.voitrans
import android.app.PendingIntent
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置日志级别,减少系统级日志
suppressSystemLogs()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 注册标准Flutter插件
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
)
// 尝试多种方式设置日志级别
systemTags.forEach { tag ->
try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
}
}
}

3
ios/Runner/Info.plist

@ -91,6 +91,9 @@
<string>google.navigation</string>
<string>googlemaps</string>
<string>comgooglemaps</string>
<string>baidumap</string>
<string>amapuri</string>
<string>maps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>

8
lib/core/bindings/initial_binding.dart

@ -1,3 +1,4 @@
import 'package:voitrans/data/services/pay_service.dart';
import 'package:voitrans/data/services/qq_music.dart';
import '../../../data/services/user_portrait.dart';
@ -22,6 +23,7 @@ import '../../data/services/account_service.dart';
import '../../data/services/version_update_service.dart';
import '../../data/services/ota_check_service.dart';
import '../../core/utils/app_lifecycle_manager.dart';
import '../services/floating_window_manager.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -87,6 +89,12 @@ class InitialBinding extends Bindings {
// OTA检查服务
Get.lazyPut<OtaCheckService>(() => OtaCheckService(), fenix: true);
// 悬浮窗管理器
Get.lazyPut<FloatingWindowManager>(() => FloatingWindowManager(),
fenix: true);
// 支付服务
Get.lazyPut<PayService>(() => PayService(), fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)

199
lib/core/services/floating_window_manager.dart

@ -0,0 +1,199 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../utils/logger.dart';
import '../utils/overlay_permission_util.dart';
/// 悬浮窗状态
enum FloatingWindowState {
hidden, // 隐藏
minimized, // 最小化
expanded, // 展开
}
/// 悬浮窗数据模型
class FloatingWindowData {
final String sourceText;
final String translatedText;
final String sourceLanguage;
final String targetLanguage;
final DateTime timestamp;
final bool isIntermediate;
FloatingWindowData({
required this.sourceText,
required this.translatedText,
required this.sourceLanguage,
required this.targetLanguage,
required this.timestamp,
this.isIntermediate = false,
});
Map<String, dynamic> toMap() {
return {
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguage': sourceLanguage,
'targetLanguage': targetLanguage,
'timestamp': timestamp.millisecondsSinceEpoch,
'isIntermediate': isIntermediate,
};
}
}
/// 悬浮窗管理器
class FloatingWindowManager extends GetxController {
static const String TAG = 'FloatingWindowManager';
static const MethodChannel _channel = MethodChannel('floating_window');
final _isEnabled = false.obs;
final _currentState = FloatingWindowState.hidden.obs;
final _hasPermission = false.obs;
// Getters
bool get isEnabled => _isEnabled.value;
FloatingWindowState get currentState => _currentState.value;
bool get hasPermission => _hasPermission.value;
@override
void onInit() {
super.onInit();
_setupMethodCallHandler();
_checkPermissions();
}
/// 设置方法调用处理器
void _setupMethodCallHandler() {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'onFloatingWindowStateChanged':
final String state = call.arguments['state'];
_updateState(_parseState(state));
break;
case 'onFloatingWindowClosed':
_currentState.value = FloatingWindowState.hidden;
_isEnabled.value = false;
break;
case 'onFloatingWindowClicked':
// 悬浮窗被点击,可以执行相应操作(如返回主应用)
_handleFloatingWindowClick();
break;
}
});
}
/// 检查权限
Future<void> _checkPermissions() async {
_hasPermission.value = await OverlayPermissionUtil.hasOverlayPermission();
}
/// 启用悬浮窗功能
Future<bool> enableFloatingWindow() async {
if (!_hasPermission.value) {
final bool granted =
await OverlayPermissionUtil.requestOverlayPermission();
if (!granted) {
Get.snackbar('权限不足'.tr, '需要悬浮窗权限才能启用此功能'.tr);
return false;
}
_hasPermission.value = true;
}
try {
await _channel.invokeMethod('enableFloatingWindow');
_isEnabled.value = true;
Logger.info('悬浮窗功能已启用');
return true;
} catch (e) {
Logger.error('启用悬浮窗失败: $e');
return false;
}
}
/// 禁用悬浮窗功能
Future<void> disableFloatingWindow() async {
try {
await _channel.invokeMethod('disableFloatingWindow');
_isEnabled.value = false;
_currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗功能已禁用');
} catch (e) {
Logger.error('禁用悬浮窗失败: $e');
}
}
/// 显示悬浮窗
Future<void> showFloatingWindow() async {
if (!_isEnabled.value) return;
try {
await _channel.invokeMethod('showFloatingWindow');
_currentState.value = FloatingWindowState.minimized;
Logger.info('悬浮窗已显示');
} catch (e) {
Logger.error('显示悬浮窗失败: $e');
}
}
/// 隐藏悬浮窗
Future<void> hideFloatingWindow() async {
try {
await _channel.invokeMethod('hideFloatingWindow');
_currentState.value = FloatingWindowState.hidden;
Logger.info('悬浮窗已隐藏');
} catch (e) {
Logger.error('隐藏悬浮窗失败: $e');
}
}
/// 更新悬浮窗内容
Future<void> updateFloatingWindowContent(FloatingWindowData data) async {
if (!_isEnabled.value ||
_currentState.value == FloatingWindowState.hidden) {
return;
}
try {
await _channel.invokeMethod('updateContent', data.toMap());
Logger.info('悬浮窗内容已更新');
} catch (e) {
Logger.error('更新悬浮窗内容失败: $e');
}
}
/// 设置悬浮窗位置
Future<void> setFloatingWindowPosition(double x, double y) async {
try {
await _channel.invokeMethod('setPosition', {'x': x, 'y': y});
} catch (e) {
Logger.error('设置悬浮窗位置失败: $e');
}
}
/// 解析状态字符串
FloatingWindowState _parseState(String state) {
switch (state) {
case 'minimized':
return FloatingWindowState.minimized;
case 'expanded':
return FloatingWindowState.expanded;
case 'hidden':
default:
return FloatingWindowState.hidden;
}
}
/// 更新状态
void _updateState(FloatingWindowState state) {
_currentState.value = state;
Logger.info('悬浮窗状态更新为: $state');
}
/// 处理悬浮窗点击事件
void _handleFloatingWindowClick() {
// 这里可以实现点击悬浮窗后的操作,比如返回主应用
Logger.info('悬浮窗被点击');
// 可以发送事件给其他组件处理
}
}

4
lib/core/translations/app_translations.dart

@ -186,11 +186,11 @@ class AppTranslations extends Translations {
return fallbackLocale;
}
return _getMatchedLocale(systemLocale);
return getMatchedLocale(systemLocale);
}
/// 智能匹配系统语言到支持的语言
static Locale _getMatchedLocale(Locale systemLocale) {
static Locale getMatchedLocale(Locale systemLocale) {
String languageCode = systemLocale.languageCode.toLowerCase();
String? countryCode = systemLocale.countryCode?.toUpperCase();
String? scriptCode = systemLocale.scriptCode;

2
lib/core/translations/language/bg_bg.dart

@ -824,7 +824,7 @@ const Map<String, String> bgBG = {
"charging": "Зареждане",
"updateContent": "Съдържание на актуализацията",
"removeEarbudsEnsureBattery":
"Извадете двете слушалки и се уверете, че батерията на двете е над 30%",
"Извадете двете слушалки и се уверете, че батерията на двете е над 20%",
// 音乐列表页面
"noMusic": "Няма музика", // 暂无音乐

2
lib/core/translations/language/cs_cz.dart

@ -810,7 +810,7 @@ const Map<String, String> csCZ = {
"charging": "Nabíjení",
"updateContent": "Obsah aktualizace",
"removeEarbudsEnsureBattery":
"Vyjměte obě sluchátka a ujistěte se, že baterie obou je nad 30%",
"Vyjměte obě sluchátka a ujistěte se, že baterie obou je nad 20%",
// 音乐列表页面
"noMusic": "Žádná hudba", // 暂无音乐

2
lib/core/translations/language/da_dk.dart

@ -809,7 +809,7 @@ const Map<String, String> daDK = {
"charging": "Oplader",
"updateContent": "Opdateringsindhold",
"removeEarbudsEnsureBattery":
"Tag begge øretelefoner ud og sørg for at batteriet på begge er over 30%",
"Tag begge øretelefoner ud og sørg for at batteriet på begge er over 20%",
// 音乐列表页面
"noMusic": "Ingen musik", // 暂无音乐

2
lib/core/translations/language/de_de.dart

@ -829,7 +829,7 @@ const Map<String, String> deDE = {
"charging": "Laden",
"updateContent": "Update-Inhalt",
"removeEarbudsEnsureBattery":
"Entfernen Sie beide Ohrhörer und stellen Sie sicher, dass der Batteriestand über 30% liegt",
"Entfernen Sie beide Ohrhörer und stellen Sie sicher, dass der Batteriestand über 20% liegt",
// 音乐列表页面
"noMusic": "Keine Musik", // 暂无音乐

2
lib/core/translations/language/el_gr.dart

@ -819,7 +819,7 @@ const Map<String, String> elGR = {
"charging": "Φόρτιση",
"updateContent": "Περιεχόμενο ενημέρωσης",
"removeEarbudsEnsureBattery":
"Αφαιρέστε και τα δύο ακουστικά και βεβαιωθείτε ότι η μπαταρία και των δύο είναι πάνω από 30%",
"Αφαιρέστε και τα δύο ακουστικά και βεβαιωθείτε ότι η μπαταρία και των δύο είναι πάνω από 20%",
// 音乐列表页面
"noMusic": "Δεν υπάρχει μουσική", // 暂无音乐

2
lib/core/translations/language/en_us.dart

@ -1137,7 +1137,7 @@ const Map<String, String> enUS = {
"charging": "Charging",
"updateContent": "Update Content",
"removeEarbudsEnsureBattery":
"Remove both earbuds and ensure battery level is above 30%",
"Remove both earbuds and ensure battery level is above 20%",
// 音乐列表页面
"noMusic": "No Music", // 暂无音乐

2
lib/core/translations/language/es_es.dart

@ -823,7 +823,7 @@ const Map<String, String> esES = {
"charging": "Cargando",
"updateContent": "Contenido de actualización",
"removeEarbudsEnsureBattery":
"Retira ambos auriculares y asegúrate de que la batería de ambos esté por encima del 30%",
"Retira ambos auriculares y asegúrate de que la batería de ambos esté por encima del 20%",
// 音乐列表页面
"noMusic": "Sin música", // 暂无音乐

2
lib/core/translations/language/et_ee.dart

@ -798,7 +798,7 @@ const Map<String, String> etEE = {
"charging": "Laadimine",
"updateContent": "Uuenduse sisu",
"removeEarbudsEnsureBattery":
"Võta mõlemad kõrvaklapid välja ja veendu, et mõlema aku on üle 30%",
"Võta mõlemad kõrvaklapid välja ja veendu, et mõlema aku on üle 20%",
// 音乐列表页面
"noMusic": "Muusikat pole", // 暂无音乐

2
lib/core/translations/language/fi_fi.dart

@ -793,7 +793,7 @@ const Map<String, String> fiFI = {
"charging": "Ladataan",
"updateContent": "Päivityksen sisältö",
"removeEarbudsEnsureBattery":
"Poista molemmat kuulokkeet ja varmista, että molempien akun varaus on yli 30%",
"Poista molemmat kuulokkeet ja varmista, että molempien akun varaus on yli 20%",
// 音乐列表页面
"noMusic": "Ei musiikkia",

2
lib/core/translations/language/fr_fr.dart

@ -832,7 +832,7 @@ const Map<String, String> frFR = {
"charging": "En charge",
"updateContent": "Contenu de la mise à jour",
"removeEarbudsEnsureBattery":
"Retirez les deux écouteurs et assurez-vous que le niveau de batterie est supérieur à 30%",
"Retirez les deux écouteurs et assurez-vous que le niveau de batterie est supérieur à 20%",
// 音乐列表页面
"noMusic": "Aucune musique", // 暂无音乐

2
lib/core/translations/language/hi_in.dart

@ -797,7 +797,7 @@ const Map<String, String> hiIN = {
"charging": "चार्ज हो रहा है",
"updateContent": "अपडेट सामग्री",
"removeEarbudsEnsureBattery":
"दोनों ईयरबड निकालें, और सुनिश्चित करें कि दोनों की बैटरी 30% से ऊपर है",
"दोनों ईयरबड निकालें, और सुनिश्चित करें कि दोनों की बैटरी 20% से ऊपर है",
// 音乐列表页面
"noMusic": "कोई संगीत नहीं",

2
lib/core/translations/language/hr_hr.dart

@ -803,7 +803,7 @@ const Map<String, String> hrHR = {
"charging": "Punjenje",
"updateContent": "Sadržaj ažuriranja",
"removeEarbudsEnsureBattery":
"Izvadite obje slušalice i provjerite je li baterija obje iznad 30%",
"Izvadite obje slušalice i provjerite je li baterija obje iznad 20%",
// 音乐列表页面
"noMusic": "Nema glazbe",

2
lib/core/translations/language/hu_hu.dart

@ -802,7 +802,7 @@ const Map<String, String> huHU = {
"charging": "Töltés",
"updateContent": "Frissítés tartalma",
"removeEarbudsEnsureBattery":
"Vegye ki mindkét fülhallgatót, és győződjön meg róla, hogy mindkettő akkumulátora 30% felett van",
"Vegye ki mindkét fülhallgatót, és győződjön meg róla, hogy mindkettő akkumulátora 20% felett van",
// 音乐列表页面
"noMusic": "Nincs zene",

2
lib/core/translations/language/id_id.dart

@ -795,7 +795,7 @@ const Map<String, String> idID = {
"charging": "Mengisi daya",
"updateContent": "Konten Pembaruan",
"removeEarbudsEnsureBattery":
"Keluarkan kedua earbud, dan pastikan baterai keduanya di atas 30%",
"Keluarkan kedua earbud, dan pastikan baterai keduanya di atas 20%",
// 音乐列表页面
"noMusic": "Tidak Ada Musik",

2
lib/core/translations/language/is_is.dart

@ -796,7 +796,7 @@ const Map<String, String> isIS = {
"charging": "Hleður",
"updateContent": "Innihald uppfærslu",
"removeEarbudsEnsureBattery":
"Taktu báðar heyrnartólin úr og gakktu úr skugga um að rafhlaðan á báðum sé yfir 30%",
"Taktu báðar heyrnartólin úr og gakktu úr skugga um að rafhlaðan á báðum sé yfir 20%",
// 音乐列表页面
"noMusic": "Engin tónlist",

2
lib/core/translations/language/it_it.dart

@ -818,7 +818,7 @@ const Map<String, String> itIT = {
"charging": "In carica",
"updateContent": "Contenuto dell'aggiornamento",
"removeEarbudsEnsureBattery":
"Rimuovi entrambi gli auricolari e assicurati che la batteria di entrambi sia superiore al 30%",
"Rimuovi entrambi gli auricolari e assicurati che la batteria di entrambi sia superiore al 20%",
// 音乐列表页面
"noMusic": "Nessuna musica",

2
lib/core/translations/language/ja_jp.dart

@ -746,7 +746,7 @@ const Map<String, String> jaJP = {
"doNotDisconnect": "切断しないでください",
"charging": "充電中",
"updateContent": "更新内容",
"removeEarbudsEnsureBattery": "両方のイヤホンを取り出し、バッテリーレベルが30%以上であることを確認してください",
"removeEarbudsEnsureBattery": "両方のイヤホンを取り出し、バッテリーレベルが20%以上であることを確認してください",
// 音乐列表页面
"noMusic": "音楽がありません", // 暂无音乐

2
lib/core/translations/language/ko_kr.dart

@ -733,7 +733,7 @@ const Map<String, String> koKR = {
"doNotDisconnect": "연결을 끊지 마세요",
"charging": "충전 중",
"updateContent": "업데이트 내용",
"removeEarbudsEnsureBattery": "양쪽 이어버드를 꺼내고 배터리가 모두 30% 이상인지 확인하세요",
"removeEarbudsEnsureBattery": "양쪽 이어버드를 꺼내고 배터리가 모두 20% 이상인지 확인하세요",
// 音乐列表页面
"noMusic": "음악 없음",

2
lib/core/translations/language/lt_lt.dart

@ -804,7 +804,7 @@ const Map<String, String> ltLT = {
"charging": "Kraunama",
"updateContent": "Atnaujinimo turinys",
"removeEarbudsEnsureBattery":
"Išimkite abi ausines ir įsitikinkite, kad abiejų baterijos lygis yra didesnis nei 30%",
"Išimkite abi ausines ir įsitikinkite, kad abiejų baterijos lygis yra didesnis nei 20%",
// 音乐列表页面
"noMusic": "Nėra muzikos",

2
lib/core/translations/language/lv_lv.dart

@ -805,7 +805,7 @@ const Map<String, String> lvLV = {
"charging": "Uzlādējas",
"updateContent": "Atjauninājuma saturs",
"removeEarbudsEnsureBattery":
"Izņemiet abas austiņas un pārliecinieties, ka abu baterijas līmenis ir virs 30%",
"Izņemiet abas austiņas un pārliecinieties, ka abu baterijas līmenis ir virs 20%",
// 音乐列表页面
"noMusic": "Nav mūzikas",

2
lib/core/translations/language/ms_my.dart

@ -795,7 +795,7 @@ const Map<String, String> msMY = {
"charging": "Mengecas",
"updateContent": "Kandungan Kemas Kini",
"removeEarbudsEnsureBattery":
"Keluarkan kedua-dua fon telinga, dan pastikan bateri kedua-duanya melebihi 30%",
"Keluarkan kedua-dua fon telinga, dan pastikan bateri kedua-duanya melebihi 20%",
// 音乐列表页面
"noMusic": "Tiada Muzik",

2
lib/core/translations/language/nb_no.dart

@ -794,7 +794,7 @@ const Map<String, String> nbNO = {
"charging": "Lader",
"updateContent": "Oppdateringsinnhold",
"removeEarbudsEnsureBattery":
"Ta ut begge øreproppene og sørg for at batterinivået på begge er over 30 %",
"Ta ut begge øreproppene og sørg for at batterinivået på begge er over 20 %",
// 音乐列表页面
"noMusic": "Ingen musikk",

2
lib/core/translations/language/nl_nl.dart

@ -804,7 +804,7 @@ const Map<String, String> nlNL = {
"charging": "Opladen",
"updateContent": "Update-inhoud",
"removeEarbudsEnsureBattery":
"Haal beide oortjes eruit en zorg ervoor dat de batterij van beide meer dan 30% is",
"Haal beide oortjes eruit en zorg ervoor dat de batterij van beide meer dan 20% is",
// 音乐列表页面
"noMusic": "Geen muziek",

2
lib/core/translations/language/pl_pl.dart

@ -810,7 +810,7 @@ const Map<String, String> plPL = {
"charging": "Ładowanie",
"updateContent": "Treść aktualizacji",
"removeEarbudsEnsureBattery":
"Wyjmij obie słuchawki i upewnij się, że poziom naładowania baterii obu wynosi ponad 30%",
"Wyjmij obie słuchawki i upewnij się, że poziom naładowania baterii obu wynosi ponad 20%",
// 音乐列表页面
"noMusic": "Brak muzyki",

2
lib/core/translations/language/pt_pt.dart

@ -812,7 +812,7 @@ const Map<String, String> ptPT = {
"charging": "A carregar",
"updateContent": "Conteúdo da Atualização",
"removeEarbudsEnsureBattery":
"Retire ambos os auriculares e certifique-se de que a bateria de ambos está acima de 30%",
"Retire ambos os auriculares e certifique-se de que a bateria de ambos está acima de 20%",
// 音乐列表页面
"noMusic": "Sem Música",

2
lib/core/translations/language/ro_ro.dart

@ -813,7 +813,7 @@ const Map<String, String> roRO = {
"charging": "Se încarcă",
"updateContent": "Conținut actualizare",
"removeEarbudsEnsureBattery":
"Scoateți ambele căști și asigurați-vă că bateria ambelor este peste 30%",
"Scoateți ambele căști și asigurați-vă că bateria ambelor este peste 20%",
// 音乐列表页面
"noMusic": "Fără muzică",

2
lib/core/translations/language/ru_ru.dart

@ -805,7 +805,7 @@ const Map<String, String> ruRU = {
"charging": "Зарядка",
"updateContent": "Содержание обновления",
"removeEarbudsEnsureBattery":
"Извлеките оба наушника и убедитесь, что уровень заряда батареи в обоих превышает 30%",
"Извлеките оба наушника и убедитесь, что уровень заряда батареи в обоих превышает 20%",
// 音乐列表页面
"noMusic": "Нет музыки",

2
lib/core/translations/language/sk_sk.dart

@ -799,7 +799,7 @@ const Map<String, String> skSK = {
"charging": "Nabíja sa",
"updateContent": "Obsah aktualizácie",
"removeEarbudsEnsureBattery":
"Vyberte obe slúchadlá a uistite sa, že batéria oboch je nad 30%",
"Vyberte obe slúchadlá a uistite sa, že batéria oboch je nad 20%",
// 音乐列表页面
"noMusic": "Žiadna hudba",

2
lib/core/translations/language/sl_si.dart

@ -804,7 +804,7 @@ const Map<String, String> slSI = {
"charging": "Polnjenje",
"updateContent": "Vsebina posodobitve",
"removeEarbudsEnsureBattery":
"Odstranite obe slušalki in se prepričajte, da je baterija obeh nad 30%",
"Odstranite obe slušalki in se prepričajte, da je baterija obeh nad 20%",
// 音乐列表页面
"noMusic": "Ni glasbe",

2
lib/core/translations/language/sr_rs.dart

@ -798,7 +798,7 @@ const Map<String, String> srRS = {
"charging": "Punjenje",
"updateContent": "Sadržaj ažuriranja",
"removeEarbudsEnsureBattery":
"Izvadite obe slušalice i uverite se da je baterija obe iznad 30%",
"Izvadite obe slušalice i uverite se da je baterija obe iznad 20%",
// 音乐列表页面
"noMusic": "Nema muzike",

2
lib/core/translations/language/sv_se.dart

@ -794,7 +794,7 @@ const Map<String, String> svSE = {
"charging": "Laddar",
"updateContent": "Uppdateringsinnehåll",
"removeEarbudsEnsureBattery":
"Ta ut båda hörlurarna och se till att batterinivån är över 30 % på båda",
"Ta ut båda hörlurarna och se till att batterinivån är över 20 % på båda",
// 音乐列表页面
"noMusic": "Ingen musik",

2
lib/core/translations/language/th_th.dart

@ -780,7 +780,7 @@ const Map<String, String> thTH = {
"charging": "กำลังชาร์จ",
"updateContent": "เนื้อหาการอัปเดต",
"removeEarbudsEnsureBattery":
"ถอดหูฟังทั้งสองข้างออก และตรวจสอบให้แน่ใจว่าแบตเตอรี่ทั้งสองข้างมีพลังงานมากกว่า 30%",
"ถอดหูฟังทั้งสองข้างออก และตรวจสอบให้แน่ใจว่าแบตเตอรี่ทั้งสองข้างมีพลังงานมากกว่า 20%",
// 音乐列表页面
"noMusic": "ไม่มีเพลง",

2
lib/core/translations/language/tl_ph.dart

@ -816,7 +816,7 @@ const Map<String, String> tlPH = {
"charging": "Nagcha-charge",
"updateContent": "Nilalaman ng Update",
"removeEarbudsEnsureBattery":
"Alisin ang parehong earbuds, at tiyaking parehong may 30% o higit pang baterya",
"Alisin ang parehong earbuds, at tiyaking parehong may 20% o higit pang baterya",
// 音乐列表页面
"noMusic": "Walang Musika",

2
lib/core/translations/language/tr_tr.dart

@ -796,7 +796,7 @@ const Map<String, String> trTR = {
"charging": "Şarj oluyor",
"updateContent": "Güncelleme İçeriği",
"removeEarbudsEnsureBattery":
"Her iki kulaklığı da çıkarın ve pil seviyelerinin %30'un üzerinde olduğundan emin olun",
"Her iki kulaklığı da çıkarın ve pil seviyelerinin %20'un üzerinde olduğundan emin olun",
// 音乐列表页面
"noMusic": "Müzik Yok",

2
lib/core/translations/language/uk_ua.dart

@ -804,7 +804,7 @@ const Map<String, String> ukUA = {
"charging": "Зарядка",
"updateContent": "Зміст оновлення",
"removeEarbudsEnsureBattery":
"Вийміть обидва навушники та переконайтеся, що заряд батареї в обох перевищує 30%",
"Вийміть обидва навушники та переконайтеся, що заряд батареї в обох перевищує 20%",
// 音乐列表页面
"noMusic": "Немає музики",

2
lib/core/translations/language/vi_vn.dart

@ -788,7 +788,7 @@ const Map<String, String> viVN = {
"charging": "Đang sạc",
"updateContent": "Nội dung cập nhật",
"removeEarbudsEnsureBattery":
"Lấy cả hai tai nghe ra và đảm bảo pin của cả hai đều trên 30%",
"Lấy cả hai tai nghe ra và đảm bảo pin của cả hai đều trên 20%",
// 音乐列表页面
"noMusic": "Không có nhạc",

2
lib/core/translations/language/zh_cn.dart

@ -1036,7 +1036,7 @@ const Map<String, String> zhCN = {
"doNotDisconnect": "请勿断开连接",
"charging": "充电中",
"updateContent": "更新内容",
"removeEarbudsEnsureBattery": "取出双耳机,并保证电量均为30%以上的状态",
"removeEarbudsEnsureBattery": "取出双耳机,并保证电量均为20%以上的状态",
// 音乐列表页面
"noMusic": "暂无音乐", // 暂无音乐

2
lib/core/translations/language/zh_hk.dart

@ -734,7 +734,7 @@ const Map<String, String> zhHK = {
"doNotDisconnect": "請勿斷開連接",
"charging": "充電中",
"updateContent": "更新內容",
"removeEarbudsEnsureBattery": "取出雙耳機,並保證電量均為30%以上的狀態",
"removeEarbudsEnsureBattery": "取出雙耳機,並保證電量均為20%以上的狀態",
// 音樂列表頁面
"noMusic": "暫無音樂",

70
lib/core/utils/overlay_permission_util.dart

@ -0,0 +1,70 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'logger.dart';
/// 悬浮窗权限管理工具类
class OverlayPermissionUtil {
static const MethodChannel _channel = MethodChannel('overlay_permission');
/// 检查是否有悬浮窗权限
static Future<bool> hasOverlayPermission() async {
if (Platform.isAndroid) {
try {
final bool hasPermission = await _channel.invokeMethod('hasOverlayPermission');
Logger.info('检查悬浮窗权限: $hasPermission');
return hasPermission;
} catch (e) {
Logger.error('检查悬浮窗权限失败: $e');
return false;
}
}
// iOS不需要特殊权限
return true;
}
/// 请求悬浮窗权限
static Future<bool> requestOverlayPermission() async {
if (Platform.isAndroid) {
// 首先检查是否已有权限
if (await hasOverlayPermission()) {
return true;
}
// 显示说明对话框
bool? userAgreed = await Get.dialog<bool>(
AlertDialog(
title: Text('悬浮窗权限'.tr),
content: Text('为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。'.tr),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: Text('取消'.tr),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
child: Text('去设置'.tr),
),
],
),
barrierDismissible: false,
);
if (userAgreed == true) {
try {
// 跳转系统设置页
await _channel.invokeMethod('requestOverlayPermission');
// 等待用户操作完成后重新检查权限
await Future.delayed(const Duration(seconds: 1));
return await hasOverlayPermission();
} catch (e) {
Logger.error('请求悬浮窗权限失败: $e');
return false;
}
}
return false;
}
return true;
}
}

4
lib/data/services/bluetooth_manager.dart

@ -76,7 +76,9 @@ class BluetoothManager extends GetxService {
currentDeviceRx.value = newDevice;
// 检查当前是否在OTA升级界面,如果是则不跳转
if (!_isInOtaUpgradePage() && !_bleManager.isConnected) {
if (!_isInOtaUpgradePage() &&
!_bleManager.isConnected &&
Get.currentRoute != Routes.pairing) {
Get.toNamed(Routes.pairing, arguments: newDevice);
} else {
Logger.info('当前在OTA升级界面,跳过配对页面跳转');

9
lib/data/services/network/api.dart

@ -171,4 +171,13 @@ class Api {
params: params,
);
}
//创建支付订单
static paycreateorder([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/pay_createorder',
params: params,
);
}
}

89
lib/data/services/ota_check_service.dart

@ -9,49 +9,50 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 负责管理全局OTA检查状态,确保在应用生命周期内只检查一次
class OtaCheckService extends GetxService {
static const String _tag = 'OtaCheckService';
// 全局状态管理
static bool _hasCheckedInCurrentSession = false;
static bool _isCheckingInProgress = false;
final _otaCoreService = OtaCoreService();
/// 获取服务实例
static OtaCheckService get to => Get.find<OtaCheckService>();
@override
void onInit() {
super.onInit();
Logger.d(_tag, 'OTA检查服务已初始化');
}
/// 重置检查状态(仅在应用重启时调用)
static void resetCheckStatus() {
_hasCheckedInCurrentSession = false;
_isCheckingInProgress = false;
Logger.d(_tag, 'OTA检查状态已重置');
}
/// 检查是否应该进行OTA检查
bool shouldCheckOta() {
return !_hasCheckedInCurrentSession && !_isCheckingInProgress;
}
/// 标记为已检查
void _markAsChecked() {
_hasCheckedInCurrentSession = true;
_isCheckingInProgress = false;
Logger.d(_tag, 'OTA检查状态已标记为完成');
}
/// 检查当前是否在Home页面
bool _isCurrentlyOnHomePage() {
final currentRoute = Get.currentRoute;
final isOnHome = currentRoute == Routes.home || currentRoute == Routes.initial;
final isOnHome =
currentRoute == Routes.home || currentRoute == Routes.initial;
Logger.d(_tag, '当前路由: $currentRoute, 是否在Home页面: $isOnHome');
return isOnHome;
}
/// 执行OTA升级检查
/// [isFullyConnected] 设备是否完全连接
/// 返回是否成功执行了检查
@ -62,53 +63,54 @@ class OtaCheckService extends GetxService {
Logger.d(_tag, '跳过OTA检查 - 已检查过或正在检查中');
return false;
}
// 检查设备连接状态
if (!isFullyConnected) {
Logger.d(_tag, '设备未完全连接,跳过OTA检查');
return false;
}
// 检查当前页面
if (!_isCurrentlyOnHomePage()) {
Logger.d(_tag, '当前不在Home页面,跳过OTA检查');
return false;
}
// 标记为检查中
_isCheckingInProgress = true;
Logger.d(_tag, '开始执行OTA固件升级检查...');
// 获取当前版本信息
final currentInfo = await _otaCoreService.getCurrentVersionInfo();
final currentVersion = currentInfo['currentVersion'] as String;
// 获取最新版本信息
final latestInfo = await _otaCoreService.getLatestVersionInfo();
final latestVersion = latestInfo['latestVersion'] as String;
Logger.d(_tag, '当前版本: $currentVersion, 最新版本: $latestVersion');
// 检查是否有更新
final hasUpdate = _otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
final hasUpdate =
_otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
if (hasUpdate) {
Logger.d(_tag, '发现新版本,准备显示升级提醒');
// 再次确认当前在Home页面(防止异步执行期间页面切换)
if (_isCurrentlyOnHomePage()) {
_showOtaUpgradeDialog(latestVersion, latestInfo['updateDescription'] as String?);
_showOtaUpgradeDialog(
latestVersion, latestInfo['updateDescription'] as String?);
} else {
Logger.d(_tag, '检查完成时已不在Home页面,取消显示弹窗');
}
} else {
Logger.d(_tag, '当前已是最新版本');
}
// 标记为已检查(无论是否有更新都标记为已检查)
_markAsChecked();
return true;
} catch (e) {
Logger.e(_tag, '检查OTA升级失败: $e');
// 即使检查失败,也标记为已检查,避免重复尝试
@ -116,7 +118,7 @@ class OtaCheckService extends GetxService {
return false;
}
}
/// 显示OTA升级提醒对话框
/// [latestVersion] 最新版本号
/// [updateDescription] 更新描述
@ -126,24 +128,32 @@ class OtaCheckService extends GetxService {
Logger.d(_tag, '显示弹窗时发现不在Home页面,取消显示');
return;
}
final isDarkMode = Get.isDarkMode;
Logger.d(_tag, '显示OTA升级提醒对话框');
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(
Icons.headset,
color: Colors.blue[600],
color: isDarkMode ? const Color(0xFF0066CC) : Colors.blue[600],
size: 24.sp,
),
SizedBox(width: 8.w),
Text(
'ota_newVersionFound'.tr,
style: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w600,
// 使用Flexible确保标题文本能够自适应不同语言长度,避免溢出
Flexible(
child: Text(
'ota_newVersionFound'.tr, // 发现新版本
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.blue[700],
fontWeight: FontWeight.w600,
fontSize: 20.sp, // 设置合适的字体大小
),
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
maxLines: 2, // 最多显示两行
),
),
],
@ -155,6 +165,7 @@ class OtaCheckService extends GetxService {
Text(
'${'latestFirmwareVersion'.tr}: $latestVersion',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontWeight: FontWeight.w500,
fontSize: 16.sp,
),
@ -173,7 +184,9 @@ class OtaCheckService extends GetxService {
},
child: Text(
'talkLater'.tr,
style: TextStyle(color: Colors.grey[600]),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
// 立即升级 (靠右)
@ -184,11 +197,15 @@ class OtaCheckService extends GetxService {
Get.toNamed(Routes.OTA_UPGRADE);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
backgroundColor:
isDarkMode ? const Color(0xFF0066CC) : Colors.blue[600],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.sp),
),
side: isDarkMode
? BorderSide(color: Colors.grey[800]!, width: 1)
: BorderSide.none,
),
child: Text(
'ota_updateNow'.tr,
@ -201,12 +218,12 @@ class OtaCheckService extends GetxService {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.sp),
),
backgroundColor: Colors.white,
backgroundColor: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
),
barrierDismissible: true, // 允许点击外部关闭
);
}
/// 获取当前检查状态(用于调试)
Map<String, dynamic> getCheckStatus() {
return {

362
lib/data/services/pay_service.dart

@ -0,0 +1,362 @@
import 'package:get/get.dart';
import 'package:voitrans/data/services/network/api.dart';
import 'package:tobias/tobias.dart';
import '../../modules/login/controllers/weixinLogin.dart';
typedef PaymentResultCallback = void Function(bool success, String message);
/// 支付服务
class PayService extends GetxService {
late FlWX _flwx;
late Tobias _tobias; // 添加 Tobias 实例
@override
void onInit() {
super.onInit();
//微信
FlWX().register(
appSecret: '9ceb39df312b3ba95a229b2aa0d15496',
appId: 'wx4b5ec8957ad5e425',
partnerId: '1719983673',
universalLink: 'https://voitrans.net/app/',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
_flwx = FlWX();
// 初始化支付宝SDK
_tobias = Tobias(); // 初始化 Tobias 实例
_initAlipay();
}
/// 初始化支付宝SDK
Future<void> _initAlipay() async {
try {
// 检查支付宝是否安装 - 修复用法
final isInstalled = await _tobias.isAliPayInstalled;
print('支付宝是否安装: $isInstalled');
} catch (e) {
print('初始化支付宝SDK失败: $e');
}
}
/// 查询支付结果
///
/// [orderId] 订单ID
/// 返回支付状态:success, failed, pending
Future<String?> queryPaymentStatus(String orderId) async {
try {
// TODO: 调用后端API查询支付状态
// final response = await Api.queryPaymentStatus({'orderId': orderId});
// return response['status'] as String?;
// 临时返回,实际使用时需要删除
return 'pending';
} catch (e) {
print('查询支付状态失败: $e');
return null;
}
}
/// 微信支付
///
/// [paymentData] 支付参数,包含以下字段:
/// - appId: 应用ID
/// - partnerId: 商户号
/// - prepayId: 预支付交易会话ID
/// - packageValue: 扩展字段,固定值"Sign=WXPay"
/// - nonceStr: 随机字符串
/// - timestamp: 时间戳
/// - sign: 签名
///
/// [onResult] 支付结果回调
Future<bool> wechatPay({
required Map<String, dynamic> paymentData,
PaymentResultCallback? onResult,
}) async {
try {
// 检查微信是否安装
if (!await _flwx.isInstalled) {
onResult?.call(false, '微信未安装');
return false;
}
// 将支付数据转换为Payment对象
final payment = paymentData.toPayment();
if (payment == null) {
onResult?.call(false, '支付参数格式错误');
return false;
}
// 调用微信支付
final result = await _flwx.pay(
payment,
onSuccess: () {
onResult?.call(true, '支付成功');
},
onResponse: (response) {
if (response.errCode == 0) {
onResult?.call(true, '支付成功');
} else if (response.errCode == -2) {
onResult?.call(false, '用户取消支付');
} else {
onResult?.call(false, '支付失败: ${response.errStr}');
}
},
);
return result;
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 创建微信支付订单
///
/// [productId] 商品ID
/// [productName] 商品名称
/// [productDesc] 商品描述
/// [amount] 支付金额(分)
/// [paymentMethod] 支付方式(1=微信支付)
/// [clientIp] 客户端IP(可选)
/// [notifyUrl] 通知URL(可选)
/// [extraData] 扩展数据(可选)
///
/// 返回支付参数,用于调用 [wechatPay] 方法
Future<Map<String, dynamic>?> createWechatPayOrder({
required String productId,
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 4, // 1表示微信支付
String? clientIp,
String? notifyUrl,
String? extraData,
}) async {
try {
// 调用后端API创建支付订单
final response = await Api.paycreateorder({
'product_id': productId,
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
});
print('创建支付订单: $response');
// 检查响应是否成功
if (response != null) {
final pay_params = response["pay_params"] as Map<String, dynamic>;
// 返回微信支付所需的参数
return {
'appId': pay_params['appid'],
'partnerId': pay_params['partnerid'],
'prepayId': pay_params['prepayid'],
'packageValue': pay_params['package'] ?? 'Sign=WXPay',
'nonceStr': pay_params['noncestr'],
'timestamp': pay_params['timestamp'],
'sign': pay_params['sign'],
};
} else {
print('创建支付订单失败: ${response?['message'] ?? '未知错误'}');
return null;
}
} catch (e) {
print('创建支付订单异常: $e');
return null;
}
}
Future<bool> processWechatPayment({
required String productId,
required int amount,
required String description,
String? userId,
PaymentResultCallback? onResult,
}) async {
try {
// 1. 创建支付订单
final paymentData = await createWechatPayOrder(
productId: productId,
productName: '会员充值',
productDesc: '1个月会员服务',
amount: 1, // 29.99元,单位:分
paymentMethod: 4, // 微信支付
notifyUrl: 'https://api.ideapsound.com/api/home/pay_wechatnotify',
);
if (paymentData == null) {
onResult?.call(false, '创建支付订单失败');
return false;
}
// 2. 调用微信支付
return await wechatPay(
paymentData: paymentData,
onResult: onResult,
);
} catch (e) {
onResult?.call(false, '支付流程异常: $e');
return false;
}
}
/// 完整的支付宝支付流程
///
/// [productId] 产品ID
/// [amount] 支付金额(分)
/// [description] 商品描述
/// [onResult] 支付结果回调
Future<bool> processAlipayPayment({
required String productId,
required int amount,
required String description,
PaymentResultCallback? onResult,
}) async {
try {
// 检查支付宝是否安装
final isInstalled = await _tobias.isAliPayInstalled;
if (!isInstalled) {
onResult?.call(false, '支付宝未安装');
return false;
}
// 调用现有的 createAlipayOrder 方法创建订单
final orderInfo = await createAlipayOrder(
productId: productId,
productName: description,
productDesc: description,
amount: amount,
paymentMethod: 3, // 支付宝支付
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
);
if (orderInfo == null) {
onResult?.call(false, '创建支付订单失败');
return false;
}
print('支付宝支付订单: $orderInfo');
// 调用支付宝支付
final result = await _tobias.pay(orderInfo);
print('支付宝支付结果: $result');
// 处理支付结果
if (result != null) {
final resultStatus = result['resultStatus'] as String?;
final memo = result['memo'] as String?;
if (resultStatus == '9000') {
onResult?.call(true, '支付成功');
return true;
} else {
onResult?.call(false, memo ?? '支付失败');
return false;
}
} else {
onResult?.call(false, '支付结果为空');
return false;
}
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 支付宝支付
///
/// [orderInfo] 支付宝订单信息字符串
/// [onResult] 支付结果回调
Future<bool> alipay({
required String orderInfo,
PaymentResultCallback? onResult,
}) async {
try {
// 检查支付宝是否安装 - 使用实例方法
final isInstalled = await _tobias.isAliPayInstalled;
if (!isInstalled) {
onResult?.call(false, '支付宝未安装');
return false;
}
// 调用支付宝支付 - 使用实例方法
final result = await _tobias.pay(orderInfo);
if (result != null) {
// 解析支付结果
final resultStatus = result['resultStatus'] as String?;
final memo = result['memo'] as String?;
if (resultStatus == '9000') {
// 支付成功
onResult?.call(true, '支付成功');
return true;
} else {
// 支付失败或取消
onResult?.call(false, memo ?? '支付失败');
return false;
}
} else {
onResult?.call(false, '支付结果为空');
return false;
}
} catch (e) {
onResult?.call(false, '支付异常: $e');
return false;
}
}
/// 创建支付宝支付订单
///
/// [productId] 商品ID
/// [productName] 商品名称
/// [productDesc] 商品描述
/// [amount] 支付金额(分)
/// [paymentMethod] 支付方式(2=支付宝)
/// [clientIp] 客户端IP(可选)
/// [notifyUrl] 通知URL(可选)
/// [extraData] 扩展数据(可选)
///
/// 返回支付宝订单信息字符串,用于调用 [alipay] 方法
Future<String?> createAlipayOrder({
required String productId,
required String productName,
String? productDesc,
required int amount,
int paymentMethod = 2, // 2表示支付宝
String? clientIp,
String? notifyUrl,
String? extraData,
}) async {
try {
// 调用后端API创建支付订单
final response = await Api.paycreateorder({
'product_id': productId,
'product_name': productName,
'product_desc': productDesc ?? '',
'amount': amount,
'payment_method': paymentMethod,
'client_ip': clientIp ?? '',
'notify_url': notifyUrl ?? '',
'extra_data': extraData ?? '',
});
print('创建支付宝订单: $response');
// 检查响应是否成功
if (response != null && response['alipay_pay_params'] != null) {
// 返回支付宝订单信息字符串
return response['alipay_pay_params'] as String?;
} else {
print('创建支付宝订单失败: ${response?['message'] ?? '未知错误'}');
return null;
}
} catch (e) {
print('创建支付宝订单异常: $e');
return null;
}
}
}

199
lib/main.dart

@ -4,34 +4,104 @@ import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart';
import 'routes/app_pages.dart';
import 'routes/app_routes.dart';
import 'core/translations/app_translations.dart';
import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart';
import '../../data/services/login_service.dart';
import 'core/bindings/initial_binding.dart';
import 'data/services/login_service.dart';
import 'dart:ui' as ui;
import 'modules/splash/controllers/splash_controller.dart';
void main() async {
// 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// 初始化存储 - 在创建应用之前初始化
// 初始化存储
await GetStorage.init();
// 初始化环境变量
await dotenv.load();
// 加载环境变量
await dotenv.load(fileName: ".env");
print("环境变量加载成功");
// 初始化登录服务
Get.put(LoginService());
// 延迟初始化深链接处理器,确保Flutter完全准备好
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 1500), () {
_initDeepLinkHandler();
});
});
runApp(MainApp());
}
// 初始化深链接处理器
void _initDeepLinkHandler() {
print("🔗 初始化深链接处理器");
const platform = MethodChannel('com.saitong.voitrans/deeplink');
// 初始化登录服务(包含Firebase初始化)
LoginService();
platform.setMethodCallHandler((call) async {
print("🔗 Flutter 收到深链接调用: ${call.method}");
// 运行应用
runApp(const MainApp());
if (call.method == 'handleDeepLink') {
final arguments = call.arguments as Map<dynamic, dynamic>;
print("🔗 深链接参数: $arguments");
final url = arguments['url'] as String?;
if (url != null) {
print("🔗 处理深链接 URL: $url");
await _handleDeepLinkNavigation(url);
}
}
});
print("✅ 深链接处理器初始化完成");
}
// 处理深链接导航
Future<void> _handleDeepLinkNavigation(String url) async {
print("🔗 开始处理深链接导航: $url");
try {
final uri = Uri.parse(url);
print("🔗 解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}");
if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') {
print("🔗 检测到QQ音乐登录回调");
// 等待SplashController注册
int retryCount = 0;
const maxRetries = 10;
while (retryCount < maxRetries) {
try {
if (Get.isRegistered<SplashController>()) {
print("🔗 找到SplashController,处理QQ音乐登录");
final controller = Get.find<SplashController>();
controller.handleQQMusicLogin();
return;
}
} catch (e) {
print("🔗 获取SplashController失败: $e");
}
retryCount++;
print("🔗 等待SplashController注册 ($retryCount/$maxRetries)");
await Future.delayed(const Duration(milliseconds: 500));
}
print("❌ 超时等待SplashController注册");
}
} catch (e) {
print("❌ 处理深链接时出错: $e");
}
}
class MainApp extends StatelessWidget {
@ -39,48 +109,71 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取保存的主题设置
// 在应用构建时立即初始化深链接处理
_initDeepLinkHandler();
// 获取保存的主题设置
final themeMode = ThemeService().getThemeMode();
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return GetMaterialApp(
title: 'Voitrans',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode, // 使用提前获取的主题模式
initialBinding: InitialBinding(),
initialRoute: Routes.splash,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: AppTranslations.deviceLocale(),
fallbackLocale: AppTranslations.getSmartFallbackLocale(),
supportedLocales: AppTranslations.supportedLocales,
builder: EasyLoading.init(
builder: (BuildContext context, Widget? widget) {
EasyLoading.instance.userInteractions = false;
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
);
},
),
);
},
);
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, child) {
return GetMaterialApp(
title: 'Voitrans',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: themeMode, // 使用提前获取的主题模式
initialBinding: InitialBinding(),
initialRoute: Routes.splash,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: AppTranslations.deviceLocale(),
fallbackLocale: AppTranslations.getSmartFallbackLocale(),
supportedLocales: AppTranslations.supportedLocales,
builder: EasyLoading.init(
builder: (BuildContext context, Widget? widget) {
EasyLoading.instance.userInteractions = false;
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
);
},
),
);
});
// // 使用 ScreenUtilInit 包装整个应用
// return ScreenUtilInit(
// designSize: const Size(375, 812), // 设计稿尺寸,根据你的设计稿调整
// minTextAdapt: true,
// splitScreenMode: true,
// builder: (context, child) {
// return GetMaterialApp(
// title: 'VoiTrans',
// theme: AppTheme.lightTheme,
// darkTheme: AppTheme.darkTheme,
// themeMode: ThemeService().getThemeMode(),
// initialRoute: Routes.splash,
// getPages: AppPages.pages,
// initialBinding: InitialBinding(),
// translations: AppTranslations(),
// locale: Get.deviceLocale,
// fallbackLocale: const Locale('en', 'US'),
// debugShowCheckedModeBanner: false,
// );
// },
// );
}
}

33
lib/modules/agent/controllers/agent_controller.dart

@ -706,7 +706,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// messages.refresh();
}
Get.snackbar('error'.tr, '${event.data['message']}');
// Get.snackbar('error'.tr, '${event.data['message']}');
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
@ -795,12 +795,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
orElse: () => "",
);
if (musicKey.isNotEmpty) {
// final qqmusic_search_songs =
// metaResult['qqmusic_searchandplay_songs']
// as Map<String, dynamic>;
// final data = qqmusic_search_songs['data'];
// if (data != null && data is List) {
Future.delayed(Duration(seconds: 2), () {
Future.delayed(Duration(seconds: 1), () {
_qqmusicManager.syncCurrentPlayInfo(isOpenPlay: true);
});
// }
@ -810,6 +805,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
if (maps_route_navigation is Map<String, dynamic>) {
final card_navigation =
maps_route_navigation["card_navigation"];
final isinternal = metaResult["isinternal"] ?? false;
if (card_navigation is Map<String, dynamic>) {
//导航
final start = card_navigation["start"] as String;
@ -826,9 +822,15 @@ class AgentController extends GetxController with WidgetsBindingObserver {
Future.delayed(Duration(seconds: 2), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
// 2秒后执行的代码
openNavigation(false, start, destination, end_latitude,
end_longitude, "");
final navigationMode =
AppConfig.env('APP_NAVIGATION_MODE') ??
'external_auto';
if (navigationMode == 'internal_gaode' || isinternal) {
_navigationManager.open(
start, "$end_longitude,$end_latitude", "driving");
}
// openNavigation(false, start, destination, end_latitude,
// end_longitude, "");
});
}
}
@ -2084,12 +2086,13 @@ class AgentController extends GetxController with WidgetsBindingObserver {
return;
}
}
// 5. 最后尝试内置高德导航
_navigationManager.open(start, "$endlongitude,$endlatitude", mode);
// 5. 如果都没有安装,提示用户
print('未找到可用的导航应用');
final availableApps =
Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图';
Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps');
// print('未找到可用的导航应用');
// final availableApps =
// Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图';
// Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps');
} catch (e) {
print('自动选择导航失败: $e');
Get.snackbar('错误', '启动导航失败');

4
lib/modules/agent/views/message_bubble.dart

@ -236,9 +236,9 @@ class MessageBubble extends GetView<AgentController> {
//音乐播放列表
return buildMusicContent(context, playlist);
}
} else if (meta!.keys.contains('qqmusic_searchandplay_songs')) {
} else if (meta!.keys.contains('qqmusic_search_songs')) {
final qqmusic_search_songs =
meta!['qqmusic_searchandplay_songs'] as Map<String, dynamic>;
meta!['qqmusic_search_songs'] as Map<String, dynamic>;
final data = qqmusic_search_songs['data'];
if (data != null && data is List) {
//qq音乐

9
lib/modules/login/controllers/weixinLogin.dart

@ -73,6 +73,11 @@ class FlWX {
/// 获取当前设置的 appSecret
String? get appSecret => _appSecret;
String? _partnerId;
/// 获取当前设置的 partnerId
String? get partnerId => _partnerId;
String _appName = '';
String token = '';
String code = '';
@ -109,6 +114,9 @@ class FlWX {
/// 用于获取用户信息
String? appSecret,
/// 商户号
String? partnerId,
/// app名称(用于分享时使用)
String appName = '',
@ -123,6 +131,7 @@ class FlWX {
_isRegister = true;
_appId = appId;
_appSecret = appSecret;
_partnerId = partnerId;
_appName = appName;
_shareThumbnail = shareThumbnail;
_params = params;

6
lib/modules/ota_upgrade/controllers/ota_core_contropller.dart

@ -188,8 +188,8 @@ class OtaCoreService {
final leftBattery = deviceInfo.leftEarBattery;
final rightBattery = deviceInfo.rightEarBattery;
// 检查左右耳电量是否都大于30%
if (leftBattery <= 30 || rightBattery <= 30) {
// 检查左右耳电量是否都大于20%
if (leftBattery <= 20 || rightBattery <= 20) {
_showLowBatteryDialog(leftBattery, rightBattery);
return false;
}
@ -206,7 +206,7 @@ class OtaCoreService {
/// [rightBattery] 右耳电量
void _showLowBatteryDialog(int leftBattery, int rightBattery) {
String message = '设备电量不足,无法进行固件升级\n';
message += '升级要求:左右耳电量均需大于30%\n';
message += '升级要求:左右耳电量均需大于20%\n';
message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n';
message += '请充电后再试';

2
lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart

@ -80,7 +80,7 @@ class OtaUpgradePopUp {
ElevatedButton(
onPressed: () {
Get.back();
Get.toNamed(Routes.pairing);
Get.offAllNamed(Routes.pairing);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],

2
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -925,7 +925,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
SizedBox(width: 12.w),
Expanded(
child: Text(
'removeEarbudsEnsureBattery'.tr, //取出双耳机,并保证电量均为30%以上的状态
'removeEarbudsEnsureBattery'.tr, //取出双耳机,并保证电量均为20%以上的状态
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode

50
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,20 +1,25 @@
import 'dart:io';
import 'dart:async';
import 'package:app_settings/app_settings.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
import '/data/services/network/api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart';
import 'dart:io';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
import '/data/services/network/api.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
import '../../../data/services/version_update_service.dart';
/// 配对状态枚举 - 简化版本
enum PairingState {
@ -48,6 +53,7 @@ enum ErrorType {
class PairingController extends GetxController with WidgetsBindingObserver {
static const String _tag = 'PairingCtrl';
static PairingController get to => Get.find();
final _storage = GetStorage();
// ========== 状态管理 ==========
final _currentState = PairingState.idle.obs;
@ -96,6 +102,12 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// Public getter for view access
BluetoothDevice? get currentDevice => _currentClassicDevice;
//华为设备权限检查标记
bool isHuaweiHasChecked = false;
// 缓存的设备品牌信息
final versionService = Get.find<VersionUpdateService>();
bool isHuaweiDevice = false;
@override
void onInit() {
super.onInit();
@ -106,6 +118,8 @@ class PairingController extends GetxController with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this);
Logger.i(_tag, '应用生命周期监听器注册完成');
isHuaweiDevice = _isHuaweiDevice();
// 确保服务已初始化
_bluetoothManager = Get.find<BluetoothManager>();
_bleManager = Get.find<BleManager>();
@ -113,6 +127,11 @@ class PairingController extends GetxController with WidgetsBindingObserver {
// 集中设置所有监听器
_setupAllListeners();
if (isHuaweiDevice) {
//检查华为设备是否检查权限检查过
isHuaweiHasChecked = _storage.read('isHuaweiHasChecked') ?? false;
}
// 开始初始状态检查
// 延迟稍长时间,确保BluetoothManager有足够时间获取已连接设备
Future.delayed(const Duration(milliseconds: 300), () {
@ -548,8 +567,26 @@ class PairingController extends GetxController with WidgetsBindingObserver {
return _bluetoothManager.bluetoothState != 'off';
}
/// 检查是否为华为设备
bool _isHuaweiDevice() {
final deviceBrand = versionService.cachedDeviceBrand;
return deviceBrand.isNotEmpty &&
(deviceBrand.contains('huawei') || deviceBrand.contains('华为'));
}
/// 检查权限
Future<bool> _checkPermissions() async {
//如果是华为设备,且已经检查过,直接返回权限状态
if (isHuaweiDevice && isHuaweiHasChecked) {
final bluetoothScanStatus = await Permission.bluetoothScan.status;
final bluetoothConnectStatus = await Permission.bluetoothConnect.status;
final locationStatus = await Permission.locationWhenInUse.status;
return bluetoothScanStatus.isGranted &&
bluetoothConnectStatus.isGranted &&
locationStatus.isGranted;
}
// Android权限检查
if (Platform.isAndroid) {
Logger.i(_tag, '开始检查Android权限');
@ -575,6 +612,9 @@ class PairingController extends GetxController with WidgetsBindingObserver {
permanentDenialText: 'locationPermanentDenial'.tr,
);
if (isHuaweiDevice) {
_storage.write('isHuaweiHasChecked', true);
}
return bluetoothScanGranted && bluetoothConnectGranted && locationGranted;
}
return true;

84
lib/modules/pairing/views/pairing_view.dart

@ -72,56 +72,60 @@ class _PairingViewState extends State<PairingView>
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
body: SafeArea(
child: _buildMainContent(isDarkMode),
child: Column(
children: [
// 固定顶部区域 - 不受下方内容影响
_buildFixedTopSection(isDarkMode),
// 可滚动的动态内容区域
Expanded(
child: _buildDynamicContentSection(isDarkMode),
),
],
),
),
// bottomNavigationBar: Opacity(
// opacity: 0.5,
// child: const CommonBottomNav(currentIndex: 0),
// ),
);
}
// 主要内容
Widget _buildMainContent(bool isDarkMode) {
return Container(
width: double.infinity,
height: MediaQuery.of(Get.context!).size.height,
padding: EdgeInsets.symmetric(horizontal: 24.w),
// 添加SingleChildScrollView以防止溢出
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 20.h), // 顶部间距
// 标题部分
_buildHeader(isDarkMode),
SizedBox(height: 20.h),
// 固定顶部区域
Widget _buildFixedTopSection(bool isDarkMode) {
return Padding(
padding: EdgeInsets.only(top: 60.h, left: 24.w, right: 24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 标题部分
_buildHeader(isDarkMode),
SizedBox(height: 20.h),
// 设备动画 - 根据状态显示不同动画
_buildDeviceAnimation(isDarkMode),
_buildDeviceAnimation(isDarkMode),
],
),
);
}
SizedBox(height: 20.h),
// 动态内容区域
Widget _buildDynamicContentSection(bool isDarkMode) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 30.h),
// 状态信息
_buildStatusInfo(isDarkMode),
// 状态信息
_buildStatusInfo(isDarkMode),
SizedBox(height: 40.h),
SizedBox(height: 40.h),
// 底部按钮
_buildActionButtons(isDarkMode),
// 底部按钮
_buildActionButtons(isDarkMode),
SizedBox(height: 20.h),
],
),
SizedBox(height: 20.h),
],
),
),
);

9
lib/modules/payment/bindings/payment_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/payment_controller.dart';
class PaymentBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<PaymentController>(() => PaymentController());
}
}

143
lib/modules/payment/controllers/payment_controller.dart

@ -0,0 +1,143 @@
import 'package:get/get.dart';
import '../../../data/services/pay_service.dart';
class PaymentController extends GetxController {
final PayService _payService = Get.find<PayService>();
// 支付状态
final RxBool isLoading = false.obs;
final RxString paymentStatus = ''.obs;
final RxString errorMessage = ''.obs;
// 测试订单信息
final RxString testProductId = 'vip_101'.obs;
final RxDouble testAmount = 0.01.obs; // 测试金额 1分钱
// 支付方式选择
final RxInt selectedPaymentMethod = 1.obs; // 1=微信支付, 2=支付宝支付
@override
void onInit() {
super.onInit();
// 生成测试订单ID
// testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}';
}
/// 测试微信支付
Future<void> testWechatPayment() async {
try {
isLoading.value = true;
paymentStatus.value = '正在发起微信支付...';
errorMessage.value = '';
// 调用支付服务,使用回调处理结果
final launched = await _payService.processWechatPayment(
productId: testProductId.value,
amount: (testAmount.value * 100).toInt(), // 转换为分
description: '微信支付测试',
onResult: (success, message) {
// 支付结果回调
isLoading.value = false;
if (success) {
paymentStatus.value = '微信支付成功!';
errorMessage.value = '';
Get.snackbar(
'支付成功',
message ?? '微信支付测试完成',
snackPosition: SnackPosition.TOP,
);
} else {
paymentStatus.value = '微信支付失败';
errorMessage.value = message ?? '未知错误';
Get.snackbar(
'支付失败',
message ?? '未知错误',
snackPosition: SnackPosition.TOP,
);
}
},
);
if (!launched) {
// 如果支付没有成功启动
isLoading.value = false;
paymentStatus.value = '微信支付启动失败';
errorMessage.value = '无法启动微信支付';
Get.snackbar('支付启动失败', '无法启动微信支付');
}
} catch (e) {
isLoading.value = false;
paymentStatus.value = '微信支付异常';
errorMessage.value = e.toString();
Get.snackbar('支付异常', e.toString());
}
}
/// 测试支付宝支付
Future<void> testAlipayPayment() async {
try {
isLoading.value = true;
paymentStatus.value = '正在发起支付宝支付...';
errorMessage.value = '';
// 调用支付宝支付服务
final launched = await _payService.processAlipayPayment(
productId: testProductId.value, // 这里传递的是产品ID
amount: (testAmount.value * 100).toInt(), // 转换为分
description: '支付宝支付测试',
onResult: (success, message) {
// 支付结果回调
isLoading.value = false;
if (success) {
paymentStatus.value = '支付宝支付成功!';
errorMessage.value = '';
} else {
paymentStatus.value = '支付宝支付失败';
errorMessage.value = message;
}
},
);
if (!launched) {
// 如果支付没有成功启动
isLoading.value = false;
paymentStatus.value = '支付宝支付启动失败';
errorMessage.value = '无法启动支付宝支付';
Get.snackbar('支付启动失败', '无法启动支付宝支付');
}
} catch (e) {
isLoading.value = false;
paymentStatus.value = '支付宝支付异常';
errorMessage.value = e.toString();
Get.snackbar('支付异常', e.toString());
}
}
/// 根据选择的支付方式进行支付
Future<void> processPayment() async {
if (selectedPaymentMethod.value == 1) {
await testWechatPayment();
} else if (selectedPaymentMethod.value == 2) {
await testAlipayPayment();
}
}
/// 选择支付方式
void selectPaymentMethod(int method) {
selectedPaymentMethod.value = method;
}
/// 重置状态
void resetStatus() {
paymentStatus.value = '';
errorMessage.value = '';
// testProductId.value = 'test_${DateTime.now().millisecondsSinceEpoch}';
}
/// 更新测试金额
void updateTestAmount(double amount) {
testAmount.value = amount;
}
}

506
lib/modules/payment/views/payment_view.dart

@ -0,0 +1,506 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/payment_controller.dart';
class PaymentView extends GetView<PaymentController> {
const PaymentView({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: 18.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,
),
onPressed: () => Get.back(),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTestInfoCard(isDarkMode),
SizedBox(height: 20.h),
_buildPaymentMethodSelector(isDarkMode),
SizedBox(height: 20.h),
_buildAmountSelector(isDarkMode),
SizedBox(height: 20.h),
_buildPaymentButton(isDarkMode),
SizedBox(height: 20.h),
_buildStatusCard(isDarkMode),
SizedBox(height: 20.h),
_buildInstructions(isDarkMode),
],
),
),
);
}
/// 测试信息卡片
Widget _buildTestInfoCard(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: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 12.h),
Obx(() => _buildInfoRow(
'商品ID', controller.testProductId.value, isDarkMode)),
SizedBox(height: 8.h),
_buildInfoRow('商品描述', '微信支付测试', isDarkMode),
SizedBox(height: 8.h),
Obx(() => _buildInfoRow(
'支付金额',
'¥${controller.testAmount.value.toStringAsFixed(2)}',
isDarkMode)),
],
),
);
}
/// 信息行
Widget _buildInfoRow(String label, String value, bool isDarkMode) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80.w,
child: Text(
'$label:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
],
);
}
/// 金额选择器
Widget _buildAmountSelector(bool isDarkMode) {
final amounts = [0.01, 0.10, 1.00, 10.00];
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: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 12.h),
Wrap(
spacing: 12.w,
runSpacing: 8.h,
children: amounts.map((amount) {
return Obx(() => GestureDetector(
onTap: () => controller.updateTestAmount(amount),
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: controller.testAmount.value == amount
? Colors.blue[600]
: (isDarkMode
? Colors.grey[800]
: Colors.grey[200]),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
'¥${amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 14.sp,
color: controller.testAmount.value == amount
? Colors.white
: (isDarkMode ? Colors.white : Colors.black),
),
),
),
));
}).toList(),
),
],
),
);
}
/// 支付方式选择器
Widget _buildPaymentMethodSelector(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: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
SizedBox(height: 16.h),
// 移除外层的 Obx,因为内部的 _buildPaymentMethodOption 已经有 Obx 了
Column(
children: [
_buildPaymentMethodOption(
method: 1,
title: '微信支付',
icon: Icons.wechat,
iconColor: Colors.green,
isDarkMode: isDarkMode,
),
SizedBox(height: 12.h),
_buildPaymentMethodOption(
method: 2,
title: '支付宝支付',
icon: Icons.account_balance_wallet,
iconColor: Colors.blue,
isDarkMode: isDarkMode,
),
],
),
],
),
);
}
/// 支付方式选项
Widget _buildPaymentMethodOption({
required int method,
required String title,
required IconData icon,
required Color iconColor,
required bool isDarkMode,
}) {
return Obx(() => GestureDetector(
onTap: () => controller.selectPaymentMethod(method),
child: Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: controller.selectedPaymentMethod.value == method
? (isDarkMode
? Colors.blue.withOpacity(0.2)
: Colors.blue.withOpacity(0.1))
: (isDarkMode ? Colors.grey[800] : Colors.grey[100]),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: controller.selectedPaymentMethod.value == method
? Colors.blue
: Colors.transparent,
width: 1.5,
),
),
child: Row(
children: [
Icon(
icon,
color: iconColor,
size: 24.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
if (controller.selectedPaymentMethod.value == method)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.sp,
),
],
),
),
));
}
/// 支付按钮
Widget _buildPaymentButton(bool isDarkMode) {
return Obx(() => Container(
width: double.infinity,
height: 50.h,
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: () {
controller.processPayment();
},
style: ElevatedButton.styleFrom(
backgroundColor: controller.isLoading.value
? Colors.grey
: (controller.selectedPaymentMethod.value == 1
? Colors.green
: Colors.blue),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 2,
),
child: controller.isLoading.value
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 12.w),
Text(
'支付中...',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
controller.selectedPaymentMethod.value == 1
? Icons.wechat
: Icons.account_balance_wallet,
color: Colors.white,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
controller.selectedPaymentMethod.value == 1
? '微信支付 ¥${controller.testAmount.value.toStringAsFixed(2)}'
: '支付宝支付 ¥${controller.testAmount.value.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
));
}
/// 状态卡片
Widget _buildStatusCard(bool isDarkMode) {
return Obx(() {
if (controller.paymentStatus.value.isEmpty) {
return const SizedBox.shrink();
}
final isError = controller.errorMessage.value.isNotEmpty;
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isError
? (isDarkMode ? Colors.red[900] : Colors.red[50])
: (isDarkMode ? Colors.green[900] : Colors.green[50]),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isError ? Colors.red[300]! : Colors.green[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isError ? Icons.error : Icons.check_circle,
color: isError ? Colors.red[600] : Colors.green[600],
size: 20.w,
),
SizedBox(width: 8.w),
Text(
'支付状态',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isError ? Colors.red[600] : Colors.green[600],
),
),
],
),
SizedBox(height: 8.h),
Text(
controller.paymentStatus.value,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
if (controller.errorMessage.value.isNotEmpty) ...[
SizedBox(height: 8.h),
Text(
'错误信息: ${controller.errorMessage.value}',
style: TextStyle(
fontSize: 12.sp,
color: Colors.red[600],
),
),
],
SizedBox(height: 12.h),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: controller.resetStatus,
style: OutlinedButton.styleFrom(
side: BorderSide(
color: isError ? Colors.red[600]! : Colors.green[600]!,
),
),
child: Text(
'重新测试',
style: TextStyle(
color: isError ? Colors.red[600] : Colors.green[600],
),
),
),
),
],
),
);
});
}
/// 使用说明
Widget _buildInstructions(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.blue[50],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.blue[200]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Colors.blue[600],
size: 20.w,
),
SizedBox(width: 8.w),
Text(
'使用说明',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.blue[600],
),
),
],
),
SizedBox(height: 12.h),
Text(
'1. 选择测试金额(建议使用0.01元进行测试)\n'
'2. 点击"发起微信支付"按钮\n'
'3. 系统将调用微信支付SDK\n'
'4. 在微信中完成支付流程\n'
'5. 返回应用查看支付结果\n\n'
'注意:这是测试环境,实际不会产生真实扣费',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
height: 1.5,
),
),
],
),
);
}
}

2
lib/modules/permissions/controllers/permissions_controller.dart

@ -184,7 +184,7 @@ class PermissionsController extends GetxController {
);
if (hasPermission) {
_showSuccessMessage();
// _showSuccessMessage();
if (Platform.isIOS && permission == Permission.calendarFullAccess) {
if (_isCalendarFullAccess == null) {
_storage.write('record_permission_calendar', true);

19
lib/modules/profile/views/profile_view.dart

@ -750,11 +750,11 @@ class EditProfileView extends GetView<ProfileController> {
title: 'clean'.tr, // 清理缓存
icon: Icons.cleaning_services,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[50]!,
iconColor:
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!,
titleColor: isDarkMode ? Colors.white : Colors.black87,
onTap: () {
// 退出登录确认对话框
Get.dialog(
@ -816,11 +816,12 @@ class EditProfileView extends GetView<ProfileController> {
title: 'logout'.tr, // 退出登录
icon: Icons.logout,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[50]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () {
// 退出登录确认对话框
Get.dialog(

8
lib/modules/settings/controllers/settings_controller.dart

@ -301,9 +301,11 @@ class SettingsController extends GetxController {
}
}
/// 设置语言
Future<void> setLanguage(LanguageModel data) async {
try {
Locale? targetLocale;
// print('lxm--设置语言: ${data.value}');
if (data.value != 'system') {
// 用户手动选择的语言
@ -311,7 +313,11 @@ class SettingsController extends GetxController {
targetLocale = Locale(languageList[0], languageList[1]);
} else {
// 跟随系统语言,使用智能匹配
targetLocale = AppTranslations.deviceLocale();
targetLocale = Get.deviceLocale;
if (targetLocale != null) {
// 智能匹配系统语言到支持的语言
targetLocale = AppTranslations.getMatchedLocale(targetLocale);
}
}
if (targetLocale != null) {

38
lib/modules/settings/views/settings_view.dart

@ -514,11 +514,13 @@ class SettingsView extends GetView<SettingsController> {
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
//BLE测试
// //BLE测试
// 支付测试
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
title: '支付测试',
subtitle: '测试微信支付功能',
icon: Icons.payment,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
@ -527,7 +529,7 @@ class SettingsView extends GetView<SettingsController> {
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
Get.toNamed(Routes.payment);
},
isDarkMode: isDarkMode,
),
@ -537,6 +539,32 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
Obx(() => controller.showDebugInfo.value
? _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,
)
: const SizedBox.shrink()),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// BLE测试
// _buildSimpleNavigationSetting(
// title: 'BLE蓝牙测试',

31
lib/modules/splash/controllers/splash_controller.dart

@ -27,12 +27,20 @@ class SplashController extends GetxController {
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 隐私政策同意状态 - 新增
final _hasAcceptedPrivacy = false.obs;
RxBool get hasAcceptedPrivacy => _hasAcceptedPrivacy;
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _canNavigate = false.obs;
@override
void onInit() {
super.onInit();
// 初始化隐私政策状态
_hasAcceptedPrivacy.value =
_storage.read<bool>('hasAcceptedPrivacy') ?? false;
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
once(_canNavigate, (_) => _navigateToTargetPage());
// 启动整个初始化流程
@ -59,10 +67,17 @@ class SplashController extends GetxController {
_canNavigate.value = true;
}
/// 切换隐私政策同意状态 - 新增方法
void togglePrivacyAcceptance() {
_hasAcceptedPrivacy.value = !_hasAcceptedPrivacy.value;
_storage.write('hasAcceptedPrivacy', _hasAcceptedPrivacy.value);
}
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
/// 返回用户是否同意
Future<bool> _checkAndRequestPrivacy() async {
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) {
_hasAcceptedPrivacy.value = true;
return true;
}
@ -71,6 +86,7 @@ class SplashController extends GetxController {
PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
_hasAcceptedPrivacy.value = true;
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
@ -289,6 +305,21 @@ class SplashController extends GetxController {
return false;
}
}
/// 处理QQ音乐登录回调
void handleQQMusicLogin() {
try {
Logger.i(_tag, '处理QQ音乐登录回调');
// 这里可以添加具体的QQ音乐登录处理逻辑
// 例如:验证登录状态、更新用户信息、导航到相应页面等
// 如果需要导航到特定页面,可以使用:
// Get.toNamed(Routes.qqmusic);
} catch (e) {
Logger.e(_tag, '处理QQ音乐登录回调失败: $e');
}
}
}
/// 显示无网络连接对话框并等待用户操作(阻塞版本)

115
lib/modules/splash/views/splash_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/splash_controller.dart';
import '../../common/webview_page.dart';
class SplashView extends GetView<SplashController> {
const SplashView({Key? key}) : super(key: key);
@ -39,6 +40,14 @@ class SplashView extends GetView<SplashController> {
),
),
// 隐私政策组件 - 添加在底部
Positioned(
bottom: 90.h,
left: 0,
right: 0,
child: _buildTermsAndPrivacy(isDarkMode),
),
// 版本号
//_buildVersionInfo(isDarkMode),
],
@ -206,6 +215,112 @@ class SplashView extends GetView<SplashController> {
);
}
// 用户协议和隐私政策
Widget _buildTermsAndPrivacy(bool isDarkMode) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 将勾选框和文本包装在 GestureDetector 中
GestureDetector(
//onTap: () => controller.togglePrivacyAcceptance(),
behavior: HitTestBehavior.opaque, // 确保透明区域也能响应点击
child: Row(
children: [
SizedBox(
width: 12.w,
height: 16.w,
child: Transform.scale(
scale: 0.7,
child: Obx(() => Checkbox(
value: controller.hasAcceptedPrivacy.value,
onChanged: (value) => controller
.hasAcceptedPrivacy.value = value ?? false,
activeColor: const Color(0xFF3B82F6),
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
side: BorderSide(
width: 1.6,
color: Colors.grey[600]!,
),
)),
),
),
SizedBox(width: 4.w),
Text(
'expressConsent'.tr, // 登录/注册即表示你同意
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
],
),
),
],
),
//SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
// onTap: () {
// String url = 'https://www.voitrans.net/doc/yhxy.html'; // 用户协议链接
// if (Get.locale?.languageCode != 'zh' ||
// Get.locale?.countryCode != 'CN') {
// url = 'https://www.voitrans.net/doc/yhxy_en.html'; // 用户协议英文链接
// }
// Get.to(() => WebViewPage(
// url: url,
// title: 'userAgreement'.tr,
// ));
// },
child: Text(
'userAgreement'.tr, // 《用户协议》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
),
),
),
Text(
'and'.tr, // 和
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[500] : Colors.grey[600],
),
),
GestureDetector(
// onTap: () {
// String url = 'https://www.voitrans.net/doc/ysxy.html'; // 隐私政策链接
// if (Get.locale?.languageCode != 'zh' ||
// Get.locale?.countryCode != 'CN') {
// url = 'https://www.voitrans.net/doc/ysxy_en.html'; // 隐私政策英文链接
// }
// Get.to(() => WebViewPage(
// url: url,
// title: 'privacyPolicy'.tr,
// ));
// },
child: Text(
'privacyPolicy'.tr, // 《隐私政策》
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF3B82F6),
),
),
),
],
),
],
);
}
// 版本信息
// 暂时不需要版本号显示,保留代码以备将来使用
// Widget _buildVersionInfo(bool isDarkMode) {

148
lib/modules/translation/controllers/translation_controller.dart

@ -16,6 +16,7 @@ import '../../../data/services/tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/permission_util.dart';
import '../../../core/services/floating_window_manager.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/translation_usage_service.dart';
@ -23,6 +24,7 @@ import '../../../data/services/ble_manager.dart';
import 'record_timer_controller.dart';
import 'translation_history_controller.dart';
import '../models/translation_models.dart';
import '../views/dialogs/translation_mode_selector.dart';
/// 音频输出设备枚举
enum AudioOutputDevice {
@ -47,6 +49,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final BleManager bleManager = Get.find<BleManager>();
final _musiceManager = Get.find<MusicManager>();
late final TranslationUsageService _usageService;
late final FloatingWindowManager _floatingWindowManager;
// ==================== 悬浮窗相关 ====================
final isFloatingWindowEnabled = false.obs;
// ==================== ASR统计相关 ====================
DateTime? _asrSessionStartTime; // ASR会话开始时间
@ -129,6 +135,100 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 底部控制栏高度
double _bottomBarHeight = 0.0;
/// 初始化悬浮窗管理器
void _initFloatingWindowManager() {
try {
if (!Get.isRegistered<FloatingWindowManager>()) {
Get.put(FloatingWindowManager(), permanent: true);
}
_floatingWindowManager = Get.find<FloatingWindowManager>();
Logger.info('悬浮窗管理器初始化完成');
} catch (e) {
Logger.error('悬浮窗管理器初始化失败: $e');
}
}
/// 切换悬浮窗功能
Future<void> toggleFloatingWindow() async {
if (isFloatingWindowEnabled.value) {
await _disableFloatingWindow();
} else {
await _enableFloatingWindow();
}
}
/// 启用悬浮窗功能
Future<void> _enableFloatingWindow() async {
// 检查是否在支持悬浮窗的模式
if (!_isFloatingWindowSupportedMode()) {
// Get.snackbar(
// 'unsupportedMode'.tr,
// '悬浮窗仅支持通话和音视频翻译模式',
// snackPosition: SnackPosition.TOP,
// );
return;
}
final bool success = await _floatingWindowManager.enableFloatingWindow();
if (success) {
isFloatingWindowEnabled.value = true;
await _floatingWindowManager.showFloatingWindow();
// 如果当前有翻译内容,立即更新到悬浮窗
if (translationHistory.isNotEmpty) {
final latestItem = translationHistory.last;
await _updateFloatingWindowWithTranslation(latestItem);
}
// Get.snackbar(
// '悬浮窗已启用',
// '翻译结果将在悬浮窗中显示',
// snackPosition: SnackPosition.TOP,
// );
}
}
/// 禁用悬浮窗功能
Future<void> _disableFloatingWindow() async {
await _floatingWindowManager.disableFloatingWindow();
isFloatingWindowEnabled.value = false;
// Get.snackbar(
// '悬浮窗已禁用',
// '翻译结果将在应用内显示',
// snackPosition: SnackPosition.TOP,
// );
}
/// 检查当前模式是否支持悬浮窗
bool _isFloatingWindowSupportedMode() {
return currentMode.value == 'call' || currentMode.value == 'audioVideo';
}
/// 更新悬浮窗翻译内容
Future<void> _updateFloatingWindowWithTranslation(
TranslationItem item) async {
if (!isFloatingWindowEnabled.value) return;
final sourceLanguageName =
_languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ??
'未知';
final targetLanguageName =
_languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
'未知';
final data = FloatingWindowData(
sourceText: item.sourceText,
translatedText: item.translatedText,
sourceLanguage: sourceLanguageName,
targetLanguage: targetLanguageName,
timestamp: item.timestamp,
isIntermediate: item.isIntermediate,
);
await _floatingWindowManager.updateFloatingWindowContent(data);
}
/// 更改翻译模式
/// [mode] 翻译模式:simultaneous(同声传译)、audioVideo(音视频)、call(通话)、faceToFace(面对面)
Future<void> changeTranslationMode(String mode) async {
@ -136,6 +236,26 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
currentModeTitle.value = _getModeTitle(mode);
stopAll();
// 检查悬浮窗支持情况
if (isFloatingWindowEnabled.value && !_isFloatingWindowSupportedMode()) {
// 当前模式不支持悬浮窗,自动禁用
await _disableFloatingWindow();
// Get.snackbar(
// '模式已切换',
// '悬浮窗仅支持通话和音视频翻译模式',
// snackPosition: SnackPosition.TOP,
// );
} else if (!isFloatingWindowEnabled.value &&
_isFloatingWindowSupportedMode()) {
// 切换到支持悬浮窗的模式,提示用户可以启用
// Get.snackbar(
// '模式已切换',
// '可以启用悬浮窗显示翻译结果',
// snackPosition: SnackPosition.TOP,
// duration: const Duration(seconds: 4),
// );
}
if (mode == 'call') {
await _initializeCallModeTranslationService();
} else {
@ -252,6 +372,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_timerManager = RecordTimerController();
_historyManager = TranslationHistoryManager();
// 初始化悬浮窗管理器
_initFloatingWindowManager();
// 预先检查权限,避免在用户交互时阻塞
_preCheckPermissions();
@ -285,6 +408,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker
WidgetsBinding.instance.addObserver(this);
// 延迟显示模式选择器,确保页面已经完全加载
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// // 确保上下文有效
// if (Get.context == null || !Get.context!.mounted) return;
// _showModeSelector();
// });
}
/// 预先检查权限
@ -339,6 +470,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
}
}
/// 显示模式选择器
void _showModeSelector() {
try {
final isDarkMode = Get.theme.brightness == Brightness.dark;
// 直接使用现有的翻译模式选择器组件
TranslationModeSelector.show(isDarkMode);
} catch (e) {
Logger.error('显示模式选择器失败: $e');
}
}
/// 保存悬浮按钮位置
void saveFloatingButtonPosition(Offset position) {
floatingButtonPosition.value = position;
@ -984,6 +1127,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
translationHistory[index].targetLanguageCode = languageCodes['target']!;
translationHistory.refresh();
// 更新悬浮窗内容
if (isFloatingWindowEnabled.value) {
await _updateFloatingWindowWithTranslation(translationHistory[index]);
}
// 添加TranslationItem日志打印
if (isFinal) {
final item = translationHistory[index];

162
lib/modules/translation/controllers/translation_detail_controller.dart

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/volcano_ai_service.dart';
// import '../../../data/services/volcano_ai_service.dart';// 注释掉火山AI服务导入
import '../controllers/translation_controller.dart';
import 'translation_audiohistory_controller.dart';
import '../models/translation_models.dart'; // 添加这行导入
@ -12,7 +12,7 @@ import '../../../data/services/translation_usage_service.dart';
//详情页管理
class TranslationDetailController extends GetxController {
// 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
//final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
@ -124,89 +124,89 @@ class TranslationDetailController extends GetxController {
sessionItems.map((e) => e.translatedText).join('\n\n');
// 使用延长的超时时间重试提取摘要
Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
return _extractSummaryInternal(text, _extendedTimeoutDuration);
}
// Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
// return _extractSummaryInternal(text, _extendedTimeoutDuration);
// }
// 提取摘要
Future<String> extractSummary(String text) async {
return _extractSummaryInternal(text, _defaultTimeoutDuration);
}
// Future<String> extractSummary(String text) async {
// return _extractSummaryInternal(text, _defaultTimeoutDuration);
// }
// 内部摘要提取方法,避免代码重复
Future<String> _extractSummaryInternal(
String text, int timeoutDuration) async {
if (text.isEmpty) {
return '无法生成摘要:文本为空';
}
isGeneratingSummary.value = true;
try {
// 构建提示词
final messages = [
{'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
];
// 系统提示词
const systemPrompt = '''
你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
请按照以下格式生成摘要:
1. 首先用1-2句话对整体内容进行简要概括
2. 然后使用项目符号列表(•)列出关键点或重要信息
3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
请遵循以下原则:
1. 保留原文的核心信息和关键点
2. 使用简洁、清晰的语言
3. 保持客观,不添加个人观点
4. 摘要总长度应该是原文的1/3或更短
5. 确保列表中的每个要点都简明扼要,突出重点
''';
String result;
try {
// 使用超时限制
result = await _aiService
.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
)
.timeout(Duration(seconds: timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
});
} on TimeoutException catch (_) {
final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
? '生成摘要超时,请稍后重试'
: '生成摘要超时,请检查网络连接后重试';
summaryResult.value = timeoutMessage;
return summaryResult.value;
}
// 更新结果并保存
if (result.isNotEmpty) {
summaryResult.value = result;
saveSummary(result);
// 记录AI摘要API调用统计
try {
_usageService.recordAiSummaryApiCall();
} catch (e) {
Logger.error('记录AI摘要统计失败: $e');
}
}
return result;
} catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage;
return errorMessage;
} finally {
isGeneratingSummary.value = false;
}
}
// Future<String> _extractSummaryInternal(
// String text, int timeoutDuration) async {
// if (text.isEmpty) {
// return '无法生成摘要:文本为空';
// }
// isGeneratingSummary.value = true;
// try {
// // 构建提示词
// final messages = [
// {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
// ];
// // 系统提示词
// const systemPrompt = '''
// 你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
// 请按照以下格式生成摘要:
// 1. 首先用1-2句话对整体内容进行简要概括
// 2. 然后使用项目符号列表(•)列出关键点或重要信息
// 3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
// 请遵循以下原则:
// 1. 保留原文的核心信息和关键点
// 2. 使用简洁、清晰的语言
// 3. 保持客观,不添加个人观点
// 4. 摘要总长度应该是原文的1/3或更短
// 5. 确保列表中的每个要点都简明扼要,突出重点
// ''';
// String result;
// try {
// // 使用超时限制
// result = await _aiService
// .sendMessage(
// messages: messages,
// systemPrompt: systemPrompt,
// )
// .timeout(Duration(seconds: timeoutDuration), onTimeout: () {
// throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
// });
// } on TimeoutException catch (_) {
// final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
// ? '生成摘要超时,请稍后重试'
// : '生成摘要超时,请检查网络连接后重试';
// summaryResult.value = timeoutMessage;
// return summaryResult.value;
// }
// // 更新结果并保存
// if (result.isNotEmpty) {
// summaryResult.value = result;
// saveSummary(result);
// // 记录AI摘要API调用统计
// try {
// _usageService.recordAiSummaryApiCall();
// } catch (e) {
// Logger.error('记录AI摘要统计失败: $e');
// }
// }
// return result;
// } catch (e) {
// final errorMessage = '生成摘要失败: ${e.toString()}';
// Logger.error('TranslationDetailController', errorMessage);
// summaryResult.value = errorMessage;
// return errorMessage;
// } finally {
// isGeneratingSummary.value = false;
// }
// }
// 删除单条翻译记录
Future<void> deleteTranslationItem(TranslationItem item) async {

46
lib/modules/translation/controllers/translation_list_controller.dart

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
@ -93,24 +94,43 @@ class TranslationListController extends GetxController {
// 请求存储权限
void _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!;
PermissionUtil.instance
.requestPermission(
// context: context,
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
// 只在Android 10及以下版本请求存储权限
if (Platform.isAndroid) {
try {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
final sdkVersion = androidInfo.version.sdkInt;
// Android 11+ (API 30+) 不需要存储权限
if (sdkVersion >= 30) {
Logger.d("Permission", "Android 11+版本,无需请求存储权限");
return; // 直接返回,表示可以使用存储
}
} catch (e) {
Logger.e("Permission", "获取Android版本信息失败: $e");
// 如果获取版本失败,为了兼容性还是请求权限
}
}
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.storage,
permissionName: 'storagePermission'.tr,
explanationText: 'storagePermissionExplanation'.tr,
permanentDenialText: 'storagePermissionPermanentDenial'.tr,
);
if (granted) {
Logger.d("Permission", "用户授予了存储权限");
//return true;
} else {
Logger.d("Permission", "用户未授予存储权限");
// 可以提示用户无法进行文件操作
//return false;
}
});
} catch (e) {
Logger.e("Permission", "请求存储权限时出错: $e");
//return false;
}
}
// 添加方法:获取音频时长

172
lib/modules/translation/views/dialogs/delete_confirm_dialog.dart

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../../core/theme/app_colors.dart';
import '../../models/translation_models.dart';
/// 删除确认对话框
/// 提供翻译记录的删除确认功能
class DeleteConfirmDialog {
/// 显示删除单条翻译记录确认对话框
static void showDeleteItemDialog({
required TranslationItem item,
required Function() onConfirm,
}) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'deleteTranslationRecord'.tr, // 删除翻译记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'deleteTranslationConfirm'.tr, // 确定要删除这条翻译记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
SizedBox(height: 12.h),
// 显示要删除的记录预览
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]?.withOpacity(0.5)
: Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.sourceText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (item.translatedText.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
onConfirm();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
/// 显示清除所有历史记录确认对话框
static void showClearHistoryDialog({
required Function() onConfirm,
}) {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'clearHistory'.tr, // 清除历史记录
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'clearHistoryConfirm'.tr, // 确定要清除所有翻译历史记录吗?此操作无法撤销。
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
onConfirm();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
}

187
lib/modules/translation/views/dialogs/translation_long_press_menu.dart

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../../core/theme/app_colors.dart';
import '../../models/translation_models.dart';
/// 翻译长按菜单对话框
/// 负责处理翻译条目的长按菜单显示和交互
class TranslationLongPressMenu {
/// 显示翻译长按菜单
static void show({
required BuildContext context,
required bool isDarkMode,
required LongPressStartDetails details,
required TranslationItem item,
required Function(TranslationItem) onCopy,
required Function(TranslationItem) onDeleteItem,
required Function() onClearHistory,
}) {
// 只对完成态记录显示长按菜单
if (item.isIntermediate) return;
// 触发触觉反馈
Haptics.vibrate(HapticsType.success);
// 计算弹出菜单位置
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
// 显示弹出菜单
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: _buildMenuItems(
context: context,
isDarkMode: isDarkMode,
item: item,
onCopy: onCopy,
onDeleteItem: onDeleteItem,
onClearHistory: onClearHistory,
),
);
}
/// 构建菜单项
static List<PopupMenuEntry> _buildMenuItems({
required BuildContext context,
required bool isDarkMode,
required TranslationItem item,
required Function(TranslationItem) onCopy,
required Function(TranslationItem) onDeleteItem,
required Function() onClearHistory,
}) {
return [
// 复制选项
_buildPopupMenuItem(
Icon(
Icons.copy,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'copy'.tr, // 复制
isDarkMode ? Colors.white : Colors.black87,
() => onCopy(item),
),
// 分隔线
PopupMenuItem(
height: 1,
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
// 删除该条记录选项
_buildPopupMenuItem(
Icon(
Icons.delete_outline,
size: 20.w,
color: isDarkMode ? Colors.orange[300] : Colors.orange,
),
'delete'.tr, // 删除
isDarkMode ? Colors.orange[300]! : Colors.orange,
() => onDeleteItem(item),
),
// 分隔线
PopupMenuItem(
height: 1,
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
// 删除所有记录选项
_buildPopupMenuItem(
Icon(
Icons.delete_sweep,
size: 20.w,
color: Colors.red,
),
'clearHistory'.tr, // 清除历史记录
Colors.red,
onClearHistory,
),
];
}
/// 构建弹出菜单项
static PopupMenuEntry _buildPopupMenuItem(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 150.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
);
}
/// 复制翻译条目内容
/// 将原文和翻译文本复制到剪贴板
static void copyTranslationItem(TranslationItem item) {
// 构建复制文本:原文 + 翻译
String copyText = item.sourceText;
if (item.translatedText.isNotEmpty) {
copyText += '\n${item.translatedText}';
}
Clipboard.setData(ClipboardData(text: copyText)).then((_) {
Get.snackbar(
'success'.tr,
'copySuccess'.tr, // 复制成功
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(milliseconds: 1200),
icon: Icon(
Icons.check_circle_outline,
color: Colors.white,
size: 24.sp,
),
);
});
}
}

81
lib/modules/translation/views/dialogs/translation_mode_selector.dart

@ -8,21 +8,82 @@ import '../../controllers/translation_controller.dart';
/// 翻译模式选择对话框
class TranslationModeSelector {
/// 显示翻译模式选择器
static void show(bool isDarkMode) {
static void show(bool isDarkMode, {Offset? anchor}) {
final controller = Get.find<TranslationController>();
Get.bottomSheet(
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: _buildTranslationModeSelector(controller),
),
isScrollControlled: true,
backgroundColor: Colors.transparent,
final Size screenSize = MediaQuery.of(Get.context!).size;
Alignment alignment;
if (anchor != null) {
// 使用传入的锚点计算对齐点
final double ax = (anchor.dx / screenSize.width) * 2 - 1;
final double ay = (anchor.dy / screenSize.height) * 2 - 1;
alignment = Alignment(ax, ay);
} else {
final Offset floatingButtonPos = controller.floatingButtonPosition.value;
// 计算悬浮按钮的中心点位置(从右下角偏移转换为屏幕坐标)
final double buttonSize = 56.0.w; // 悬浮按钮的大小
final double buttonCenterX =
screenSize.width - floatingButtonPos.dx - buttonSize / 2;
final double buttonCenterY =
screenSize.height - floatingButtonPos.dy - buttonSize / 2;
// 计算缩放对齐点(-1..1)
final double ax = (buttonCenterX / screenSize.width) * 2 - 1;
final double ay = (buttonCenterY / screenSize.height) * 2 - 1;
alignment = Alignment(ax, ay);
}
Get.generalDialog(
barrierLabel: 'translation_mode_selector',
barrierDismissible: true, // 允许点击背景关闭
barrierColor: Colors.transparent, // 设置为透明,让自定义模糊层处理点击
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return const SizedBox.shrink();
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic);
return Stack(
children: [
// 修复:将模糊背景放在可点击区域
GestureDetector(
onTap: () => Get.back(), // 添加点击关闭功能
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
color: isDarkMode
? Colors.black.withOpacity(0.55)
: Colors.grey.withOpacity(0.95), // 添加半透明背景
),
),
),
// 内容区域
ScaleTransition(
alignment: alignment,
scale: Tween<double>(begin: 0.1, end: 1.0).animate(curved),
child: FadeTransition(
opacity: curved,
child: SizedBox.expand(
child: Align(
alignment: Alignment.bottomCenter,
child: _buildTranslationModeSelector(controller),
),
),
),
),
],
);
},
);
}
// 构建翻译模式选择器 - 参考会议模块样式
static Widget _buildTranslationModeSelector(TranslationController controller) {
static Widget _buildTranslationModeSelector(
TranslationController controller) {
return Builder(
builder: (BuildContext context) {
return SafeArea(

352
lib/modules/translation/views/translation_detail_view.dart

@ -142,9 +142,9 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
if (controller.selectedTabIndex.value == 0)
..._buildOriginalRecords(isDarkMode)
else
_buildSummaryWidget(isDarkMode),
//_buildSummaryWidget(isDarkMode),
SizedBox(height: 24.h),
SizedBox(height: 24.h),
],
);
}),
@ -323,23 +323,23 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}
// 构建总结视图
Widget _buildSummaryWidget(bool isDarkMode) {
final String combinedSourceText = _getCombinedSourceText();
// Widget _buildSummaryWidget(bool isDarkMode) {
// final String combinedSourceText = _getCombinedSourceText();
return Column(
children: [
// 摘要内容容器
_buildSummaryContainer(isDarkMode),
// return Column(
// children: [
// // 摘要内容容器
// _buildSummaryContainer(isDarkMode),
// 提取摘要按钮
Obx(() => _buildExtractSummaryButton(combinedSourceText, isDarkMode)),
// // 提取摘要按钮
// Obx(() => _buildExtractSummaryButton(combinedSourceText, isDarkMode)),
// 重新生成按钮
Obx(() =>
_buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
],
);
}
// // 重新生成按钮
// Obx(() =>
// _buildRegenerateSummaryButton(combinedSourceText, isDarkMode)),
// ],
// );
// }
// 构建摘要容器
Widget _buildSummaryContainer(bool isDarkMode) {
@ -484,163 +484,163 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
}
// 构建提取摘要按钮
Widget _buildExtractSummaryButton(
String combinedSourceText, bool isDarkMode) {
final bool isDisabled = controller.isGeneratingSummary.value ||
controller.summaryResult.value.isNotEmpty;
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: ElevatedButton(
onPressed:
isDisabled ? null : () => _extractSummary(combinedSourceText),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? AppColors.primary : Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: controller.isGeneratingSummary.value
? isDarkMode
? AppColors.primary.withAlpha(128)
: Colors.blue.withAlpha(128)
: Colors.grey.withAlpha(128),
),
child: controller.isGeneratingSummary.value
? _buildGeneratingIndicator(isDarkMode)
: Text(
controller.summaryResult.value.isEmpty
? 'extractSummary'.tr
: 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
);
}
// Widget _buildExtractSummaryButton(
// String combinedSourceText, bool isDarkMode) {
// final bool isDisabled = controller.isGeneratingSummary.value ||
// controller.summaryResult.value.isNotEmpty;
// return Container(
// width: double.infinity,
// margin: EdgeInsets.only(bottom: 16.h),
// child: ElevatedButton(
// onPressed:
// isDisabled ? null : () => _extractSummary(combinedSourceText),
// style: ElevatedButton.styleFrom(
// backgroundColor: isDarkMode ? AppColors.primary : Colors.blue,
// foregroundColor: Colors.white,
// padding: EdgeInsets.symmetric(vertical: 12.h),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16.r),
// ),
// elevation: 0,
// disabledBackgroundColor: controller.isGeneratingSummary.value
// ? isDarkMode
// ? AppColors.primary.withAlpha(128)
// : Colors.blue.withAlpha(128)
// : Colors.grey.withAlpha(128),
// ),
// child: controller.isGeneratingSummary.value
// ? _buildGeneratingIndicator(isDarkMode)
// : Text(
// controller.summaryResult.value.isEmpty
// ? 'extractSummary'.tr
// : 'summaryGenerated'.tr, // 提取摘要 : 已生成摘要
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// );
// }
// 构建生成中指示器
Widget _buildGeneratingIndicator(bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.h,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.0,
),
),
SizedBox(width: 12.w),
Text(
'generating'.tr, // 生成中...
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
],
);
}
// Widget _buildGeneratingIndicator(bool isDarkMode) {
// return Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// SizedBox(
// width: 20.w,
// height: 20.h,
// child: CircularProgressIndicator(
// color: Colors.white,
// strokeWidth: 2.0,
// ),
// ),
// SizedBox(width: 12.w),
// Text(
// 'generating'.tr, // 生成中...
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// );
// }
// 构建重新生成按钮
Widget _buildRegenerateSummaryButton(
String combinedSourceText, bool isDarkMode) {
if (controller.summaryResult.value.isEmpty ||
controller.isGeneratingSummary.value) {
return SizedBox.shrink();
}
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: OutlinedButton(
onPressed: () => _extractSummary(combinedSourceText),
style: OutlinedButton.styleFrom(
foregroundColor: isDarkMode ? AppColors.primary : Colors.blue,
side: BorderSide(color: isDarkMode ? AppColors.primary : Colors.blue),
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
),
child: Text(
'regenerate'.tr, // 重新生成
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
);
}
// Widget _buildRegenerateSummaryButton(
// String combinedSourceText, bool isDarkMode) {
// if (controller.summaryResult.value.isEmpty ||
// controller.isGeneratingSummary.value) {
// return SizedBox.shrink();
// }
// return Container(
// width: double.infinity,
// margin: EdgeInsets.only(bottom: 16.h),
// child: OutlinedButton(
// onPressed: () => _extractSummary(combinedSourceText),
// style: OutlinedButton.styleFrom(
// foregroundColor: isDarkMode ? AppColors.primary : Colors.blue,
// side: BorderSide(color: isDarkMode ? AppColors.primary : Colors.blue),
// padding: EdgeInsets.symmetric(vertical: 12.h),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16.r),
// ),
// ),
// child: Text(
// 'regenerate'.tr, // 重新生成
// style: TextStyle(
// fontSize: 16.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// );
// }
// 提取摘要方法
void _extractSummary(String text) async {
final result = await controller.extractSummary(text);
// void _extractSummary(String text) async {
// final result = await controller.extractSummary(text);
// 检查是否超时,如果超时则显示重试选项
if (result.contains('超时')) {
_showTimeoutDialog(text);
}
}
// // 检查是否超时,如果超时则显示重试选项
// if (result.contains('超时')) {
// _showTimeoutDialog(text);
// }
// }
// 显示超时对话框
void _showTimeoutDialog(String text) {
Get.dialog(
AlertDialog(
title: Text(
'requestTimeout'.tr, // 请求超时
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
content: Text(
'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。
style: TextStyle(
fontSize: 16.sp,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 使用延长的超时时间重试
controller.retryExtractSummaryWithExtendedTimeout(text);
},
child: Text(
'retry'.tr, // 重试
style: TextStyle(
fontSize: 16.sp,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// void _showTimeoutDialog(String text) {
// Get.dialog(
// AlertDialog(
// title: Text(
// 'requestTimeout'.tr, // 请求超时
// style: TextStyle(
// fontSize: 18.sp,
// fontWeight: FontWeight.bold,
// ),
// ),
// content: Text(
// 'summaryTimeoutMessage'.tr, // 生成摘要请求超时,可能是网络问题或服务器繁忙。
// style: TextStyle(
// fontSize: 16.sp,
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Get.back(),
// child: Text(
// 'cancel'.tr, // 取消
// style: TextStyle(
// fontSize: 16.sp,
// color: Colors.grey[700],
// ),
// ),
// ),
// TextButton(
// onPressed: () {
// Get.back();
// // 使用延长的超时时间重试
// controller.retryExtractSummaryWithExtendedTimeout(text);
// },
// child: Text(
// 'retry'.tr, // 重试
// style: TextStyle(
// fontSize: 16.sp,
// color: Colors.blue,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// ],
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.r),
// ),
// ),
// );
// }
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
@ -750,18 +750,18 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
'${'originalText'.tr}:\n$sourceText\n\n${'translatedText'.tr}:\n$translatedText'; // 原文:译文:
} else {
// 分享摘要
if (controller.summaryResult.value.isNotEmpty) {
shareText =
'${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
} else {
Get.snackbar(
'cannotShare'.tr, // 无法分享
'generateSummaryFirst'.tr, // 请先生成摘要
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
return;
}
// if (controller.summaryResult.value.isNotEmpty) {
// shareText =
// '${'sessionSummary'.tr}:\n${controller.summaryResult.value}'; // 会话摘要:
// } else {
// Get.snackbar(
// 'cannotShare'.tr, // 无法分享
// 'generateSummaryFirst'.tr, // 请先生成摘要
// snackPosition: SnackPosition.BOTTOM,
// duration: Duration(seconds: 2),
// );
// return;
// }
}
// 调用系统分享

358
lib/modules/translation/views/translation_view.dart

@ -2,8 +2,10 @@ import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../controllers/translation_controller.dart';
import '../models/translation_models.dart';
import '../views/translation_history_view.dart';
@ -16,6 +18,7 @@ import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局
import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮
import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框
import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器
import 'dialogs/translation_long_press_menu.dart'; // 长按菜单
// import 'package:intl/intl.dart';
@ -42,10 +45,10 @@ class TranslationView extends GetView<TranslationController> {
children: [
_buildBody(isDarkMode),
// 可拖拽的悬浮按钮
DraggableFloatingButton(
isDarkMode: isDarkMode,
controller: controller,
),
// DraggableFloatingButton(
// isDarkMode: isDarkMode,
// controller: controller,
// ),
],
),
);
@ -141,6 +144,15 @@ class TranslationView extends GetView<TranslationController> {
title: Obx(() => GestureDetector(
onTap: () {
// 点击标题时弹出翻译模式选择器
// 从拖拽按钮位置作为锚点打开
// final pos = controller.floatingButtonPosition.value;
// final Size screen = MediaQuery.of(Get.context!).size;
// // 已保存的是距右、下的偏移,需要换算为屏幕坐标中的中心点
// final double buttonSize = 56.w;
// final double left = screen.width - pos.dx - buttonSize;
// final double top = screen.height - pos.dy - buttonSize;
// final Offset anchor =
// Offset(left + buttonSize / 2, top + buttonSize / 2);
TranslationModeSelector.show(isDarkMode);
},
child: Text(
@ -191,9 +203,31 @@ class TranslationView extends GetView<TranslationController> {
),
);
} else {
// 未录音:显示红色录音按钮 + 文件夹按钮
// 未录音:显示红色录音按钮 + 文件夹按钮 + 悬浮窗按钮
return Row(
children: [
// 悬浮窗切换按钮(仅在支持的模式下显示)
Obx(() {
if (controller.currentMode.value == 'call' ||
controller.currentMode.value == 'audioVideo') {
return IconButton(
icon: Icon(
controller.isFloatingWindowEnabled.value
? Icons.picture_in_picture
: Icons.picture_in_picture_alt_outlined,
color: controller.isFloatingWindowEnabled.value
? Colors.blue
: (isDarkMode ? Colors.white : Colors.black),
size: 24.sp,
),
onPressed: controller.toggleFloatingWindow,
tooltip: controller.isFloatingWindowEnabled.value
? '禁用悬浮窗'
: '启用悬浮窗',
);
}
return const SizedBox.shrink();
}),
IconButton(
icon: Icon(
Icons.radio_button_checked,
@ -343,7 +377,17 @@ class TranslationView extends GetView<TranslationController> {
}
Widget _buildBody(bool isDarkMode) {
return Column(
return
// GestureDetector(
// onPanUpdate: (details) {
// // 检测左滑手势
// if (details.delta.dx < -10) {
// // 左滑距离超过10像素时触发
// _handleSwipeLeft();
// }
// },
// child:
Column(
children: [
Expanded(
child: _buildTranslationContent(isDarkMode),
@ -351,8 +395,15 @@ class TranslationView extends GetView<TranslationController> {
_buildControlBar(isDarkMode),
],
);
// );
}
// 处理左滑手势
// void _handleSwipeLeft() async {
// await controller.stopAll();
// Get.toNamed(Routes.translationHistory);
// }
Widget _buildTranslationContent(bool isDarkMode) {
return Obx(() {
// 其他模式使用原有的列表布局
@ -397,103 +448,116 @@ class TranslationView extends GetView<TranslationController> {
Widget _buildTranslationItemWidget(
BuildContext context, TranslationItem item) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(0.1) // 中间结果使用浅灰色底色
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? Theme.of(context)
.colorScheme
.primary
.withOpacity(0.08) // 源语言匹配时使用主题色底色
: Theme.of(context)
.colorScheme
.secondary
.withOpacity(0.08)), // 否则使用次要色底色
borderRadius: BorderRadius.circular(12),
border: Border.all(
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 定义自适应颜色
final selfColor = isDarkMode
? Colors.blue[800]!.withOpacity(0.15) // 深色模式:深蓝+透明度
: Colors.blue[50]!; // 浅色模式:浅蓝
final otherColor = isDarkMode
? Colors.green[800]!.withOpacity(0.15) // 深色模式:深绿+透明度
: Colors.green[50]!; // 浅色模式:浅绿
return GestureDetector(
onLongPressStart: (details) => _onLongPressTranslationItem(
context,
isDarkMode,
details,
item,
),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(0.2)
: Colors.transparent,
width: 1,
? Colors.grey.withOpacity(isDarkMode ? 0.15 : 0.1) // 中间结果
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? selfColor // 自己说的话
: otherColor), // 对方说的话
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.3 : 0.2)
: Colors.transparent,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
),
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
),
),
),
),
],
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
],
),
],
// 语言信息和时间戳
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
],
// 语言信息和时间戳
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
],
),
],
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
],
),
),
);
}
@ -569,8 +633,26 @@ class TranslationView extends GetView<TranslationController> {
if (isRecognizing) {
controller.stopAll();
} else {
// 弹出翻译模式选择器
TranslationModeSelector.show(isDarkMode);
// 获取底部按钮的位置
if (controlBarKey.currentContext != null) {
final RenderBox renderBox =
controlBarKey.currentContext!.findRenderObject()
as RenderBox;
final buttonPosition =
renderBox.localToGlobal(Offset.zero);
final buttonSize = renderBox.size;
// 计算按钮中心点
final Offset anchor = buttonPosition +
Offset(buttonSize.width / 2,
buttonSize.height / 2);
TranslationModeSelector.show(isDarkMode,
anchor: anchor);
//controller.startRecognition();
} else {
TranslationModeSelector.show(isDarkMode);
}
}
},
child: Container(
@ -858,6 +940,24 @@ class TranslationView extends GetView<TranslationController> {
});
}
/// 处理翻译条目长按事件
void _onLongPressTranslationItem(
BuildContext context,
bool isDarkMode,
LongPressStartDetails details,
TranslationItem item,
) {
TranslationLongPressMenu.show(
context: context,
isDarkMode: isDarkMode,
details: details,
item: item,
onCopy: (item) => TranslationLongPressMenu.copyTranslationItem(item),
onDeleteItem: (item) => _deleteTranslationItem(item),
onClearHistory: _showClearHistoryConfirmDialog,
);
}
// 显示删除单条记录确认对话框
void _showDeleteItemConfirmDialog(TranslationItem item) {
final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
@ -969,14 +1069,14 @@ class TranslationView extends GetView<TranslationController> {
controller.deleteTranslationItem(item.sessionId, item.timestamp);
// 显示删除成功提示
Get.snackbar(
'success'.tr, // deleteSuccess
'已删除该翻译记录', // deleteTranslationSuccess
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(milliseconds: 800),
);
// Get.snackbar(
// 'success'.tr, // deleteSuccess
// '已删除该翻译记录', // deleteTranslationSuccess
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
// colorText: Get.isDarkMode ? Colors.white : Colors.black,
// duration: const Duration(milliseconds: 800),
// );
} catch (e) {
// 显示删除失败提示
Get.snackbar(
@ -990,6 +1090,62 @@ class TranslationView extends GetView<TranslationController> {
}
}
// 显示删除所有记录确认对话框
void _showClearHistoryConfirmDialog() {
final isDarkMode = Get.isDarkMode;
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
title: Text(
'clearHistory'.tr,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
content: Text(
'clearHistoryConfirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[300] : Colors.black87,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
// 清除历史记录
controller.clearTranslationHistory();
Get.back();
},
child: Text(
'confirm'.tr,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
// 构建语言选择栏
Widget _buildLanguageSelectionBar(bool isDarkMode) {
return Container(

37
lib/modules/translation/views/widgets/draggable_floating_button.dart

@ -55,19 +55,7 @@ class DraggableFloatingButton extends StatelessWidget {
),
),
onDragEnd: (details) {
// 计算新位置(相对于屏幕右下角)
final screenSize = MediaQuery.of(Get.context!).size;
final newRight = screenSize.width - details.offset.dx - 56.w;
final newBottom = screenSize.height - details.offset.dy - 56.w;
// 边界检查,确保按钮不会超出屏幕
final clampedRight = newRight.clamp(0.0, screenSize.width - 56.w);
final clampedBottom =
newBottom.clamp(0.0, screenSize.height - 56.w);
// 保存新位置
controller.saveFloatingButtonPosition(
Offset(clampedRight, clampedBottom));
_handleDragEnd(context, details);
},
child: GestureDetector(
onTap: () => _showTranslationModeSelector(isDarkMode),
@ -96,8 +84,31 @@ class DraggableFloatingButton extends StatelessWidget {
));
}
/// 处理拖拽结束事件
void _handleDragEnd(BuildContext context, DraggableDetails details) {
final screenSize = MediaQuery.of(context).size;
final appBarHeight = Scaffold.of(context).appBarMaxHeight ?? kToolbarHeight;
final statusBarHeight = MediaQuery.of(context).padding.top;
final safeAreaTop = statusBarHeight + appBarHeight;
// 计算新位置(相对于屏幕右下角)
final newRight = screenSize.width - details.offset.dx - 56.w;
final newBottom = screenSize.height - details.offset.dy - 56.w;
// 边界检查,确保按钮不会超出屏幕或被AppBar遮盖
final clampedRight = newRight.clamp(0.0, screenSize.width - 56.w);
final clampedBottom = newBottom.clamp(
0.0, // 底部边界
screenSize.height - safeAreaTop - 56.w, // 顶部边界(避免被AppBar遮盖)
);
// 保存新位置
controller.saveFloatingButtonPosition(Offset(clampedRight, clampedBottom));
}
/// 显示翻译模式选择器
void _showTranslationModeSelector(bool isDarkMode) {
// 直接调用模式选择器,它会自动从悬浮按钮位置展开
TranslationModeSelector.show(isDarkMode);
}
}

9
lib/routes/app_pages.dart

@ -63,6 +63,8 @@ import '../modules/music/views/music_playlist_view.dart';
import '../modules/music/bindings/music_playlist_binding.dart';
import '../modules/FTFTranslation/bindings/FTFTranslation_binding.dart';
import '../modules/FTFTranslation/views/FTFTranslation_view.dart';
import '../modules/payment/bindings/payment_binding.dart';
import '../modules/payment/views/payment_view.dart';
abstract class AppPages {
static final pages = [
@ -288,5 +290,12 @@ abstract class AppPages {
page: () => const QqmusicPlaylistView(),
binding: QQMusicPlaylistBinding(),
),
GetPage(
name: Routes.payment,
page: () => const PaymentView(),
binding: PaymentBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
];
}

3
lib/routes/app_routes.dart

@ -63,4 +63,7 @@ abstract class Routes {
static const ble = '/ble';
// ble设备管理
static const qqmusic = '/qqmusic';
// 支付相关
static const payment = '/payment'; // 支付测试页面
}

413
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -36,6 +36,11 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date
// 添加后台启动相关导入
import android.app.PendingIntent
import android.app.NotificationManager
import android.app.NotificationChannel
import androidx.core.app.NotificationCompat
/**
@ -1015,7 +1020,7 @@ object AgentService : CoroutineScope {
if (reply) {
sendEvent("assistant_response", responseData)
}
Log.d(TAG, "liwei--------------- AI Call onComplete:$response")
if (response.isNotEmpty()) {
// 添加AI回复到历史记录
addToHistoryMessages(createAssistantMessage(response))
@ -1081,19 +1086,23 @@ object AgentService : CoroutineScope {
audioPlayer?.stopAudio()
val name = functionCall.get("name") as String;
val resultmap = autoHandleFcunCallResult(name, functionCallResult);
aiMetadata = resultmap["meta"] as String;
broadcast = resultmap["broadcast"] as Boolean;
reply = !(resultmap["notreply"] as Boolean);
iscard = resultmap["iscard"] as Boolean;
if (iscard){
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
)
)
aiMetadata = resultmap["meta"] as String;
}
sendEvent(
"function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
"meta" to aiMetadata,
)
)
Log.e(
TAG,
"liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard"
)
} catch (e: Exception) {
Log.e(
TAG,
@ -1859,13 +1868,13 @@ object AgentService : CoroutineScope {
toolname: String,
functionCallResult: JSONObject
): Map<String, Any> {
println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
// println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
try {
val contentStr = functionCallResult.optString("context")
println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
// println("协议工具返回数据 检查是否存在卡片! context: ${contentStr}")
val content = JSONObject(contentStr)
val textStr = content.optString("text")
println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
// println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
val metadata: MutableMap<String, Any> = mutableMapOf()
var broadcast = true;
var notreply = false;
@ -1884,7 +1893,7 @@ object AgentService : CoroutineScope {
metadata[toolname] = meta // 现在可以赋值
metadata["notreply"] = notreply // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString()
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
@ -1956,7 +1965,7 @@ object AgentService : CoroutineScope {
)
}
if (playlist.isNotEmpty()) {
processQQMusicPlayList(playlist)
// processQQMusicPlayList(playlist)
} else {
Log.w(TAG, "qqmusic_searchandplay_songs 中没有有效的音乐条目")
}
@ -1967,9 +1976,24 @@ object AgentService : CoroutineScope {
}else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
processNavigation(start, end);
val destination = cardNavigation.optString("destination", "")
val end_longitude = cardNavigation.optString("end_longitude", "")
val end_latitude = cardNavigation.optString("end_latitude", "")
// final start = card_navigation["start"] as String;
// final destination =
// card_navigation["destination"] as String;
// final end_longitude =
// card_navigation["end_longitude"] as String;
// final end_latitude =
// card_navigation["end_latitude"] as String;
var isinternal = processNavigation(start, destination,end_latitude,end_longitude,"driving");
metadata["isinternal"] = isinternal // 是否是内部导航
}else if (toolname == "map_weather") { //导航服务
iscard = true
}else if (toolname == "maps_weather") { //导航服务
iscard = true
}
val metaStr = JSONObject(metadata as Map<*, *>).toString()
return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply,"iscard" to iscard);
}
} catch (e: Exception) {
@ -2051,25 +2075,352 @@ object AgentService : CoroutineScope {
}
/**
* 处理导航
* 将ai返回的音乐连接 在后台启动播放
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* 启动导航服务到指定目的地
* @param start 起点地址
* @param destination 目的地地址
* @param endlatitude 目的地纬度
* @param endlongitude 目的地经度
* @param mode 导航模式:driving(驾车)、transit(公交)、walking(步行)、cycling(骑行)
* @return 是否成功开始处理
*/
fun processNavigation(start: String, end: String) {
if (navigationMode == "internal_gaode"){
// 如果 playlist 中有有效的歌曲,开始播放
if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) {
NavigationServiceHelper.startNavigation(
context,
"start",
startpos = start,
endpos = end
)
fun processNavigation(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String):Boolean {
Log.d(TAG, "processNavigation: $start -> $destination ($endlongitude,$endlatitude,), mode: $mode")
if (destination.isEmpty()) {
Log.i(TAG, "启动导航服务失败:目的地为空")
return false
}
// 获取导航模式配置,默认为自动选择
val configNavigationMode = navigationMode.ifEmpty { "external_auto" }
Log.d(TAG, "导航模式配置: $configNavigationMode")
when (configNavigationMode) {
"internal_gaode" -> {
// 使用内置高德导航
navigateWithInternalGaode(start, destination, endlatitude, endlongitude, mode)
return true
}
"external_baidu" -> {
// 使用外部百度地图
navigateWithBaiduMap(destination, endlatitude, endlongitude, mode)
return false
}
"external_gaode" -> {
// 使用外部高德地图
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
return false
}
"external_google" -> {
// 使用外部谷歌地图
navigateWithGoogleMap(destination, mode)
return false
}
else -> {
// 自动选择导航应用,优先级:百度 > 高德 > 谷歌 > 内置高德
return autoSelectNavigation(start, destination, endlatitude, endlongitude, mode)
}
}
}
/**
* 使用内置高德导航
*/
private fun navigateWithInternalGaode(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String) {
try {
val endpos = if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
"$endlongitude,$endlatitude"
} else {
destination
}
NavigationServiceHelper.startNavigation(
context,
"start",
startpos = start,
endpos = "$endlongitude,$endlatitude"
)
Log.i(TAG, "启动内置高德导航服务成功")
} catch (e: Exception) {
Log.e(TAG, "启动内置高德导航失败: ${e.message}", e)
}
}
/**
* 使用百度地图导航
*/
private fun navigateWithBaiduMap(destination: String, endlatitude: String,
endlongitude: String, mode: String) {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val src = packageInfo.packageName
// 百度地图导航模式映射
val baiduMode = when (mode) {
"transit" -> "transit"
"walking" -> "walking"
"cycling" -> "riding"
else -> "driving"
}
// 构建目的地参数
val destinationParam = if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
"name:$destination|latlng:$endlatitude,$endlongitude"
} else {
destination
}
val uri = android.net.Uri.parse("baidumap://map/direction?" +
"destination=$destinationParam&" +
"coord_type=gcj02&" +
"mode=$baiduMode&" +
"src=$src")
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.baidu.BaiduMap")
}
if (isAppInstalled("com.baidu.BaiduMap")) {
try {
context.startActivity(intent)
Log.i(TAG, "已启动百度地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动百度地图: ${e.message}")
startActivityWithPendingIntent(intent, "百度地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动百度地图失败: ${e.message}", e)
throw e
}
} else {
Log.w(TAG, "百度地图未安装")
throw Exception("百度地图未安装")
}
} catch (e: Exception) {
Log.e(TAG, "启动百度地图失败: ${e.message}", e)
// 回退到其他导航方式
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
}
}
/**
* 使用高德地图导航
*/
private fun navigateWithGaodeMap(destination: String, endlatitude: String,
endlongitude: String, mode: String) {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val src = packageInfo.packageName
// 高德地图导航模式映射
val gaodeMode = when (mode) {
"transit" -> "1"
"walking" -> "2"
"cycling" -> "3"
else -> "0"
}
val uriBuilder = StringBuilder("androidamap://route/plan?")
if (endlatitude.isNotEmpty() && endlongitude.isNotEmpty()) {
uriBuilder.append("dlat=$endlatitude&")
uriBuilder.append("dlon=$endlongitude&")
}
uriBuilder.append("dname=$destination&")
uriBuilder.append("dev=0&")
uriBuilder.append("t=$gaodeMode&")
uriBuilder.append("sourceApplication=$src")
val uri = android.net.Uri.parse(uriBuilder.toString())
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.autonavi.minimap")
}
if (isAppInstalled("com.autonavi.minimap")) {
try {
context.startActivity(intent)
Log.i(TAG, "已启动高德地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动高德地图: ${e.message}")
startActivityWithPendingIntent(intent, "高德地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动高德地图失败: ${e.message}", e)
throw e
}
} else {
Log.i(TAG, "启动导航服务失败")
Log.w(TAG, "高德地图未安装")
throw Exception("高德地图未安装")
}
} catch (e: Exception) {
Log.e(TAG, "启动高德地图失败: ${e.message}", e)
// 回退到其他导航方式
navigateWithGoogleMap(destination, mode)
}
}
/**
* 使用谷歌地图导航
*/
private fun navigateWithGoogleMap(destination: String, mode: String) {
try {
// 谷歌地图导航模式映射
val googleMode = when (mode) {
"transit" -> "transit"
"walking" -> "walking"
"cycling" -> "bicycling"
else -> "driving"
}
// 尝试使用Google Maps应用
val appUri = android.net.Uri.parse("comgooglemaps://?saddr=Current+Location&daddr=$destination&directionsmode=$googleMode")
val appIntent = Intent(Intent.ACTION_VIEW, appUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
// 添加包名确保启动正确的应用
setPackage("com.google.android.apps.maps")
}
if (isAppInstalled("com.google.android.apps.maps")) {
try {
context.startActivity(appIntent)
Log.i(TAG, "已启动谷歌地图导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图: ${e.message}")
startActivityWithPendingIntent(appIntent, "谷歌地图导航")
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图失败: ${e.message}", e)
throw e
}
} else {
// 回退到网页版
val webUri = android.net.Uri.parse("https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$destination&travelmode=$googleMode")
val webIntent = Intent(Intent.ACTION_VIEW, webUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
try {
context.startActivity(webIntent)
Log.i(TAG, "已启动谷歌地图网页版导航")
} catch (e: SecurityException) {
// Android 10+ 后台启动限制,尝试使用 PendingIntent
Log.w(TAG, "后台启动受限,尝试使用通知启动谷歌地图网页版: ${e.message}")
startActivityWithPendingIntent(webIntent, "谷歌地图网页版导航")
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图网页版失败: ${e.message}", e)
throw e
}
}
} catch (e: Exception) {
Log.e(TAG, "启动谷歌地图失败: ${e.message}", e)
// 最后回退到内置导航
navigateWithInternalGaode("", destination, "", "", mode)
}
}
/**
* 自动选择导航应用
*/
private fun autoSelectNavigation(start: String, destination: String,
endlatitude: String, endlongitude: String, mode: String):Boolean {
Log.d(TAG, "自动选择导航应用")
// 1. 优先尝试百度地图
if (isAppInstalled("com.baidu.BaiduMap")) {
Log.d(TAG, "自动选择:使用百度地图")
navigateWithBaiduMap(destination, endlatitude, endlongitude, mode)
return false
}
// 2. 其次尝试高德地图
if (isAppInstalled("com.autonavi.minimap")) {
Log.d(TAG, "自动选择:使用高德地图")
navigateWithGaodeMap(destination, endlatitude, endlongitude, mode)
return false
}
// 3. 然后尝试谷歌地图
if (isAppInstalled("com.google.android.apps.maps")) {
Log.d(TAG, "自动选择:使用谷歌地图")
navigateWithGoogleMap(destination, mode)
return false
}
// 4. 最后使用内置高德导航
Log.d(TAG, "自动选择:使用内置高德导航")
navigateWithInternalGaode(start, destination, endlatitude, endlongitude, mode)
return true
}
/**
* 使用 PendingIntent 启动 Activity(适用于后台启动场景)
*/
private fun startActivityWithPendingIntent(intent: android.content.Intent, description: String) {
try {
val pendingIntent = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(), // 使用时间戳作为唯一ID
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 创建通知渠道(Android 8.0+)
val channelId = "navigation_channel"
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"导航服务",
NotificationManager.IMPORTANCE_HIGH
).apply {
this.description = "用于启动导航应用"
enableVibration(true)
enableLights(true)
}
notificationManager.createNotificationChannel(channel)
}
// 创建通知
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setContentTitle("启动导航")
.setContentText("点击启动$description")
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.build()
notificationManager.notify(1001, notification)
Log.i(TAG, "已通过通知启动$description")
} catch (e: Exception) {
Log.e(TAG, "PendingIntent 启动失败: ${e.message}", e)
}
}
/**
* 检查应用是否已安装
*/
private fun isAppInstalled(packageName: String): Boolean {
return try {
context.packageManager.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}

340
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -1,4 +1,5 @@
import Foundation
import UIKit
import AVFoundation
import ble_service
import location_service
@ -834,6 +835,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult:
private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -> [String: Any] {
var broadcast = true
var notreply = false
var iscard = false
var metadata: [String: Any] = [:]
metadata[toolName] = jsonDict
@ -843,7 +845,11 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
if let _notreply = jsonDict["notreply"] as? Bool {
notreply = _notreply
}
if let _iscard = jsonDict["iscard"] as? Bool {
iscard = _iscard
}
metadata["notreply"] = notreply
metadata["iscard"] = iscard
// 处理卡片数据
// if let cardMusic = jsonDict["card_music"] as? [String: Any] {
// processMusicCard(cardMusic)
@ -851,11 +857,16 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
if let cardMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "music_searchandplay_songs" {
processMusicListCard(cardMusiclist)
} else if let cardNavigation = jsonDict["card_navigation"] as? [String: Any] {
processNavigationCard(cardNavigation)
let isinternal = processNavigationCard(cardNavigation)
metadata["isinternal"] = isinternal
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
processQQMusicPlayList(cardQQMusiclist)
} else if toolName == "maps_weather" {
iscard = true
}else if toolName == "map_weather" {
iscard = true
}
metadata["iscard"] = iscard
let metaStr: String
if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: []),
let jsonString = String(data: jsonData, encoding: .utf8) {
@ -973,11 +984,305 @@ private func jsonToString(_ json: [String: Any]) -> String? {
}
// 处理导航卡片
private func processNavigationCard(_ cardNavigation: [String: Any]) {
private func processNavigationCard(_ cardNavigation: [String: Any]) -> Bool {
let start = cardNavigation["start"] as? String ?? ""
let end = cardNavigation["end"] as? String ?? ""
// 实际处理导航逻辑
print("启动导航: 从 \(start) 到 \(end)")
let destination = cardNavigation["destination"] as? String ?? ""
let endlatitude = cardNavigation["endlatitude"] as? String ?? ""
let endlongitude = cardNavigation["endlongitude"] as? String ?? ""
let mode = cardNavigation["mode"] as? String ?? "driving"
print("启动导航: 从 \(start) 到 \(destination), 坐标: \(endlatitude),\(endlongitude), 模式: \(mode)")
// 根据导航模式选择不同的导航方式
switch mode {
case "internal_gaode":
// 使用内置高德导航
// startInternalNavigation(start: start, destination: destination,
// latitude: endlatitude, longitude: endlongitude, mode: mode)
return true
case "external_baidu":
// 使用外部百度地图
openBaiduMap(destination: destination, latitude: endlatitude,
longitude: endlongitude, mode: mode)
return false
case "external_gaode":
// 使用外部高德地图
openGaodeMap(destination: destination, latitude: endlatitude,
longitude: endlongitude, mode: mode)
return false
case "external_google":
// 使用外部谷歌地图
openGoogleMap(destination: destination, mode: mode)
return false
default:
// 默认使用自动选择
return autoSelectNavigation(start: start, destination: destination,
latitude: endlatitude, longitude: endlongitude, mode: mode)
}
}
// 启动内置高德导航
private func startInternalNavigation(start: String, destination: String,
latitude: String, longitude: String, mode: String) {
// 发送导航事件通知
NotificationCenter.default.post(name: .navigationdEvent, object: nil, userInfo: [
"start": start,
"destination": destination,
"latitude": latitude,
"longitude": longitude,
"mode": mode
])
print("启动内置导航: 从 \(start) 到 \(destination)")
}
// 打开百度地图
private func openBaiduMap(destination: String, latitude: String, longitude: String, mode: String) {
guard let bundleId = Bundle.main.bundleIdentifier else {
print("无法获取应用包名")
return
}
let src = "ios.\(bundleId)"
// 百度地图导航模式映射
let baiduMode: String
switch mode {
case "transit":
baiduMode = "transit" // 公交
case "walking":
baiduMode = "walking" // 步行
case "cycling":
baiduMode = "riding" // 骑行
case "driving":
fallthrough
default:
baiduMode = "driving" // 驾车
}
var components = URLComponents()
components.scheme = "baidumap"
components.host = "map"
components.path = "/direction"
components.queryItems = [
URLQueryItem(name: "destination", value: "name:\(destination)|latlng:\(latitude),\(longitude)"),
URLQueryItem(name: "coord_type", value: "gcj02"),
URLQueryItem(name: "mode", value: baiduMode),
URLQueryItem(name: "src", value: src)
]
guard let url = components.url else {
print("构建百度地图URL失败")
return
}
print("百度导航链接: \(url.absoluteString)")
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开百度地图")
} else {
print("打开百度地图失败")
}
}
} else {
print("百度地图未安装")
// 可以在这里添加提示用户安装百度地图的逻辑
}
}
}
// 打开高德地图
private func openGaodeMap(destination: String, latitude: String, longitude: String, mode: String) {
guard let bundleId = Bundle.main.bundleIdentifier else {
print("无法获取应用包名")
return
}
let src = "ios.\(bundleId)"
// 导航模式映射
let gaodeMode: String
switch mode {
case "transit":
gaodeMode = "1" // 公交
case "walking":
gaodeMode = "2" // 步行
case "cycling":
gaodeMode = "3" // 骑行
case "driving":
fallthrough
default:
gaodeMode = "0" // 驾车
}
var components = URLComponents()
components.scheme = "amapuri"
components.host = "route"
components.path = "/plan"
components.queryItems = [
URLQueryItem(name: "dlat", value: latitude),
URLQueryItem(name: "dlon", value: longitude),
URLQueryItem(name: "dname", value: destination),
URLQueryItem(name: "dev", value: "0"), // 0: gcj02
URLQueryItem(name: "t", value: gaodeMode),
URLQueryItem(name: "sourceApplication", value: src)
]
guard let url = components.url else {
print("构建高德地图URL失败")
return
}
print("高德导航链接: \(url.absoluteString)")
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开高德地图")
} else {
print("打开高德地图失败")
}
}
} else {
print("高德地图未安装")
// 可以在这里添加提示用户安装高德地图的逻辑
}
}
}
// 打开谷歌地图
private func openGoogleMap(destination: String, mode: String) {
// 方案1: 尝试使用Google Maps应用的URL scheme
var components = URLComponents()
components.scheme = "comgooglemaps"
components.queryItems = [
URLQueryItem(name: "saddr", value: "Current Location"),
URLQueryItem(name: "daddr", value: destination),
URLQueryItem(name: "directionsmode", value: mode)
]
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if let appUrl = components.url, UIApplication.shared.canOpenURL(appUrl) {
UIApplication.shared.open(appUrl, options: [:]) { success in
if success {
print("成功打开谷歌地图应用")
} else {
print("打开谷歌地图应用失败")
}
}
return
}
// 方案2: 使用网页版谷歌地图
let webUrlString = "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=\(destination)&travelmode=\(mode)"
if let webUrl = URL(string: webUrlString) {
UIApplication.shared.open(webUrl, options: [:]) { success in
if success {
print("成功打开谷歌地图网页版")
} else {
print("打开谷歌地图网页版失败")
}
}
}
}
}
// 打开苹果地图
private func openAppleMap(destination: String, mode: String) {
var components = URLComponents()
components.scheme = "maps"
components.queryItems = [
URLQueryItem(name: "daddr", value: destination),
URLQueryItem(name: "dirflg", value: mode == "driving" ? "d" : "r") // d=驾车, r=公交
]
guard let url = components.url else {
print("构建苹果地图URL失败")
return
}
// 确保在主线程上执行UI操作
DispatchQueue.main.async {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { success in
if success {
print("成功打开苹果地图")
} else {
print("打开苹果地图失败")
}
}
} else {
print("苹果地图不可用")
}
}
}
// 自动选择导航应用
private func autoSelectNavigation(start: String, destination: String,
latitude: String, longitude: String, mode: String) -> Bool {
// 1. 优先尝试百度地图
if canOpenBaiduMap() {
print("自动选择:使用百度地图")
openBaiduMap(destination: destination, latitude: latitude, longitude: longitude, mode: mode)
return false
}
// 2. 其次尝试高德地图
if canOpenGaodeMap() {
print("自动选择:使用高德地图")
openGaodeMap(destination: destination, latitude: latitude, longitude: longitude, mode: mode)
return false
}
// 3. 然后尝试谷歌地图
if canOpenGoogleMap() {
print("自动选择:使用谷歌地图")
openGoogleMap(destination: destination, mode: mode)
return false
}
// 4. 尝试苹果地图
if canOpenAppleMap() {
print("自动选择:使用苹果地图")
openAppleMap(destination: destination, mode: mode)
return false
}
// 5. 最后尝试内置高德导航
print("自动选择:使用内置导航")
// startInternalNavigation(start: start, destination: destination,
// latitude: latitude, longitude: longitude, mode: mode)
return true
}
// 检查是否可以打开百度地图
private func canOpenBaiduMap() -> Bool {
guard let url = URL(string: "baidumap://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开高德地图
private func canOpenGaodeMap() -> Bool {
guard let url = URL(string: "amapuri://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开谷歌地图
private func canOpenGoogleMap() -> Bool {
guard let url = URL(string: "comgooglemaps://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 检查是否可以打开苹果地图
private func canOpenAppleMap() -> Bool {
guard let url = URL(string: "maps://") else { return false }
return UIApplication.shared.canOpenURL(url)
}
// 后台播放单曲
@ -1285,6 +1590,7 @@ class ChatApiStreamCallback: StreamCallback {
private var responseBuilder = ""
private var metadata = ""
private var userMetadata:[String: Any] = [:]
private var iscard = false;
private var reply = true; //是否回复
private var broadcast = true //是否播发
@ -1434,15 +1740,19 @@ class ChatApiStreamCallback: StreamCallback {
metadata = resultmap["meta"] as? String ?? ""
broadcast = resultmap["broadcast"] as? Bool ?? true
reply = !(resultmap["notreply"] as? Bool ?? true)
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult,
"meta":metadata,
])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(metadata)")
iscard = !(resultmap["iscard"] as? Bool ?? false)
if (iscard){
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult,
"meta":metadata,
])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(metadata)")
}
}catch{
print("liwei--------------- AI Call onFunctionCallResult 异常: \(error)")
}

17
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -547,19 +547,20 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "启动录音前台服务失败: ${e.message}")
}
}
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile()
}
if (!audioStream.isContinuousRecognitionActive) {
// 启动音频处理
Log.i(tag, "开启录音:audioSourceType${audioSourceType}")
// Initialize if not already done
if (!audioStream.isInitialized()) {
audioStream.initAudioRecord()
}
audioStream.setupStream()
audioStream.startAudioRecord( when (audioSourceType) {
AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE
AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL
@ -635,7 +636,7 @@ class AzureAsrHelper(private val context: Context) {
}
Log.i(tag, "关闭录音:")
audioStream.recordfile ?: return
if (!audioStream.isContinuousRecognitionActive) {
audioStream.stopMicrophoneCapture()

391
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -4,88 +4,93 @@ import MicrosoftCognitiveServicesSpeech
import os.log
// 自定义语音处理组件,提供音频流处理等功能
import speech
public class AzureTtsHelper: NSObject, ITtsService {
private let tag = "AzureTtsHelper"
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureTtsHelper")
private static let DEFAULT_LANGUAGE = "zh-CN"
private static let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
// Azure语音服务配置
private var speechConfig: SPXSpeechConfiguration?
private var synthesizer: SPXSpeechSynthesizer?
private var isInitialized = false
private var speaking = false
private var sessionid = "" //语音播报任务idsetAudioOutputRoute
private var sessionid = "" //语音播报任务id
// 当前配置
private var currentVoice = DEFAULT_VOICE
private var currentLanguage = DEFAULT_LANGUAGE
private var currentRate = "+0%"
private var currentPitch = "+0%"
private var currentVolume = "100%"
// 事件监听器列表
private var eventListeners = NSHashTable<AnyObject>.weakObjects()
private var audioDataListeners = NSHashTable<AnyObject>.weakObjects()
// 流式文本处理的缓冲区
private var streamBuffer = ""
private var lastSpeakTime: TimeInterval = 0
// 添加文本计数统计变量
private var totalTextCount = 0
private var currentSessionTextCount = 0
private var pendingTextCount = 0
// 内部异步处理队列
private let synthesisQueue = DispatchQueue(label: "com.azure.tts.synthesis", qos: .userInitiated)
private var isProcessing = false // 添加处理状态标志
private let synthesisGroup = DispatchGroup()
private var pendingTasks: [() -> Void] = []
private let taskLock = NSLock()
// 自定义音频输出流
private var customAudioOutputStream: SPXPushAudioOutputStream?
private var useInternalPlayer = true
private var micCapture: MicrophoneCapture!
// 记录最后播放的文本
private var lastSpokenText: String?
/**
* 初始化语音合成服务
*/
public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool {
do {
micCapture = MicrophoneCapture.shared
micCapture = MicrophoneCapture.shared
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource)
speechConfig?.speechSynthesisLanguage = language
currentLanguage = language
// 设置音频输出格式 - 与Android一致
speechConfig?.setPropertyTo("riff-16khz-16bit-mono-pcm",
speechConfig?.setPropertyTo("riff-16khz-16bit-mono-pcm",
byName: "SpeechServiceConnection_SynthOutputFormat")
// 设置低延迟属性
speechConfig?.setPropertyTo("300", byName: "SpeechServiceConnection_InitialSilenceTimeoutMs")
speechConfig?.setPropertyTo("300", byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
// 创建合成器
recreateSynthesizer()
// 设置初始化完成
isInitialized = true
// 新增:监听音频会话中断和路由变化,避免通话时崩溃
setupAudioSessionObservers()
// 预热TTS引擎
warmupSynthesizer()
return true
} catch {
os_log("语音合成服务初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
/**
* 设置自定义音频输出流
*/
@ -95,13 +100,13 @@ public class AzureTtsHelper: NSObject, ITtsService {
recreateSynthesizer()
}
}
/**
* 设置语音角色
*/
public func setVoice(_ voiceName: String) -> Bool {
if !isInitialized { return false }
do {
currentVoice = voiceName
speechConfig?.speechSynthesisVoiceName = voiceName
@ -116,48 +121,29 @@ public class AzureTtsHelper: NSObject, ITtsService {
return false
}
}
/**
* 开始语音合成会话
* @param sessionid 会话ID
* @return 是否成功启动
*/
public func startspeak(sessionid sessionid: String)-> Bool {
public func startspeak(sessionid sessionid: String) -> Bool {
self.sessionid = sessionid
print("startspeak sessionid \(sessionid)")
// 重置当前会话的文本计数
self.currentSessionTextCount = 0
self.pendingTextCount = 0
do {
// 新增:使用 AVAudioSession.sharedInstance 配置为“通话模式 + 扬声器外放”
// - 使用 .playAndRecord 以保证通话上下行链路
// - 使用 .voiceChat 获得通话处理(AEC 等)
// - 使用 .defaultToSpeaker 默认走外放
// - 通过 overrideOutputAudioPort(.speaker) 强制切到扬声器
let audioSession = AVAudioSession.sharedInstance()
//try audioSession.setActive(false)
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.defaultToSpeaker,
.mixWithOthers,
.defaultToSpeaker,
.allowBluetoothA2DP,
.allowBluetooth])
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
} catch {
print("音频会话配置失败: \(error.localizedDescription)")
}
// 使用安全的音频会话配置,兼容通话场景
safeConfigureAudioSessionForTTS()
return true
}
/**
* 单次合成并播放
*/
public func speakOnce(sessionid sessionid: String,_ text: String) -> Bool {
public func speakOnce(sessionid sessionid: String, _ text: String) -> Bool {
if !isInitialized {
os_log("语音合成未初始化", log: log, type: .error)
notifyEvent(eventType: .error, params: [
@ -166,30 +152,29 @@ public class AzureTtsHelper: NSObject, ITtsService {
])
return false
}
if (self.sessionid != sessionid){
return false
}
// 清理文本
let cleanedText = cleanTextForTTS(text)
if cleanedText.isEmpty {
return false
}
// 异步处理
enqueueSynthesisTask {
self.performSynthesis(sessionid: sessionid, text: cleanedText)
}
return true
}
/**
* 流式合成文本
*/
public func speakStream(sessionid sessionid: String,_ text: String) -> Bool {
public func speakStream(sessionid sessionid: String, _ text: String) -> Bool {
if !isInitialized {
notifyEvent(eventType: .error, params: [
"errorCode": "NOT_INITIALIZED",
@ -201,33 +186,31 @@ public class AzureTtsHelper: NSObject, ITtsService {
return false
}
self.sessionid = sessionid
if text.isEmpty {
return true
}
// 增加文本计数统计
totalTextCount += text.count
currentSessionTextCount += text.count
//print("流式合成文本\(text.count),总计文本数量:\(totalTextCount),当前会话:\(currentSessionTextCount)")
// 清理文本并添加到缓冲区
let cleanedText = cleanTextForTTS(text)
streamBuffer.append(cleanedText)
// 防抖逻辑 (150ms)
let currentTime = Date().timeIntervalSince1970
if currentTime - lastSpeakTime < 0.15 {
return true
}
lastSpeakTime = currentTime
let currentText = streamBuffer
// 标点符号列表 (与Android一致)
let punctuationMarks: [Character] = [".", "。", "!", "!","*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
let punctuationMarks: [Character] = [".", "。", "!", "!", "*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
// 从后往前查找最后一个标点符号
var lastPunctuationIndex = -1
for (i, char) in currentText.enumerated().reversed() {
@ -236,21 +219,21 @@ public class AzureTtsHelper: NSObject, ITtsService {
break
}
}
// 播放到找到的标点符号
if lastPunctuationIndex >= 0 {
let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1))
let startIndex = currentText.index(currentText.startIndex, offsetBy: lastPunctuationIndex + 1)
streamBuffer = String(currentText[startIndex...])
if !textToSpeak.isEmpty {
return speakOnce(sessionid: sessionid, textToSpeak)
}
}
return true
}
/**
* 刷新并播放流式文本
*/
@ -262,17 +245,17 @@ public class AzureTtsHelper: NSObject, ITtsService {
])
return false
}
let remainingText = streamBuffer
streamBuffer = ""
if remainingText.isEmpty {
return true
}
return speakOnce(sessionid:sessionid,remainingText)
return speakOnce(sessionid: sessionid, remainingText)
}
/**
* 停止语音合成
*/
@ -286,12 +269,12 @@ public class AzureTtsHelper: NSObject, ITtsService {
taskLock.lock()
pendingTasks.removeAll()
taskLock.unlock()
do {
print("liwei-------------停止语音合成播放")
try self.synthesizer?.stopSpeaking()
self.notifyEvent(eventType: .synthesisCanceled)
if !micCapture.isCapturing{
// 停止时设置音频会话为非活跃状态
// try AVAudioSession.sharedInstance().setActive(false)
@ -305,7 +288,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
"errorMessage": error.localizedDescription
])
}
synthesisQueue.async {
do {
self.synthesizer = nil
@ -314,28 +297,10 @@ public class AzureTtsHelper: NSObject, ITtsService {
os_log("重置语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
}
}
return true
}
/**
* 释放资源
*/
public func dispose() {
_ = stop()
synthesisGroup.wait()
synthesisQueue.sync {
synthesizer = nil
speechConfig = nil
customAudioOutputStream = nil
isInitialized = false
speaking = false
taskLock.lock()
pendingTasks.removeAll()
taskLock.unlock()
}
}
/**
* 添加事件监听器
@ -343,28 +308,28 @@ public class AzureTtsHelper: NSObject, ITtsService {
public func addListener(_ listener: TtsEventListener) {
eventListeners.add(listener as AnyObject)
}
/**
* 移除事件监听器
*/
public func removeListener(_ listener: TtsEventListener) {
eventListeners.remove(listener as AnyObject)
}
/**
* 添加音频数据监听器
*/
public func addAudioDataListener(_ listener: AudioDataListener) {
audioDataListeners.add(listener as AnyObject)
}
/**
* 移除音频数据监听器
*/
public func removeAudioDataListener(_ listener: AudioDataListener) {
audioDataListeners.remove(listener as AnyObject)
}
/**
* 设置是否使用内部播放器
*/
@ -374,6 +339,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
recreateSynthesizer()
}
}
/**
* 设置音频输出设备
*/
@ -383,6 +349,12 @@ public class AzureTtsHelper: NSObject, ITtsService {
if wasSpeaking {
_ = stop()
}
// 新增:在通话期间不进行音频设备切换,避免崩溃
if isTelephonyActive() {
os_log("检测到通话进行中,跳过音频设备切换以避免崩溃", log: log, type: .info)
return true
}
// 重新创建合成器以应用新设备
if isInitialized {
@ -397,7 +369,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
return true
}
// MARK: - 私有方法
/**
@ -411,6 +383,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
// 确保任务被处理
processTasksIfNeeded()
}
/**
* 处理任务队列
*/
@ -460,7 +433,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
self.processNextTask()
}
}
/**
* 执行语音合成
*/
@ -672,7 +645,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
currentVolume = "\(min(max(volume, 0), 100))%"
return true
}
/**
* 是否正在播放
*/
@ -704,5 +677,215 @@ public func resetTextCount() {
}
/**
* 音频会话并发防护和状态
*/
private let audioSessionQueue = DispatchQueue(label: "com.azure.tts.audioSession")
private var audioInterrupted = false
private var audioSessionObserversAdded = false
// 新增:路由变化的去抖与配置重入保护标记
private var routeChangeDebounceWorkItem: DispatchWorkItem?
private var isApplyingAudioSessionConfig = false
/**
* 监听并处理音频中断与路由变化(通话/Siri/蓝牙切换等)
* - 目的:在通话期间或路由变化时安全地停止/重配,避免 AVAudioSession 断言导致崩溃
*/
private func setupAudioSessionObservers() {
if audioSessionObserversAdded { return }
audioSessionObserversAdded = true
NotificationCenter.default.addObserver(self,
selector: #selector(handleAudioInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance())
NotificationCenter.default.addObserver(self,
selector: #selector(handleRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: AVAudioSession.sharedInstance())
}
/**
* 判断当前是否处于通话相关音频占用(电话或蓝牙 HFP)
* - 使用 secondaryAudioShouldBeSilencedHint 与蓝牙 HFP 路由作为判断依据
*/
private func isTelephonyActive() -> Bool {
let session = AVAudioSession.sharedInstance()
if session.secondaryAudioShouldBeSilencedHint { return true }
// 检查是否使用蓝牙 HFP(通话用的 Hands-Free Profile)
for out in session.currentRoute.outputs {
if out.portType == .bluetoothHFP {
return true
}
}
return false
}
/**
* 安全配置音频会话用于 TTS,兼容通话场景
* - 通话中:使用 .playback + .duckOthers,避免 .voiceChat/.allowBluetoothA2DP 导致断言
* - 非通话:使用 .playAndRecord + .voiceChat + .allowBluetooth(不使用 A2DP)
*/
/// 为 TTS 安全配置音频会话:
/// - 使用异步队列,避免在同一串行队列上 sync 导致死锁
/// - 根据当前音频路由(蓝牙 A2DP / 蓝牙 HFP / 有线耳机)选择更合适的类别与模式
/// - 仅当类别或模式发生变化时才 setCategory,降低 AVAudioSessionRouteChangeReason.categoryChange 的触发概率
private func safeConfigureAudioSessionForTTS() {
// 使用异步,避免串行队列重入自锁
audioSessionQueue.async { [weak self] in
guard let self = self else { return }
// 防重入:配置中不重复进入
if self.isApplyingAudioSessionConfig { return }
self.isApplyingAudioSessionConfig = true
defer { self.isApplyingAudioSessionConfig = false }
let audioSession = AVAudioSession.sharedInstance()
// 显式指定 outputs 的类型,确保闭包参数类型可推断
let outputs: [AVAudioSessionPortDescription] = audioSession.currentRoute.outputs
// 使用 contains(where:) 明确谓词闭包。
// 保持使用 audioSessionQueue.async,防止在串行队列上自锁。
// 仅在类别或模式发生变化时调用 setCategory,减少 categoryChange 的回调风暴。
let hasBluetoothHFP = outputs.contains(where: { $0.portType == .bluetoothHFP })
let hasBluetoothA2DP = outputs.contains(where: { $0.portType == .bluetoothA2DP })
let hasWiredHeadphones = outputs.contains(where: { $0.portType == .headphones || $0.portType == .headsetMic })
// 计算目标配置
let desiredCategory: AVAudioSession.Category
let desiredMode: AVAudioSession.Mode
var desiredOptions: AVAudioSession.CategoryOptions = [.mixWithOthers]
if self.isTelephonyActive() || hasBluetoothHFP {
// 通话活跃或存在 HFP(语音)链路:使用语音聊天模式
desiredCategory = .playAndRecord
desiredMode = .voiceChat
desiredOptions.insert(.allowBluetooth) // HFP 需 allowBluetooth
} else if hasBluetoothA2DP || hasWiredHeadphones {
// 纯播放链路耳机(A2DP/有线耳机):避免强制切语音链路
desiredCategory = .playback
desiredMode = .default
desiredOptions.insert(.allowBluetoothA2DP)
} else {
// 默认:本机扬声器的语音链路
desiredCategory = .playAndRecord
desiredMode = .voiceChat
desiredOptions.insert(.defaultToSpeaker)
desiredOptions.insert(.allowBluetooth) // 允许外接麦/耳机(非 HFP 也不冲突)
}
do {
// 仅当类别或模式变化时设置,减少无谓的 categoryChange
if audioSession.category != desiredCategory || audioSession.mode != desiredMode {
try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions)
}
// 激活会话(通常不会引发死锁),失败时仅记录错误
try audioSession.setActive(true)
} catch {
os_log("TTS 音频会话配置失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
}
}
}
/**
* 处理系统音频中断(来电/通话开始、Siri等)
* - began:标记中断并停止当前合成播放
* - ended:清除标记并按场景重配音频会话
*/
@objc private func handleAudioInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
audioInterrupted = true
_ = stop()
os_log("音频中断开始(可能为通话/Siri),停止TTS", log: log, type: .info)
case .ended:
audioInterrupted = false
os_log("音频中断结束,重新配置会话", log: log, type: .info)
safeConfigureAudioSessionForTTS()
@unknown default:
break
}
}
/**
* 处理音频路由变化(设备插拔、蓝牙切换、扬声器/听筒切换等)
* - 忽略 .categoryChange/.routeConfigurationChange 等由我们设置类别引发的变化,避免死循环
* - 配置进行中(isApplyingAudioSessionConfig)直接忽略,防止重入
* - 加入 300ms 去抖,汇聚瞬时多次变化后再重配
* - 配置过程中(isApplyingAudioSessionConfig = true)直接忽略
*/
@objc private func handleRouteChange(_ notification: Notification) {
guard let info = notification.userInfo,
let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
os_log("音频路由变化: %{public}@", log: log, type: .info, String(describing: reason))
// 配置中触发的路由变化,直接忽略,避免重入导致死循环
if isApplyingAudioSessionConfig {
os_log("配置进行中,忽略本次路由变化", log: log, type: .debug)
return
}
// 忽略会造成循环的原因:categoryChange(你的日志 reason=3),以及 routeConfigurationChange 等
switch reason {
case .categoryChange, .routeConfigurationChange, .wakeFromSleep, .noSuitableRouteForCategory:
os_log("忽略路由变化原因: %{public}@", log: log, type: .debug, String(describing: reason))
return
default:
break
}
// 通话期间避免重配,避免与电话路由冲突
if isTelephonyActive() {
os_log("通话期间检测到路由变化,跳过重配", log: log, type: .info)
return
}
// 去抖处理:300ms 内只执行最后一次重配
// 忽略 categoryChange 等
// 去抖:在 audioSessionQueue 上延迟调用
routeChangeDebounceWorkItem?.cancel()
let work = DispatchWorkItem { [weak self] in
self?.safeConfigureAudioSessionForTTS()
}
routeChangeDebounceWorkItem = work
audioSessionQueue.asyncAfter(deadline: .now() + 0.3, execute: work)
}
/**
* 释放资源
*/
public func dispose() {
_ = stop()
synthesisGroup.wait()
synthesisQueue.sync {
synthesizer = nil
speechConfig = nil
customAudioOutputStream = nil
isInitialized = false
speaking = false
taskLock.lock()
pendingTasks.removeAll()
taskLock.unlock()
}
// 新增:移除音频会话观察者,防止泄漏
if audioSessionObserversAdded {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
audioSessionObserversAdded = false
}
}
}

26
local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift

@ -444,6 +444,32 @@ public class MicrophoneCapture: NSObject {
// 安全地处理音频会话
guard let audioSession = audioSession else { return }
do {
try audioSession.setActive(false)
if self.hasBluetoothDevices {
print("耳机")
// 设置音频会话参数
try audioSession.setCategory(.playback,
mode: .videoChat,
options: [
.allowBluetoothA2DP,
.allowBluetooth
]) // 添加音频优先级控制
} else {
print("手机")
try audioSession.setCategory(.playback,
mode: .videoChat,
options: [
.mixWithOthers,
.defaultToSpeaker])
}
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
} catch {
print("音频会话配置失败: \(error.localizedDescription)")
}
}

95
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -2,6 +2,7 @@ import Foundation
import AVFoundation
import MicrosoftCognitiveServicesSpeech
import os.log
import CallKit
/**
* 简单音频接收器类,用于处理音频录制和流传输
@ -15,6 +16,9 @@ public class SimpleAudioReceiver: NSObject {
private let tag = "SimpleAudioReceiver"
private let log = OSLog(subsystem: "com.azure.speech", category: "SimpleAudioReceiver")
// 用于检测系统电话通话状态(iOS10+)
private let callObserver = CXCallObserver()
/**
* 麦克风捕获实例
* 使用 MicrophoneCapture 的单例模式
@ -187,32 +191,27 @@ public class SimpleAudioReceiver: NSObject {
/**
* 运行麦克风捕获
* 针对通话/非通话场景做安全的音频会话配置,避免因会话类别/模式不兼容导致启动录音失败
*/
private func runMicrophoneCapture() {
do {
// 确保正在写入状态
guard self._isWriting else { return }
// 设置音频会话参数
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.defaultToSpeaker,
.mixWithOthers,
.defaultToSpeaker,
.allowBluetoothA2DP,
.allowBluetooth])
try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true)
try micCapture.startCapture()
try micCapture.audioDataHandler = { data in
// 根据通话状态进行安全的音频会话配置
try safeConfigureAudioSessionForMic()
// 启动麦克风采集
try micCapture.startCapture()
// 捕获到的数据放入写入队列(后续处理)
try micCapture.audioDataHandler = { data in
self.writeQueue.put(data)
}
// // 将数据放入写入队列(后续处理)
// self.writeQueue.put(data)
} catch {
print("麦克风启动失败: \(error)")
// 【新增】添加详细错误处理
// 详细错误处理
if let nsError = error as NSError? {
print("错误域: \(nsError.domain), 错误代码: \(nsError.code)")
print("错误描述: \(nsError.localizedDescription)")
@ -243,8 +242,68 @@ public class SimpleAudioReceiver: NSObject {
}
}
/**
* 继续麦克风捕获
* 在通话/非通话场景下安全配置音频会话用于麦克风采集
* - 通话中:使用 .record + .voiceChat + [.allowBluetooth],避免 A2DP 和强制外放,兼容电话路由
* - 非通话:使用 .playAndRecord + .videoChat + [.allowBluetooth, .mixWithOthers, .defaultToSpeaker]
* - 失败兜底:退回到最简单的 .record + .default,确保录音尽可能可用
*/
private func safeConfigureAudioSessionForMic() throws {
let audioSession = self.audioSession
do {
// 计算目标配置
let desiredCategory: AVAudioSession.Category
let desiredMode: AVAudioSession.Mode
let desiredOptions: AVAudioSession.CategoryOptions
if isTelephonyActive() {
// 通话中:录音优先,避免 A2DP(仅播放),改用 HFP(.allowBluetooth)
desiredCategory = .record
desiredMode = .voiceChat
desiredOptions = [.allowBluetooth]
} else {
// 非通话:保持原有逻辑,但使用 .allowBluetooth 以支持录音(而不是 A2DP)
desiredCategory = .playAndRecord
desiredMode = .videoChat
//desiredOptions = [.allowBluetooth, .mixWithOthers, .defaultToSpeaker]
desiredOptions = [.mixWithOthers, .defaultToSpeaker]
}
// 仅在类别或模式变化时才调用 setCategory,避免触发不必要的 categoryChange
let needsSetCategory = (audioSession.category != desiredCategory) || (audioSession.mode != desiredMode)
if needsSetCategory {
try audioSession.setCategory(desiredCategory,
mode: desiredMode,
options: desiredOptions)
} else {
// 类别与模式一致时,也尽量避免重复 setCategory;如需确保 options,可在确有必要时再设置
// 如果你确实需要校正 options,可取消下一行注释,但这可能再次触发 categoryChange
// try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions)
}
// 保持激活;这里重复 setActive(true) 一般安全,但如果你观察到重复激活也触发路由回调,可在外层控制频率
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
// 兜底:尽量保证能开始录音
print("音频会话配置失败,尝试兜底配置: \(error)")
try audioSession.setCategory(.record, mode: .default, options: [])
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
}
}
/**
* 检测是否存在正在进行的系统电话通话
* - iOS10+:使用 CallKit 的 CXCallObserver
* - 旧系统:使用 secondaryAudioShouldBeSilencedHint 近似判断
*/
private func isTelephonyActive() -> Bool {
if #available(iOS 10.0, *) {
return callObserver.calls.contains { !$0.hasEnded }
} else {
// 非精确,但可作为兜底判断:若需要静音则可能有电话或系统占用
return AVAudioSession.sharedInstance().secondaryAudioShouldBeSilencedHint
}
}
/**
* 继续麦克风捕获
*/

50
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -364,7 +364,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
Log.d(TAG, "liwei-------------------------开始AI 对话 0")
// Log.d(TAG, "liwei-------------------------开始AI 对话 0")
currentStreamJob = launch {
try {
// 转换消息格式
@ -374,22 +374,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
throw e
}
Log.d(TAG, "liwei-------------------------开始AI 对话 1")
// Log.d(TAG, "liwei-------------------------开始AI 对话 1")
// 直接获取工具列表
val tools = getOpenAiTools()
Log.d(TAG, "liwei-------------------------开始AI 对话 2")
// Log.d(TAG, "liwei-------------------------开始AI 对话 2")
// 构建请求
if (currentModel.isEmpty()) {
Log.e("ChatApiService", "模型名称为空")
throw IllegalArgumentException("模型名称不能为空")
}
Log.d(TAG, "liwei-------------------------开始AI 对话 3")
// Log.d(TAG, "liwei-------------------------开始AI 对话 3")
if (chatMessages.isEmpty()) {
Log.e("ChatApiService", "消息列表为空")
throw IllegalArgumentException("消息列表不能为空")
}
// Log.e("ChatApiService", "AI解析工具列表: ${tools}")
Log.d(TAG, "liwei-------------------------开始AI 对话 4")
// Log.d(TAG, "liwei-------------------------开始AI 对话 4")
val chatsFlow = try {
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(currentModel),
@ -398,17 +398,17 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
temperature = 0.7,
tools = if (tools.isNotEmpty()) tools else null
)
Log.d(TAG, "liwei-------------------------开始AI 对话 5")
// Log.d(TAG, "liwei-------------------------开始AI 对话 5")
if (openAI == null) {
Log.e("ChatApiService", "openAI对象为null")
throw IllegalStateException("OpenAI客户端未初始化")
}
Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}")
// 添加超时控制,10秒超时
val flow = withTimeout(10.seconds) {
openAI!!.chatCompletions(chatCompletionRequest)
}
Log.d(TAG, "liwei-------------------------开始AI 对话 6")
// Log.d(TAG, "liwei-------------------------开始AI 对话 6")
flow
} catch (e: Exception) {
Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e)
@ -425,7 +425,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
var replyContext = "";
Log.d(TAG, "liwei-------------------------开始AI 对话 7")
// Log.d(TAG, "liwei-------------------------开始AI 对话 7")
chatsFlow.collect { result ->
// val usage = result.usage
// if (usage != null){ //发现流式不返回这歌数据
@ -436,19 +436,19 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// throw CancellationException("Session cancelled")
return@collect
}
Log.d(TAG, "liwei-------------------------开始AI 对话 7-1")
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-1")
val choice = result.choices.firstOrNull()
if (choice == null) {
Log.w("ChatApiService", "[Session: $sessionid] choice为null")
return@collect
}
Log.d(TAG, "liwei-------------------------开始AI 对话 7-2")
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-2")
val delta = choice.delta
if (delta == null) {
Log.w("ChatApiService", "[Session: $sessionid] delta为null")
return@collect
}
Log.d(TAG, "liwei-------------------------开始AI 对话 7-3")
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-3")
if (delta.toolCalls == null){
// 处理普通文本内容
delta.content?.let { content ->
@ -461,7 +461,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
}
}
Log.d(TAG, "liwei-------------------------开始AI 对话 7-4")
// Log.d(TAG, "liwei-------------------------开始AI 对话 7-4")
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
try {
@ -513,10 +513,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
val promptTokens = estimatePromptTokens(chatMessages); //计算上传token
val completionTokens = roughTokenCount(replyContext); //计算上传token
getSessionCallback(sessionid)?.onUsage(sessionid,promptTokens,completionTokens,promptTokens+completionTokens)
Log.d(TAG, "liwei-------------------------开始AI 对话 8")
// Log.d(TAG, "liwei-------------------------开始AI 对话 8")
// 完成处理
if (sessionid == currSessionId) {
Log.d(TAG, "liwei-------------------------开始AI 对话 9")
// Log.d(TAG, "liwei-------------------------开始AI 对话 9")
// 检查是否有工具调用需要处理
val hasToolCalls = processToolCalls(sessionid)
if (!hasToolCalls) {
@ -528,7 +528,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
clearCurrentSession()
}
}
Log.d(TAG, "liwei-------------------------开始AI 对话 10")
// Log.d(TAG, "liwei-------------------------开始AI 对话 10")
} catch (e: CancellationException) {
// 会话被取消,不需要处理
Log.d(TAG, "Session $sessionid cancelled")
@ -652,7 +652,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
mapOf("context" to "Tool not found: $functionName")
}
if (sessionid == currSessionId) {
if (functionName != "set_user_profile_field"){
// if (functionName != "set_user_profile_field"){
// 处理结果
try {
if(!isError){
@ -674,14 +674,14 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
functionCall = functionCall,
functionResult = gson.toJson(result)
)
}else{
try {
getSessionCallback(sessionid)?.onComplete(sessionid)
} catch (ex: Exception) {
Log.e(TAG, "onComplete回调异常: ${ex.message}", ex)
}
clearCurrentSession()
}
// }else{
// try {
// getSessionCallback(sessionid)?.onComplete(sessionid)
// } catch (ex: Exception) {
// Log.e(TAG, "onComplete回调异常: ${ex.message}", ex)
// }
// clearCurrentSession()
// }
}
}
} catch (e: Exception) {

119
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt

@ -27,16 +27,16 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
*/
fun registerAllFunctions(client: MCPClient) {
try {
// Only register QQ Music search function
// 注册QQ音乐搜索功能(仅搜索,不播放)
client.registerLocalFunction(
"qqmusic_searchandplay_songs",
"在QQ音乐中搜索并播放歌曲,支持歌曲、专辑、歌手等搜索类型",
"qqmusic_search_songs",
"在QQ音乐中搜索歌曲,支持歌曲、专辑、歌手等搜索类型",
mapOf(
"type" to "object",
"properties" to mapOf(
"keyword" to mapOf(
"type" to "string",
"description" to "搜索关键词,歌曲名、专辑名或歌手名"
"description" to "搜索关键词,歌曲名、专辑名或歌手名或者类型"
),
"searchType" to mapOf(
"type" to "integer",
@ -46,7 +46,30 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
),
"required" to listOf("keyword")
),
QQMusicSearchHandler(context)
QQMusicSearchOnlyHandler(context)
)
// 注册QQ音乐播放歌曲列表功能
client.registerLocalFunction(
"qqmusic_play_songs",
"播放指定的QQ音乐歌曲列表",
mapOf(
"type" to "object",
"properties" to mapOf(
"songMids" to mapOf(
"type" to "array",
"items" to mapOf("type" to "string"),
"description" to "歌曲MID列表"
),
"index" to mapOf(
"type" to "integer",
"description" to "开始播放的歌曲索引,默认为0",
"default" to 0
)
),
"required" to listOf("songMids")
),
QQMusicPlaySongsHandler(context)
)
client.registerLocalFunction(
@ -111,8 +134,8 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
}
}
private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler {
// 新的仅搜索处理器(不包含播放功能)
private class QQMusicSearchOnlyHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
return try {
val keyword = arguments["keyword"] as? String ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}"
@ -124,7 +147,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
qqMusicSingleton.searchSongs(keyword) { searchResult ->
val result = if (searchResult.isSuccess) {
val songs = searchResult.getOrNull() ?: emptyList()
// 根据搜索类型处理结果
val processedSongs = when (searchType) {
0 -> { // 歌曲搜索,返回所有结果
@ -138,7 +161,7 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
val songsJson = processedSongs.joinToString(",") { song ->
val songMap = song as Map<String, Any?>
// 添加调试日志
Log.d("QQMusicSearchHandler", "原始数据: $songMap")
Log.d("QQMusicSearchOnlyHandler", "原始数据: $songMap")
// 修正字段名映射
val mid = songMap["mid"] ?: "unknown"
val name = songMap["name"] ?: "unknown"
@ -148,7 +171,12 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
val duration = songMap["duration"] ?: 0
"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}"
}
"{\"success\": true,\"iscard\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}"
if (songs.isNotEmpty()) {
"{\"success\": true, \"message\": \"搜索成功\",\"iscard\": true,\"broadcast\": false, \"data\": [$songsJson]}"
} else {
"{\"success\": false, \"message\": \"未找到相关歌曲\"}"
}
} else {
val error = searchResult.exceptionOrNull()
"{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}"
@ -158,64 +186,11 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand
}
}
} catch (e: Exception) {
Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e)
Log.e("QQMusicSearchOnlyHandler", "QQ音乐搜索失败", e)
"{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}"
}
}
}
//private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler {
// override suspend fun handle(arguments: Map<String, Any>): String {
// return try {
// val keyword = arguments["keyword"] as? String ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}"
// val searchType = arguments["searchType"] as? Int ?: 0 // 默认搜索歌曲
// val qqMusicSingleton = QQMusicSingleton.getInstance()
// var result = ""
//
// // 直接调用搜索,内部会自动处理登录检查
// qqMusicSingleton.searchSongs(keyword) { searchResult ->
// result = if (searchResult.isSuccess) {
// val songs = searchResult.getOrNull() ?: emptyList()
//
// // 根据搜索类型处理结果
// val processedSongs = when (searchType) {
// 0 -> { // 歌曲搜索,只返回第一个结果
//// if (songs.isNotEmpty()) listOf(songs.first()) else emptyList()
// songs
// }
// else -> { // 专辑、歌手等其他类型,返回所有结果
// songs
// }
// }
//
// val songsJson = processedSongs.joinToString(",") { song ->
// val songMap = song as Map<String, Any?>
// // 添加调试日志
// Log.d("QQMusicSearchHandler", "原始数据: $songMap")
// // 修正字段名映射
// val mid = songMap["mid"] ?: "unknown"
// val name = songMap["name"] ?: "unknown"
// val artist = songMap["singer"] ?: "unknown"
// val album = songMap["album"] ?: ""
// val pic = songMap["pic"] ?: ""
// val duration = songMap["duration"] ?: 0
// "{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}"
// }
// "{\"success\": true, \"broadcast\": false, \"message\": \"搜索成功,增在播放\", \"data\": [$songsJson]}"
// } else {
// val error = searchResult.exceptionOrNull()
// "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}"
// }
// }
//
// // 等待回调完成
// delay(5000)
// result.ifEmpty { "{\"success\": false, \"message\": \"搜索超时\"}" }
// } catch (e: Exception) {
// Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e)
// "{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}"
// }
// }
//}
private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
@ -228,16 +203,16 @@ private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionH
qqMusicSingleton.playSongs(songMids, index) { playResult ->
result = if (playResult.isSuccess) {
"{\"nocard\": true, \"success\": true, \"message\": \"开始播放歌曲列表\"}"
"{\"broadcast\": false, \"success\": true, \"message\": \"开始播放歌曲列表\"}"
} else {
val error = playResult.exceptionOrNull()
"{\"nocard\": true, \"success\": false, \"message\": \"播放失败: ${error?.message}\"}"
"{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}"
}
}
// 等待回调完成
delay(3000)
result.ifEmpty { "{\"nocard\": true, \"success\": false, \"message\": \"播放超时\"}" }
// 等待回调完成 十秒
delay(10000)
result.ifEmpty { "{\"nocard\": false,\"broadcast\": true, \"success\": false, \"message\": \"播放超时\"}" }
} catch (e: Exception) {
Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e)
"{\"nocard\": true, \"success\": false, \"message\": \"播放异常: ${e.message}\"}"
@ -253,14 +228,14 @@ private class QQMusicPlayHandler(private val context: Context?) : FunctionHandle
qqMusicSingleton.playMusic { playResult ->
result = if (playResult.isSuccess) {
"{\"success\": true, \"message\": \"开始播放\"}"
"{\"broadcast\": false, \"success\": true, \"message\": \"开始播放\"}"
} else {
val error = playResult.exceptionOrNull()
"{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}"
}
}
delay(2000)
delay(10000)
result.ifEmpty { "{\"success\": false, \"message\": \"播放超时\"}" }
} catch (e: Exception) {
Log.e("QQMusicPlayHandler", "QQ音乐播放失败", e)

190
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -81,10 +81,32 @@ class SystemFunctionHandler(private val context: Context? = null) {
handler = SendTextMessageHandler(context)
)
// 注册获取联系人列表函数
client.registerLocalFunction(
name = "get_contacts_list",
description = "获取用户通信录联系人列表",
parameters = mapOf(
"type" to "object",
"properties" to mapOf(
"limit" to mapOf(
"type" to "integer",
"description" to "返回联系人数量限制,默认20个",
"default" to 20
),
"search" to mapOf(
"type" to "string",
"description" to "搜索关键词,可选参数"
)
),
"required" to emptyList<String>()
),
handler = GetContactsListHandler(context)
)
// 注册拨打电话函数
client.registerLocalFunction(
name = "make_phone_call",
description = "拨打电话",
description = "拨打电话给指定联系人",
parameters = mapOf(
"type" to "object",
"properties" to mapOf(
@ -1328,6 +1350,172 @@ private class MaxVolumeHandler(private val context: Context?) : FunctionHandler
}
/**
* 获取联系人列表处理器
*/
private class GetContactsListHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
val handler = SystemFunctionHandler(context)
// 检查读取联系人权限
if (!handler.checkPermissionAndGuide(Manifest.permission.READ_CONTACTS, "读取联系人", "获取联系人列表")) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要联系人权限才能获取通信录,请在权限管理中开启联系人权限")
})
}.toString()
}
return try {
val limit = (arguments["limit"] as? Number)?.toInt() ?: 20
val searchKeyword = arguments["search"] as? String
val contacts = getContactsList(context, limit, searchKeyword)
JSONObject().apply {
put("result", "成功获取联系人列表")
put("count", contacts.size)
put("contacts", contacts)
}.toString()
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "获取联系人列表失败: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "fetch_failed")
put("message", "获取联系人列表失败: ${e.message}")
})
}.toString()
}
}
/**
* 获取联系人列表
* @param context 上下文
* @param limit 返回数量限制
* @param searchKeyword 搜索关键词
* @return 联系人列表
*/
private fun getContactsList(context: Context, limit: Int, searchKeyword: String?): List<Map<String, String>> {
val contacts = mutableListOf<Map<String, String>>()
try {
val contentResolver = context.contentResolver
// 构建查询条件
val selection = if (!searchKeyword.isNullOrBlank()) {
"${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?"
} else {
null
}
val selectionArgs = if (!searchKeyword.isNullOrBlank()) {
arrayOf("%$searchKeyword%")
} else {
null
}
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
)
// 按姓名排序,限制数量
val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME} ASC LIMIT $limit"
contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID)
val nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
val hasPhoneColumn = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)
while (cursor.moveToNext() && contacts.size < limit) {
val contactId = cursor.getString(idColumn)
val displayName = cursor.getString(nameColumn) ?: "未知联系人"
val hasPhoneNumber = cursor.getInt(hasPhoneColumn) > 0
if (hasPhoneNumber) {
// 获取该联系人的电话号码
val phoneNumbers = getPhoneNumbersForContact(context, contactId)
if (phoneNumbers.isNotEmpty()) {
contacts.add(mapOf(
"id" to contactId,
"name" to displayName,
"phone" to phoneNumbers.first(), // 使用第一个电话号码
"allPhones" to phoneNumbers.joinToString(", ") // 所有电话号码
))
}
}
}
}
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "查询联系人失败: ${e.message}", e)
}
return contacts
}
/**
* 获取指定联系人的所有电话号码
* @param context 上下文
* @param contactId 联系人ID
* @return 电话号码列表
*/
private fun getPhoneNumbersForContact(context: Context, contactId: String): List<String> {
val phoneNumbers = mutableListOf<String>()
try {
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
val phoneProjection = arrayOf(
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE
)
val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?"
val phoneSelectionArgs = arrayOf(contactId)
context.contentResolver.query(
phoneUri,
phoneProjection,
phoneSelection,
phoneSelectionArgs,
null
)?.use { phoneCursor ->
val numberColumn = phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
while (phoneCursor.moveToNext()) {
val phoneNumber = phoneCursor.getString(numberColumn)
if (!phoneNumber.isNullOrBlank()) {
phoneNumbers.add(phoneNumber)
}
}
}
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "获取联系人电话号码失败: ${e.message}", e)
}
return phoneNumbers
}
}
/**
* 更新用户画像
*/

10
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -520,7 +520,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool {
}
if sessionId == self.currSessionId {
if (firstToolCall.name != "set_user_profile_field"){
// if (firstToolCall.name != "set_user_profile_field"){
// 处理结果
if (!isError){
self.getSessionCallback(sessionId)?.onFunctionCallResult(
@ -541,10 +541,10 @@ private func processToolCalls(_ sessionId:String) async -> Bool {
functionCall: functionCall,
functionResult: self.jsonToString(result) ?? "{}"
)
}else{
self.getSessionCallback(sessionId)?.onComplete(sessionId)
self.clearCurrentSession()
}
// }else{
// self.getSessionCallback(sessionId)?.onComplete(sessionId)
// self.clearCurrentSession()
// }
}
}
} catch {

1
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift

@ -67,6 +67,7 @@ private class MusicSearchHandler: FunctionHandler {
let result: [String: Any] = [
"success": true,
"broadcast": false,
"iscard": true,
"message": "搜索成功,正在播放",
"data": songsJson
]

1
local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift

@ -162,6 +162,7 @@ private class QQMusicSearchHandler: FunctionHandler {
result = [
"success": true,
"message": "Found \(items.count) songs",
"iscard": true,
"broadcast": false,
"data": songs
]

14
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

@ -270,13 +270,19 @@ object LocationService {
if (!hasForegroundPermission) {
return 1 // 前台权限未授予
}
// 前台权限已授予,再根据前后台状态检查后台权限
if (!isAppInForeground(context)) {
// 应用在后台:Android 10+ 需要检查后台权限
return 2 // 后台权限未授予
// 应用在后台:Android 10+ 需要检查后台权限的实际授予状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val hasBackgroundPermission = hasBackgroundLocationPermission(context)
if (!hasBackgroundPermission) {
return 2 // 后台权限未授予
}
}
// Android 9 及以下或已授予后台权限
}
// 所有所需权限均已授予
return 0
}

2
local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift

@ -323,7 +323,7 @@ extension LocationService: CLLocationManagerDelegate {
// 过滤过旧的位置(超过5秒)
let locationAge = -location.timestamp.timeIntervalSinceNow
guard locationAge < 5.0 else {
guard locationAge < 360 else {
print("[LocationService] 位置数据过旧(\(locationAge)秒),忽略")
return
}

72
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt

@ -1,6 +1,7 @@
package com.yunqiinnovation.qq_music
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.os.Bundle
import android.os.Handler
@ -18,12 +19,12 @@ import org.json.JSONTokener
* 负责处理QQ音乐的登录授权流程
*/
class QQMusicLoginManager(private val context: Context) {
companion object {
private const val TAG = "QQMusicLoginManager"
private const val BASE_SCHEME = "voitrans://"
private const val URI_LOGIN = BASE_SCHEME + "login"
// 定义错误码常量,替代 LoginExampleActivity 中的常量
const val LOGIN_FAILED = 1
}
@ -32,7 +33,7 @@ class QQMusicLoginManager(private val context: Context) {
private var qqMusicApi: IQQMusicApi? = null
private lateinit var qqMusicApiWrapper: QQMusicApiWrapper
private val handler = Handler(Looper.getMainLooper())
/**
* 登录结果回调接口
*/
@ -51,63 +52,63 @@ class QQMusicLoginManager(private val context: Context) {
*/
fun startLogin(callback: LoginCallback) {
Log.d(TAG, "开始QQ音乐登录流程")
// 检查配置
if (Config.OPENID_APPID.isEmpty() || Config.OPENID_APP_PRIVATE_KEY.isEmpty()) {
callback.onError("CONFIG_ERROR", "请先配置APPID和私钥")
return
}
// 初始化绑定助手
qPlayBindHelper = QPlayBindHelper(context)
// 开始绑定QQ音乐服务
bindQQMusicService(callback)
}
/**
* 绑定QQ音乐服务
*/
private fun bindQQMusicService(callback: LoginCallback) {
Log.d(TAG, "开始绑定QQ音乐服务")
qPlayBindHelper.ensureQQMusicBindByStartProcess { success ->
if (success && qPlayBindHelper.isBindQQMusic()) {
Log.d(TAG, "QQ音乐服务绑定成功")
qqMusicApi = qPlayBindHelper.getQQMusicApi()!!
qqMusicApiWrapper = QQMusicApiWrapper(qqMusicApi!!)
// 检查是否已经授权
checkAuthStatus(callback)
} else {
Log.e(TAG, "QQ音乐服务绑定失败")
callback.onError("APP_NOT_INSTALLED", "请先安装QQ音乐App才能使用音乐功能")
}
}
}
/**
* 检查授权状态
*/
private fun checkAuthStatus(callback: LoginCallback) {
Log.d(TAG, "检查授权状态")
val bundle = Bundle().apply {
putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION)
putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM)
}
try {
val result = qqMusicApi?.execute("hi", bundle)
val code = result?.getInt(Keys.API_RETURN_KEY_CODE)
when (code) {
ErrorCodes.ERROR_OK -> {
// 已授权,直接请求登录信息
Log.d(TAG, "已授权,直接获取登录信息")
// isBindQQMusicService = true
startAuthProcess2(callback)
startAuthProcess(callback)
}
ErrorCodes.ERROR_API_NO_PERMISSION -> {
// 需要授权
@ -147,21 +148,46 @@ class QQMusicLoginManager(private val context: Context) {
*/
fun startAuthProcess2(callback: LoginCallback) {
Log.d(TAG, "启动QQ音乐授权流程")
if (isAppInForeground(context)) {
CommonCmd.loginQQMusic(
context,
context.packageName,
BASE_SCHEME
)
// Log.d(TAG, "当前是前台服务")
} else {
CommonCmd.loginQQMusic(
context,
context.packageName,
BASE_SCHEME,
)
Log.d(TAG, "当前是后台服务")
}
// 启动QQ音乐登录
// CommonCmd.startQQMusicProcess(context,context.packageName)
CommonCmd.loginQQMusic(
context,
context.packageName,
BASE_SCHEME
)
callback.onSuccess()
isBindQQMusicService = true
Handler(Looper.getMainLooper()).postDelayed({
callback.onSuccess()
}, 1000) // 延迟500ms
// 等待用户登录完成后进行身份验证
// handler.postDelayed({
// verifyIdentity(callback)
// }, 3000) // 给用户足够的登录时间
}
private fun isAppInForeground(context: Context): Boolean {
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
}
}
return false
}
/**
* 启动授权流程
*/

62
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

@ -567,17 +567,9 @@ class QQMusicSingleton private constructor() {
if (allagain > 0 && curragain < allagain){ //重试机制
executePlaybackAction(actionName,curragain+1,allagain,action)(callback)
}else{
handler.post {
Log.e(TAG, "${actionName}失败: $errorCode $errorMessage")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
callback(Result.failure(Exception("播放失败: $errorMessage")))
} else if (errorCode == 1030) {
callback(Result.success(Unit))
}else{
callback(Result.failure(Exception("播放失败: $errorMessage")))
}
}
//播放失败时,检查当前播放状态
Log.d(TAG, "${actionName}失败,检查当前播放状态: $errorCode $errorMessage")
checkPlaybackStateOnError(actionName, errorCode, errorMessage, callback)
}
}
})
@ -591,7 +583,55 @@ class QQMusicSingleton private constructor() {
}
}
})
}
/**
* 播放失败时检查播放状态
*/
private fun checkPlaybackStateOnError(
actionName: String,
errorCode: Int,
errorMessage: String,
callback: (Result<Unit>) -> Unit
) {
musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
val playState = data["state"] as? Int ?: 0
val isPlaying = when (playState) {
PlayState.STARTED -> true
PlayState.PREPARED -> true
PlayState.BUFFERING -> true
PlayState.PAUSED -> false
PlayState.STOPPED -> false
PlayState.ERROR -> false
else -> false
}
handler.post {
if (isPlaying) {
Log.d(TAG, "${actionName}虽然返回失败,但检查到正在播放,返回成功")
callback(Result.success(Unit))
} else {
Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
}
callback(Result.failure(Exception("播放失败: $errorMessage")))
}
}
}
override fun onError(errorCode: Int, errorMessage: String) {
// 获取播放状态失败,直接返回原始错误
handler.post {
Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage")
if (errorCode == -1){
musicManager?.bindQQMusicApiService()
}
callback(Result.failure(Exception("播放失败: $errorMessage")))
}
}
})
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save