Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
liwei1dao 8 months ago
parent
commit
b093fdd9cc
  1. 40
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  2. 87
      lib/core/services/native_localization_sync.dart
  3. 15
      lib/core/translations/language/ar_sa.dart
  4. 15
      lib/core/translations/language/de_de.dart
  5. 16
      lib/core/translations/language/en_us.dart
  6. 16
      lib/core/translations/language/fr_fr.dart
  7. 30
      lib/core/translations/language/ja_jp.dart
  8. 15
      lib/core/translations/language/ko_kr.dart
  9. 15
      lib/core/translations/language/ru_ru.dart
  10. 13
      lib/core/translations/language/zh_cn.dart
  11. 13
      lib/core/translations/language/zh_hk.dart
  12. 13
      lib/core/translations/language/zh_tw.dart
  13. 4
      lib/core/utils/app_lifecycle_manager.dart
  14. 6
      lib/data/services/db/database_helper.dart
  15. 58
      lib/data/services/db/sqflite_api.dart
  16. 4
      lib/main.dart
  17. 505
      lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart
  18. 2
      lib/modules/settings/controllers/settings_controller.dart
  19. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  20. 36
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt
  21. 28
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt
  22. 42
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  23. 26
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt

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

@ -19,6 +19,7 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import org.json.JSONObject
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"
@ -34,6 +35,10 @@ class MainActivity: FlutterActivity() {
private val DEEPLINK_CHANNEL = "com.saitong.voitrans/deeplink" private val DEEPLINK_CHANNEL = "com.saitong.voitrans/deeplink"
private val DELIVERY_RETRY_DELAY_MS = 300L private val DELIVERY_RETRY_DELAY_MS = 300L
private val MAX_DELIVERY_RETRIES = 12 private val MAX_DELIVERY_RETRIES = 12
private val NATIVE_LOCALIZATION_CHANNEL = "com.saitong.voitrans/native_localization"
private val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private val NATIVE_LOCALIZATION_PREF_KEY_LOCALE = "locale"
private val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "🚀 MainActivity onCreate 开始") Log.d(TAG, "🚀 MainActivity onCreate 开始")
@ -147,6 +152,8 @@ class MainActivity: FlutterActivity() {
} else { } else {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
} }
setupNativeLocalizationChannel(flutterEngine)
// Flutter引擎配置完成后,处理待处理的深链接 // Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
@ -172,6 +179,39 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "✅ configureFlutterEngine 完成") Log.d(TAG, "✅ configureFlutterEngine 完成")
} }
private fun setupNativeLocalizationChannel(flutterEngine: FlutterEngine) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NATIVE_LOCALIZATION_CHANNEL)
channel.setMethodCallHandler { call, result ->
when (call.method) {
"updateNativeStrings" -> {
try {
val args = call.arguments as? Map<*, *>
val locale = args?.get("locale") as? String
val strings = args?.get("strings") as? Map<*, *>
val obj = JSONObject()
strings?.forEach { (k, v) ->
val key = k as? String ?: return@forEach
val value = v as? String ?: return@forEach
obj.put(key, value)
}
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
prefs.edit()
.putString(NATIVE_LOCALIZATION_PREF_KEY_LOCALE, locale)
.putString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, obj.toString())
.apply()
result.success(null)
} catch (e: Exception) {
result.error("NATIVE_LOCALIZATION_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
}
private fun deliverOrQueueIntent(intent: Intent?) { private fun deliverOrQueueIntent(intent: Intent?) {
intent ?: return intent ?: return

87
lib/core/services/native_localization_sync.dart

@ -0,0 +1,87 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../translations/app_translations.dart';
class NativeLocalizationSync {
static const MethodChannel _channel =
MethodChannel('com.saitong.voitrans/native_localization');
static const List<String> nativeKeys = <String>[
'native.notification.appName',
'native.notification.navigation.title',
'native.notification.navigation.keepRunning',
'native.notification.bleAgent.running',
'native.notification.audioRecording.recording',
'native.notification.music.waiting',
'native.notification.music.previous',
'native.notification.music.next',
'native.notification.music.play',
'native.notification.music.pause',
'native.notification.qqMusic.playing',
'unknownSong',
'unknownArtist',
];
static String? _lastLocaleKey;
static int? _lastPayloadHash;
static bool _isSyncing = false;
static Future<void> syncToNative({bool force = false}) async {
if (!Platform.isAndroid) return;
if (_isSyncing) return;
final Locale locale = Get.locale ??
AppTranslations.deviceLocale() ??
AppTranslations.fallbackLocale;
final String localeKey = _toAppTranslationsLocaleKey(locale);
final Map<String, String> payload = _buildPayload(localeKey);
final int payloadHash = Object.hashAll(
payload.entries.map((e) => Object.hash(e.key, e.value)),
);
if (!force &&
_lastLocaleKey == localeKey &&
_lastPayloadHash == payloadHash) {
return;
}
_isSyncing = true;
try {
await _channel.invokeMethod('updateNativeStrings', <String, dynamic>{
'locale': localeKey,
'strings': payload,
});
_lastLocaleKey = localeKey;
_lastPayloadHash = payloadHash;
} finally {
_isSyncing = false;
}
}
static Map<String, String> _buildPayload(String localeKey) {
final Map<String, Map<String, String>> all = AppTranslations().keys;
final Map<String, String>? current = all[localeKey];
final Map<String, String>? fallback =
all['en_US'] ?? all[AppTranslations.fallbackLocale.toString()];
final Map<String, String> result = <String, String>{};
for (final String key in nativeKeys) {
final String? value = current?[key] ?? fallback?[key];
if (value == null || value.isEmpty) continue;
result[key] = value;
}
return result;
}
static String _toAppTranslationsLocaleKey(Locale locale) {
final String language = locale.languageCode;
final String? country = locale.countryCode;
if (country == null || country.isEmpty) {
return language.toLowerCase();
}
return '${language.toLowerCase()}_${country.toUpperCase()}';
}
}

15
lib/core/translations/language/ar_sa.dart

@ -1064,6 +1064,21 @@ const Map<String, String> arSA = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'تم اكتشاف أنك رفضت إذن التخزين بشكل دائم. للمتابعة في استخدام هذه الميزة، يرجى الانتقال إلى إعدادات التطبيق لتمكين إذن التخزين يدويًا.', 'تم اكتشاف أنك رفضت إذن التخزين بشكل دائم. للمتابعة في استخدام هذه الميزة، يرجى الانتقال إلى إعدادات التطبيق لتمكين إذن التخزين يدويًا.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'التنقل جارٍ', // 导航进行中
'native.notification.navigation.keepRunning':
'التنقل جارٍ، يرجى إبقاء التطبيق قيد التشغيل', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running': 'المساعد الصوتي قيد التشغيل', // 语音助手正在运行
'native.notification.audioRecording.recording':
'يتم التسجيل، اضغط للعودة إلى التطبيق', // 正在录音,点击返回应用
'native.notification.music.waiting': 'في انتظار التشغيل', // 等待播放
'native.notification.music.previous': 'السابق', // 上一首
'native.notification.music.next': 'التالي', // 下一首
'native.notification.music.play': 'تشغيل', // 播放
'native.notification.music.pause': 'إيقاف مؤقت', // 暂停
'native.notification.qqMusic.playing': 'يتم تشغيل الموسيقى', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'يحتاج ', 'permissionUtilNeed': 'يحتاج ',
'permissionUtilPermission': ' إذن', 'permissionUtilPermission': ' إذن',

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

@ -1101,6 +1101,21 @@ const Map<String, String> deDE = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'Speicherberechtigung wurde dauerhaft verweigert. Um diese Funktion weiterhin nutzen zu können, gehen Sie bitte zu den App-Einstellungen, um die Speicherberechtigung manuell zu aktivieren.', 'Speicherberechtigung wurde dauerhaft verweigert. Um diese Funktion weiterhin nutzen zu können, gehen Sie bitte zu den App-Einstellungen, um die Speicherberechtigung manuell zu aktivieren.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'Navigation läuft', // 导航进行中
'native.notification.navigation.keepRunning':
'Navigation läuft, App geöffnet lassen', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running': 'Sprachassistent läuft', // 语音助手正在运行
'native.notification.audioRecording.recording':
'Audioaufnahme läuft, tippen um zur App zurückzukehren', // 正在录音,点击返回应用
'native.notification.music.waiting': 'Warte auf Wiedergabe', // 等待播放
'native.notification.music.previous': 'Vorheriger Titel', // 上一首
'native.notification.music.next': 'Nächster Titel', // 下一首
'native.notification.music.play': 'Wiedergabe', // 播放
'native.notification.music.pause': 'Pause', // 暂停
'native.notification.qqMusic.playing': 'Musik wird wiedergegeben', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Benötige ', 'permissionUtilNeed': 'Benötige ',
'permissionUtilPermission': ' Berechtigung', 'permissionUtilPermission': ' Berechtigung',

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

@ -1083,6 +1083,22 @@ const Map<String, String> enUS = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'Navigation in progress', // 导航进行中
'native.notification.navigation.keepRunning':
'Navigating, keep the app running', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running':
'Voice assistant agent is running', // 语音助手正在运行
'native.notification.audioRecording.recording':
'Recording audio, tap to return to app', // 正在录音,点击返回应用
'native.notification.music.waiting': 'Waiting to play', // 等待播放
'native.notification.music.previous': 'Previous', // 上一首
'native.notification.music.next': 'Next', // 下一首
'native.notification.music.play': 'Play', // 播放
'native.notification.music.pause': 'Pause', // 暂停
'native.notification.qqMusic.playing': 'Playing music', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Need ', 'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission', 'permissionUtilPermission': ' Permission',

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

@ -1088,6 +1088,22 @@ const Map<String, String> frFR = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'Navigation en cours', // 导航进行中
'native.notification.navigation.keepRunning':
'Navigation en cours, gardez l’application active', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running':
'Assistant vocal en cours d’exécution', // 语音助手正在运行
'native.notification.audioRecording.recording':
'Enregistrement audio, appuyez pour revenir à l’application', // 正在录音,点击返回应用
'native.notification.music.waiting': 'En attente de lecture', // 等待播放
'native.notification.music.previous': 'Précédent', // 上一首
'native.notification.music.next': 'Suivant', // 下一首
'native.notification.music.play': 'Lecture', // 播放
'native.notification.music.pause': 'Pause', // 暂停
'native.notification.qqMusic.playing': 'Musique en cours de lecture', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Need ', 'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission', 'permissionUtilPermission': ' Permission',

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

@ -439,11 +439,15 @@ const Map<String, String> jaJP = {
'meetingRecordStopFailed': '録音を停止できませんでした。もう一度お試しください。', // 结束录音失败,请重试 'meetingRecordStopFailed': '録音を停止できませんでした。もう一度お試しください。', // 结束录音失败,请重试
'meetingRecordSaveFailed': '録音を保存できませんでした。もう一度お試しください。', // 保存录音失败,请重试 'meetingRecordSaveFailed': '録音を保存できませんでした。もう一度お試しください。', // 保存录音失败,请重试
'meetingRecordRecordingNotificationTitle': '録音中', // 录音进行中 'meetingRecordRecordingNotificationTitle': '録音中', // 录音进行中
'meetingRecordRecordingNotificationBody': 'バックグラウンドで録音しています。タップしてアプリに戻ります。', // 正在后台录音,点击返回应用 'meetingRecordRecordingNotificationBody':
'バックグラウンドで録音しています。タップしてアプリに戻ります。', // 正在后台录音,点击返回应用
'overlayPermissionTitle': '他のアプリの上に表示', // 悬浮窗权限 'overlayPermissionTitle': '他のアプリの上に表示', // 悬浮窗权限
'overlayPermissionTranslationDesc': '通話中または音声/動画翻訳中に翻訳結果を表示するため、他のアプリの上に表示する権限が必要です。これにより、他のアプリを使用中でもリアルタイム翻訳を確認できます。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。 'overlayPermissionTranslationDesc':
'overlayPermissionRecordingDesc': '録音中に録音フローティングバー(タイマー/一時停止/再開)を表示するため、他のアプリの上に表示する権限が必要です。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 '通話中または音声/動画翻訳中に翻訳結果を表示するため、他のアプリの上に表示する権限が必要です。これにより、他のアプリを使用中でもリアルタイム翻訳を確認できます。', // 为了在通话或音视频翻译时显示翻译结果,需要悬浮窗权限。这样您就可以在使用其他应用时看到实时翻译。
'overlayPermissionOpenSettingsFailed': 'システム設定を自動で開けませんでした。設定を開き、手動で「他のアプリの上に表示」を有効にしてください。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 'overlayPermissionRecordingDesc':
'録音中に録音フローティングバー(タイマー/一時停止/再開)を表示するため、他のアプリの上に表示する権限が必要です。', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。
'overlayPermissionOpenSettingsFailed':
'システム設定を自動で開けませんでした。設定を開き、手動で「他のアプリの上に表示」を有効にしてください。', // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。
'transcribing': 'Transcribing...', 'transcribing': 'Transcribing...',
'summarizing': 'Summarizing...', 'summarizing': 'Summarizing...',
'analyzing': 'Analyzing...', 'analyzing': 'Analyzing...',
@ -618,8 +622,7 @@ const Map<String, String> jaJP = {
// Meeting模块 - 发言者相关 // Meeting模块 - 发言者相关
'applyToAllParagraphs': 'この話者のすべての段落に適用', 'applyToAllParagraphs': 'この話者のすべての段落に適用',
'summaryWillBeUpdatedAutomatically': 'summaryWillBeUpdatedAutomatically': '要約はテンプレートに基づいて自動的に更新されます',
'要約はテンプレートに基づいて自動的に更新されます',
'nameCurrentSpeaker': 'Name Current Speaker', 'nameCurrentSpeaker': 'Name Current Speaker',
'recentlyUsedNames': 'Recently Used Names', 'recentlyUsedNames': 'Recently Used Names',
'applyToCurrentParagraph': 'Apply to Current Paragraph', 'applyToCurrentParagraph': 'Apply to Current Paragraph',
@ -1032,6 +1035,21 @@ const Map<String, String> jaJP = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'ナビゲーション中', // 导航进行中
'native.notification.navigation.keepRunning':
'ナビゲーション中です。アプリを起動したままにしてください', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running': '音声アシスタントが実行中です', // 语音助手正在运行
'native.notification.audioRecording.recording':
'録音中です。タップしてアプリに戻る', // 正在录音,点击返回应用
'native.notification.music.waiting': '再生待機中', // 等待播放
'native.notification.music.previous': '前の曲', // 上一首
'native.notification.music.next': '次の曲', // 下一首
'native.notification.music.play': '再生', // 播放
'native.notification.music.pause': '一時停止', // 暂停
'native.notification.qqMusic.playing': '音楽を再生中', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Need ', 'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission', 'permissionUtilPermission': ' Permission',

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

@ -1027,6 +1027,21 @@ const Map<String, String> koKR = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.', 'Storage permission has been permanently denied. To continue using this feature, please go to app settings to manually enable storage permission.',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': '내비게이션 진행 중', // 导航进行中
'native.notification.navigation.keepRunning':
'내비게이션 중입니다. 앱을 실행 상태로 유지하세요', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running': '음성 비서가 실행 중입니다', // 语音助手正在运行
'native.notification.audioRecording.recording':
'녹음 중, 탭하여 앱으로 돌아가기', // 正在录音,点击返回应用
'native.notification.music.waiting': '재생 대기 중', // 等待播放
'native.notification.music.previous': '이전 곡', // 上一首
'native.notification.music.next': '다음 곡', // 下一首
'native.notification.music.play': '재생', // 播放
'native.notification.music.pause': '일시정지', // 暂停
'native.notification.qqMusic.playing': '음악 재생 중', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Need ', 'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission', 'permissionUtilPermission': ' Permission',

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

@ -1093,6 +1093,21 @@ const Map<String, String> ruRU = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。', '检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS', // VOITRANS
'native.notification.navigation.title': 'Навигация запущена', // 导航进行中
'native.notification.navigation.keepRunning':
'Идёт навигация, держите приложение запущенным', // 正在导航中,请保持应用运行
'native.notification.bleAgent.running': 'Голосовой помощник работает', // 语音助手正在运行
'native.notification.audioRecording.recording':
'Идёт запись, нажмите чтобы вернуться в приложение', // 正在录音,点击返回应用
'native.notification.music.waiting': 'Ожидание воспроизведения', // 等待播放
'native.notification.music.previous': 'Предыдущая', // 上一首
'native.notification.music.next': 'Следующая', // 下一首
'native.notification.music.play': 'Воспроизвести', // 播放
'native.notification.music.pause': 'Пауза', // 暂停
'native.notification.qqMusic.playing': 'Воспроизведение музыки', // 正在播放音乐
//permission_util.dart //permission_util.dart
'permissionUtilNeed': 'Need ', 'permissionUtilNeed': 'Need ',
'permissionUtilPermission': ' Permission', 'permissionUtilPermission': ' Permission',

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

@ -981,6 +981,19 @@ const Map<String, String> zhCN = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。', '检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS',
'native.notification.navigation.title': '导航进行中',
'native.notification.navigation.keepRunning': '正在导航中,请保持应用运行',
'native.notification.bleAgent.running': '语音助手正在运行',
'native.notification.audioRecording.recording': '正在录音,点击返回应用',
'native.notification.music.waiting': '等待播放',
'native.notification.music.previous': '上一首',
'native.notification.music.next': '下一首',
'native.notification.music.play': '播放',
'native.notification.music.pause': '暂停',
'native.notification.qqMusic.playing': '正在播放音乐',
//permission_util.dart //permission_util.dart
'permissionUtilNeed': '需要', 'permissionUtilNeed': '需要',
'permissionUtilPermission': '权限', 'permissionUtilPermission': '权限',

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

@ -980,6 +980,19 @@ const Map<String, String> zhHK = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。', '檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS',
'native.notification.navigation.title': '導航進行中',
'native.notification.navigation.keepRunning': '正在導航中,請保持應用運行',
'native.notification.bleAgent.running': '語音助手正在運行',
'native.notification.audioRecording.recording': '正在錄音,點擊返回應用',
'native.notification.music.waiting': '等待播放',
'native.notification.music.previous': '上一首',
'native.notification.music.next': '下一首',
'native.notification.music.play': '播放',
'native.notification.music.pause': '暫停',
'native.notification.qqMusic.playing': '正在播放音樂',
//permission_util.dart //permission_util.dart
'permissionUtilNeed': '需要', 'permissionUtilNeed': '需要',
'permissionUtilPermission': '權限', 'permissionUtilPermission': '權限',

13
lib/core/translations/language/zh_tw.dart

@ -980,6 +980,19 @@ const Map<String, String> zhTW = {
'storagePermissionPermanentDenial': 'storagePermissionPermanentDenial':
'檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。', '檢測到您已永久拒絕儲存權限。如需繼續使用此功能,請前往應用設定中手動開啟儲存權限。',
// 原生前台服务通知(Android)
'native.notification.appName': 'VOITRANS',
'native.notification.navigation.title': '導航進行中',
'native.notification.navigation.keepRunning': '正在導航中,請保持應用運行',
'native.notification.bleAgent.running': '語音助手正在運行',
'native.notification.audioRecording.recording': '正在錄音,點擊返回應用',
'native.notification.music.waiting': '等待播放',
'native.notification.music.previous': '上一首',
'native.notification.music.next': '下一首',
'native.notification.music.play': '播放',
'native.notification.music.pause': '暫停',
'native.notification.qqMusic.playing': '正在播放音樂',
//permission_util.dart //permission_util.dart
'permissionUtilNeed': '需要', 'permissionUtilNeed': '需要',
'permissionUtilPermission': '權限', 'permissionUtilPermission': '權限',

4
lib/core/utils/app_lifecycle_manager.dart

@ -7,6 +7,7 @@ import 'package:get_storage/get_storage.dart';
import '../../modules/meeting/controllers/meeting_record_controller.dart'; import '../../modules/meeting/controllers/meeting_record_controller.dart';
import '../../routes/app_routes.dart'; import '../../routes/app_routes.dart';
import 'logger.dart'; import 'logger.dart';
import '../services/native_localization_sync.dart';
import '../translations/app_translations.dart'; import '../translations/app_translations.dart';
//import '../../modules/settings/controllers/settings_controller.dart'; //import '../../modules/settings/controllers/settings_controller.dart';
@ -36,6 +37,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
// 注册应用生命周期观察者 // 注册应用生命周期观察者
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
Logger.info('AppLifecycleManager 已初始化'); Logger.info('AppLifecycleManager 已初始化');
unawaited(NativeLocalizationSync.syncToNative(force: true));
unawaited(refreshBleWakeupPolicy()); unawaited(refreshBleWakeupPolicy());
} }
@ -174,6 +176,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
// 延迟执行,确保系统语言变化处理完成 // 延迟执行,确保系统语言变化处理完成
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
Get.updateLocale(userLocale); Get.updateLocale(userLocale);
unawaited(NativeLocalizationSync.syncToNative(force: true));
Logger.info('已恢复用户设置的语言: ${userLocale.toString()}'); Logger.info('已恢复用户设置的语言: ${userLocale.toString()}');
}); });
} else { } else {
@ -183,6 +186,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
Locale? newLocale = AppTranslations.deviceLocale(); Locale? newLocale = AppTranslations.deviceLocale();
if (newLocale != null) { if (newLocale != null) {
Get.updateLocale(newLocale); Get.updateLocale(newLocale);
unawaited(NativeLocalizationSync.syncToNative(force: true));
// 同步更新设置控制器中的语言状态 // 同步更新设置控制器中的语言状态
try { try {

6
lib/data/services/db/database_helper.dart

@ -94,7 +94,8 @@ class DatabaseHelper {
batch.execute(''' batch.execute('''
CREATE TABLE IF NOT EXISTS meetingcategorytemplate ( CREATE TABLE IF NOT EXISTS meetingcategorytemplate (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT name TEXT,
language TEXT
) )
'''); ''');
// 商务会议助手模版 // 商务会议助手模版
@ -107,7 +108,8 @@ class DatabaseHelper {
tag TEXT, tag TEXT,
desc TEXT, desc TEXT,
outline TEXT, outline TEXT,
prompt TEXT prompt TEXT,
language TEXT
) )
'''); ''');
batch.insert('user', { batch.insert('user', {

58
lib/data/services/db/sqflite_api.dart

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '/core/utils/logger.dart';
import 'database_helper.dart'; import 'database_helper.dart';
@ -182,6 +183,42 @@ class SqfliteApi {
}); });
} }
static String _getMeetingTemplateLanguageCode() {
final locale = Get.locale ?? Get.deviceLocale;
if (locale == null) {
return 'en-US';
}
final code = locale.languageCode.toLowerCase();
switch (code) {
case 'zh':
return 'zh-CN';
case 'en':
return 'en-US';
case 'ja':
return 'ja-JP';
case 'ko':
return 'ko-KR';
case 'fr':
return 'fr-FR';
case 'de':
return 'de-DE';
case 'es':
return 'es-ES';
case 'it':
return 'it-IT';
case 'pt':
return 'pt-BR';
case 'ru':
return 'ru-RU';
case 'ar':
return 'ar-SA';
case 'hi':
return 'hi-IN';
default:
return 'en-US';
}
}
// 获取商务会议助手Ask AI列表 // 获取商务会议助手Ask AI列表
static Future getMeetingAiList(int id) async { static Future getMeetingAiList(int id) async {
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
@ -223,6 +260,7 @@ class SqfliteApi {
'desc': item['description'], 'desc': item['description'],
'outline': item['outline'], 'outline': item['outline'],
'prompt': item['template'], 'prompt': item['template'],
'language': item['language'],
}; };
if (result != null) { if (result != null) {
result['templates'].add(template); result['templates'].add(template);
@ -230,6 +268,7 @@ class SqfliteApi {
categoryList.add({ categoryList.add({
'name': item['ttype'], 'name': item['ttype'],
'templates': [template], 'templates': [template],
'language': item['language'],
}); });
} }
} }
@ -252,6 +291,7 @@ class SqfliteApi {
await txn.insert('meetingcategorytemplate', { await txn.insert('meetingcategorytemplate', {
'id': categoryId, 'id': categoryId,
'name': item['name'], 'name': item['name'],
'language': item['language'],
}); });
for (var j = 0; j < item['templates'].length; j++) { for (var j = 0; j < item['templates'].length; j++) {
Map template = item['templates'][j]; Map template = item['templates'][j];
@ -264,6 +304,7 @@ class SqfliteApi {
'desc': template['desc'], 'desc': template['desc'],
'outline': template['outline'], 'outline': template['outline'],
'prompt': template['prompt'], 'prompt': template['prompt'],
'language': template['language'],
}); });
} }
} }
@ -272,15 +313,22 @@ class SqfliteApi {
// 获取商务会议助手所有分类模版列表 // 获取商务会议助手所有分类模版列表
static Future getMeetingCategoryTemplateList() async { static Future getMeetingCategoryTemplateList() async {
final language = _getMeetingTemplateLanguageCode();
Logger.info('language: $language');
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
List categorytemplateList = await db.query('meetingcategorytemplate'); List categorytemplateList = await db.query(
'meetingcategorytemplate',
where: '(language = ? OR language IS NULL OR language = "")',
whereArgs: [language],
);
Logger.info('categorytemplateList: $categorytemplateList');
final dataList = final dataList =
categorytemplateList.map((item) => Map.from(item)).toList(); categorytemplateList.map((item) => Map.from(item)).toList();
for (var element in dataList) { for (var element in dataList) {
final List templateList = await db.query( final List templateList = await db.query(
'meetingtemplate', 'meetingtemplate',
limit: 4, limit: 4,
where: 'categoryid = ?', where: 'categoryid = ? ',
whereArgs: [element['id']], whereArgs: [element['id']],
orderBy: 'id ${element['name'] == '自定义' ? 'DESC' : 'ASC'}', orderBy: 'id ${element['name'] == '自定义' ? 'DESC' : 'ASC'}',
); );
@ -296,11 +344,13 @@ class SqfliteApi {
// 获取商务会议助手模版列表 // 获取商务会议助手模版列表
static Future getMeetingTemplateList(int id, {bool isCustom = false}) async { static Future getMeetingTemplateList(int id, {bool isCustom = false}) async {
final language = _getMeetingTemplateLanguageCode();
Logger.info('language: $language');
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
return await db.query( return await db.query(
'meetingtemplate', 'meetingtemplate',
where: 'categoryid = ?', where: 'categoryid = ? AND language = ?',
whereArgs: [id], whereArgs: [id, language],
orderBy: 'id ${isCustom ? 'DESC' : 'ASC'}', orderBy: 'id ${isCustom ? 'DESC' : 'ASC'}',
); );
} }

4
lib/main.dart

@ -19,6 +19,7 @@ import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart'; import 'core/themes/theme_service.dart';
import 'core/bindings/initial_binding.dart'; import 'core/bindings/initial_binding.dart';
import 'core/utils/logger.dart'; import 'core/utils/logger.dart';
import 'core/services/native_localization_sync.dart';
import 'data/services/login_service.dart'; import 'data/services/login_service.dart';
import 'modules/meeting/controllers/meeting_home_controller.dart'; import 'modules/meeting/controllers/meeting_home_controller.dart';
import 'modules/splash/controllers/splash_controller.dart'; import 'modules/splash/controllers/splash_controller.dart';
@ -165,6 +166,9 @@ class MainApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 在应用构建时立即初始化深链接处理 // 在应用构建时立即初始化深链接处理
_initDeepLinkHandler(); _initDeepLinkHandler();
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(NativeLocalizationSync.syncToNative());
});
// 获取保存的主题设置 // 获取保存的主题设置
final themeMode = ThemeService().getThemeMode(); final themeMode = ThemeService().getThemeMode();

505
lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart

@ -93,285 +93,288 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
Icons.audio_file_outlined, Icons.audio_file_outlined,
color: Colors.grey, color: Colors.grey,
size: 32.w, size: 32.w,
), ),
5.horizontalSpace, 5.horizontalSpace,
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_controller.meetingData.value.title, _controller.meetingData.value.title,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.black87, color: Colors.black87,
),
), ),
), Text(
Text( '${_controller.meetingData.value.seconds}s',
'${_controller.meetingData.value.seconds}s', style: TextStyle(
style: TextStyle( fontSize: 12.sp,
fontSize: 12.sp, color: Colors.grey,
color: Colors.grey, ),
), ),
), ],
],
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
margin: EdgeInsets.only(bottom: 10.w),
child: Divider(
height: 1,
color: Colors.grey[300],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Row(
children: [
Expanded(
child: Text(
'selectSummaryTemplate'.tr, // 对应中文:选择总结模版
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
), ),
), ),
), GestureDetector(
GestureDetector( onTap: () => Get.back(),
onTap: () async { child: Container(
var result = await Get.to( width: 26.w,
() => TemplateView( height: 26.w,
_templateData, decoration: BoxDecoration(
_lastTemplateData['id'] ?? 0, color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
), ),
preventDuplicates: false, child: Icon(
); Icons.close,
if (result != null) { size: 18.w,
_scrollController.jumpTo(0); color: Colors.grey[400],
List list = List.from(_originalTemplate);
int index = list
.indexWhere((element) => element['id'] == result['id']);
if (index != -1) {
Map itemToMove = list.removeAt(index);
list.insert(0, itemToMove);
} else {
list.insert(0, result);
}
setState(() {
_templateData = result;
_templateList = list;
});
}
if (_controller.editTemplate.isNotEmpty) {
Map? editMap = _templateList.firstWhere(
(element) =>
element['id'] == _controller.editTemplate['id'],
orElse: () => null);
if (editMap != null) {
editMap['name'] = _controller.editTemplate['name'];
editMap['desc'] = _controller.editTemplate['desc'];
editMap['prompt'] = _controller.editTemplate['prompt'];
}
if (_controller.editTemplate['id'] == _templateData['id']) {
_templateData['name'] = _controller.editTemplate['name'];
_templateData['desc'] = _controller.editTemplate['desc'];
_templateData['prompt'] =
_controller.editTemplate['prompt'];
}
setState(() {});
}
if (_controller.deleteTemplateId != 0) {
_templateList.removeWhere((element) =>
element['id'] == _controller.deleteTemplateId);
if (_controller.deleteTemplateId == _templateData['id']) {
_templateData = _templateList[0];
}
setState(() {});
_storage.write("meeting_last_template_Data", {});
_storage.write("meeting_template_list", _templateList);
}
},
child: Row(
children: [
Text(
'viewAll'.tr, // 对应中文:查看全部
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
), ),
const Icon( ),
Icons.keyboard_arrow_right, )
color: Colors.grey, ],
) ),
],
),
),
],
),
),
Container(
height: 130.w,
margin: EdgeInsets.only(bottom: 16.w),
child: ListView.builder(
controller: _scrollController,
itemCount: _templateList.length,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 4.w),
itemBuilder: (BuildContext context, int index) {
Map data = _templateList[index];
return _item(data);
},
), ),
),
if (!widget.isSummarizeAgain)
Container( Container(
margin: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration( margin: EdgeInsets.only(bottom: 10.w),
color: Colors.white, child: Divider(
borderRadius: BorderRadius.circular(8.r), height: 1,
color: Colors.grey[300],
), ),
child: Column( ),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Row(
children: [ children: [
_row( Expanded(
'speakerIdentification'.tr, // 对应中文:区分说话人 child: Text(
widget: Transform.scale( 'selectSummaryTemplate'.tr, // 对应中文:选择总结模版
scale: 0.8, style: TextStyle(
alignment: Alignment.centerRight, fontSize: 14.sp,
child: Switch( color: Colors.black87,
value: _isSpeaker,
onChanged: (bool value) {
setState(() {
_isSpeaker = value;
});
},
), ),
), ),
), ),
_row( GestureDetector(
'recordingLanguage'.tr, // 对应中文:录音语言 onTap: () async {
value: _formlanguageText, var result = await Get.to(
onTap: () { () => TemplateView(
Get.bottomSheet( _templateData,
RecordLanguageBottomSheet(_formlanguage), _lastTemplateData['id'] ?? 0,
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
), ),
).then((value) { preventDuplicates: false,
if (value != null) { );
setState(() { if (result != null) {
_formlanguageText = value['name']; _scrollController.jumpTo(0);
_formlanguage = value['value']; List list = List.from(_originalTemplate);
}); int index = list.indexWhere(
(element) => element['id'] == result['id']);
if (index != -1) {
Map itemToMove = list.removeAt(index);
list.insert(0, itemToMove);
} else {
list.insert(0, result);
}
setState(() {
_templateData = result;
_templateList = list;
});
}
if (_controller.editTemplate.isNotEmpty) {
Map? editMap = _templateList.firstWhere(
(element) =>
element['id'] == _controller.editTemplate['id'],
orElse: () => null);
if (editMap != null) {
editMap['name'] = _controller.editTemplate['name'];
editMap['desc'] = _controller.editTemplate['desc'];
editMap['prompt'] = _controller.editTemplate['prompt'];
}
if (_controller.editTemplate['id'] ==
_templateData['id']) {
_templateData['name'] =
_controller.editTemplate['name'];
_templateData['desc'] =
_controller.editTemplate['desc'];
_templateData['prompt'] =
_controller.editTemplate['prompt'];
}
setState(() {});
}
if (_controller.deleteTemplateId != 0) {
_templateList.removeWhere((element) =>
element['id'] == _controller.deleteTemplateId);
if (_controller.deleteTemplateId == _templateData['id']) {
_templateData = _templateList[0];
} }
}); setState(() {});
_storage.write("meeting_last_template_Data", {});
_storage.write("meeting_template_list", _templateList);
}
}, },
), child: Row(
_row( children: [
'targetLanguage'.tr, // 对应中文:目标语言 Text(
value: _tolanguageText, 'viewAll'.tr, // 对应中文:查看全部
isBorder: false, style: TextStyle(
onTap: () { fontSize: 12.sp,
Get.bottomSheet( color: Colors.grey,
RecordLanguageBottomSheet(_tolanguage),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
), ),
), ),
).then((value) { const Icon(
if (value != null) { Icons.keyboard_arrow_right,
setState(() { color: Colors.grey,
_tolanguageText = value['name']; )
_tolanguage = value['value']; ],
}); ),
}
});
},
), ),
], ],
), ),
), ),
Padding( Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), height: 130.w,
child: GestureDetector( margin: EdgeInsets.only(bottom: 16.w),
onTap: () { child: ListView.builder(
if (User.isLoggedIn() && User.instance.meetintegral <= 0) { controller: _scrollController,
showRechargeDialog('meeting'); itemCount: _templateList.length,
return; scrollDirection: Axis.horizontal,
} padding: EdgeInsets.symmetric(horizontal: 4.w),
if (_templateData['id'] != _templateList[0]['id']) { itemBuilder: (BuildContext context, int index) {
int index = _templateList Map data = _templateList[index];
.indexWhere((v) => v['id'] == _templateData['id']); return _item(data);
if (index != -1) { },
Map itemToMove = _templateList.removeAt(index); ),
_templateList.insert(0, itemToMove); ),
} if (!widget.isSummarizeAgain)
} Container(
_storage.write("meeting_last_template_Data", _templateData); margin: EdgeInsets.symmetric(horizontal: 12.w),
_storage.write("meeting_template_list", _templateList);
Get.back();
if (widget.isSummarizeAgain) {
_controller.refreshSummary(_templateData['id']);
} else {
_controller.summaryCreate(
_formlanguage,
_tolanguage,
_isSpeaker,
_templateData['id'],
);
}
},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black, color: Colors.white,
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(8.r),
), ),
child: Text( child: Column(
'meetingGenerateNow'.tr, // 对应中文:立即生成 children: [
style: TextStyle( _row(
fontSize: 16.sp, 'speakerIdentification'.tr, // 对应中文:区分说话人
color: Colors.white, widget: Transform.scale(
scale: 0.8,
alignment: Alignment.centerRight,
child: Switch(
value: _isSpeaker,
onChanged: (bool value) {
setState(() {
_isSpeaker = value;
});
},
),
),
),
_row(
'recordingLanguage'.tr, // 对应中文:录音语言
value: _formlanguageText,
onTap: () {
Get.bottomSheet(
RecordLanguageBottomSheet(_formlanguage),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
).then((value) {
if (value != null) {
setState(() {
_formlanguageText = value['name'];
_formlanguage = value['value'];
});
}
});
},
),
_row(
'targetLanguage'.tr, // 对应中文:目标语言
value: _tolanguageText,
isBorder: false,
onTap: () {
Get.bottomSheet(
RecordLanguageBottomSheet(_tolanguage),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
).then((value) {
if (value != null) {
setState(() {
_tolanguageText = value['name'];
_tolanguage = value['value'];
});
}
});
},
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
child: GestureDetector(
onTap: () {
if (User.isLoggedIn() && User.instance.meetintegral <= 0) {
showRechargeDialog('meeting');
return;
}
if (_templateData['id'] != _templateList[0]['id']) {
int index = _templateList
.indexWhere((v) => v['id'] == _templateData['id']);
if (index != -1) {
Map itemToMove = _templateList.removeAt(index);
_templateList.insert(0, itemToMove);
}
}
_storage.write("meeting_last_template_Data", _templateData);
_storage.write("meeting_template_list", _templateList);
Get.back();
if (widget.isSummarizeAgain) {
_controller.refreshSummary(_templateData['id']);
} else {
_controller.summaryCreate(
_formlanguage,
_tolanguage,
_isSpeaker,
_templateData['id'],
);
}
},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'meetingGenerateNow'.tr, // 对应中文:立即生成
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
), ),
), ),
), ),
), ),
),
], ],
), ),
); );

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

@ -8,6 +8,7 @@ import 'package:logger/logger.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../core/services/native_localization_sync.dart';
import '../../../core/translations/app_translations.dart'; import '../../../core/translations/app_translations.dart';
import '../../../data/services/network/dio_manager.dart'; import '../../../data/services/network/dio_manager.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
@ -128,6 +129,7 @@ class SettingsController extends GetxController {
Get.updateLocale(targetLocale); Get.updateLocale(targetLocale);
Logger().i("使用fallback语言: ${targetLocale.toString()}"); Logger().i("使用fallback语言: ${targetLocale.toString()}");
} }
await NativeLocalizationSync.syncToNative(force: true);
// 保存用户选择 // 保存用户选择
currentLanguage.value = selectedLanguage; currentLanguage.value = selectedLanguage;

28
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -12,6 +12,7 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import android.util.Log import android.util.Log
import org.json.JSONObject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@ -47,6 +48,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val CHANNEL_ID = "ble_agent_service_channel" private const val CHANNEL_ID = "ble_agent_service_channel"
private const val NOTIFICATION_ID = 1002 private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config" private const val PREF_NAME = "deep_voice_config"
private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
// 目标蓝牙设备名称 // 目标蓝牙设备名称
private const val TARGET_DEVICE_NAME = "DeapSound" private const val TARGET_DEVICE_NAME = "DeapSound"
@ -412,6 +415,10 @@ private var companionManager: BleCompanionManager? = null
// 设置服务状态 // 设置服务状态
isRunning.set(true) isRunning.set(true)
// 前台通知显示条件:
// - 只要 BleMediaService 被启动(Flutter 调用 AgentService.startAgentService),onCreate 会立刻执行 startForeground();
// - 渠道使用 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏里显示常驻通知;
// - Flutter 侧常见入口:PairingController 跳过配对时会调用 BleManager.startAgentService()。
startForeground(NOTIFICATION_ID, createNotification()) startForeground(NOTIFICATION_ID, createNotification())
// 初始化BLE伴侣设备管理器 - 使用单例模式获取实例 // 初始化BLE伴侣设备管理器 - 使用单例模式获取实例
companionManager = BleCompanionManager.getInstance(this) companionManager = BleCompanionManager.getInstance(this)
@ -882,11 +889,16 @@ private var companionManager: BleCompanionManager? = null
} }
val appIconResId = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info val appIconResId = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
val title = getNativeLocalizedString("native.notification.appName", "Voitrans")
val content = getNativeLocalizedString(
"native.notification.bleAgent.running",
"Voice assistant agent is running"
)
// 构建通知 // 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID) val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Voitrans") .setContentTitle(title)
.setContentText("Voice assistant agent is running") .setContentText(content)
.setSmallIcon(appIconResId) .setSmallIcon(appIconResId)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
@ -899,4 +911,16 @@ private var companionManager: BleCompanionManager? = null
return builder.build() return builder.build()
} }
private fun getNativeLocalizedString(key: String, defaultValue: String): String {
return try {
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue
val obj = JSONObject(json)
val value = obj.optString(key, defaultValue)
if (value.isBlank()) defaultValue else value
} catch (_: Exception) {
defaultValue
}
}
} }

36
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt

@ -14,6 +14,7 @@ import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.json.JSONObject
import com.amap.api.navi.AMapNavi import com.amap.api.navi.AMapNavi
import com.amap.api.navi.AMapNaviListener import com.amap.api.navi.AMapNaviListener
import com.amap.api.navi.enums.PathPlanningStrategy import com.amap.api.navi.enums.PathPlanningStrategy
@ -42,6 +43,8 @@ class NavigationService: Service(),AMapNaviListener {
private const val CHANNEL_ID = "navigation_channel" private const val CHANNEL_ID = "navigation_channel"
private const val CHANNEL_Name = "导航服务" private const val CHANNEL_Name = "导航服务"
private const val NOTIFICATION_ID = 1003 private const val NOTIFICATION_ID = 1003
private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var startpos: String = "" private var startpos: String = ""
@ -58,6 +61,11 @@ class NavigationService: Service(),AMapNaviListener {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
// 前台通知显示条件:
// - 只要该 Service 被启动(ContextCompat.startForegroundService / startForegroundService),onCreate 就会立刻调用 startForeground();
// - 通知渠道使用 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏里出现常驻通知;
// - Flutter 侧需要调用插件 com.yunqiinnovation.amap_service 的 "start"(会走 NavigationServiceHelper.startNavigation -> 启动该 Service),
// 调用 "open" 只会打开导航页面,不会启动该前台服务,因此不会有通知。
startForegroundNotification() startForegroundNotification()
// 注册广播监听器 // 注册广播监听器
@ -135,19 +143,27 @@ class NavigationService: Service(),AMapNaviListener {
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
} }
val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中")
val content = getNativeLocalizedString(
"native.notification.navigation.keepRunning",
"正在导航中,请保持应用运行"
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("导航进行中") .setContentTitle(title)
.setContentText("正在导航中,请保持应用运行") .setContentText(content)
.setSmallIcon(R.drawable.ic_navigation) // 使用你自己的图标资源 .setSmallIcon(R.drawable.ic_navigation) // 使用你自己的图标资源
.setOngoing(true) .setOngoing(true)
.build() .build()
// 触发系统显示“前台服务常驻通知”的关键点:startForeground()
// 如果 Android 13+ 未授予通知权限 / 用户关闭了该渠道通知,可能导致通知不可见或启动前台服务失败(不同 ROM 行为可能不同)。
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
} }
private fun updateNotification(contentText: String, iconBitmap: Bitmap?) { private fun updateNotification(contentText: String, iconBitmap: Bitmap?) {
val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中")
val builder = NotificationCompat.Builder(this, CHANNEL_ID) val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("导航进行中") .setContentTitle(title)
.setContentText(contentText) .setContentText(contentText)
.setSmallIcon(R.drawable.ic_navigation) // 通知栏图标(小) .setSmallIcon(R.drawable.ic_navigation) // 通知栏图标(小)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
@ -162,6 +178,18 @@ class NavigationService: Service(),AMapNaviListener {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, notification) notificationManager.notify(NOTIFICATION_ID, notification)
} }
private fun getNativeLocalizedString(key: String, defaultValue: String): String {
return try {
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue
val obj = JSONObject(json)
val value = obj.optString(key, defaultValue)
if (value.isBlank()) defaultValue else value
} catch (_: Exception) {
defaultValue
}
}
private fun tintBitmap(source: Bitmap, tintColor: Int): Bitmap { private fun tintBitmap(source: Bitmap, tintColor: Int): Bitmap {
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888) val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(result) val canvas = android.graphics.Canvas(result)
@ -435,4 +463,4 @@ class NavigationService: Service(),AMapNaviListener {
isRunning.set(false) isRunning.set(false)
super.onDestroy() super.onDestroy()
} }
} }

28
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt

@ -11,6 +11,7 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import android.util.Log import android.util.Log
import org.json.JSONObject
/** /**
* 音频录制前台服务 * 音频录制前台服务
@ -24,6 +25,8 @@ class AudioRecordingForegroundService : Service() {
private const val CHANNEL_ID = "audio_recording_channel" private const val CHANNEL_ID = "audio_recording_channel"
private const val CHANNEL_NAME = "音频录制服务" private const val CHANNEL_NAME = "音频录制服务"
private const val ACTION_STOP = "com.yunqiinnovation.azure_speech.tools.STOP_SERVICE" private const val ACTION_STOP = "com.yunqiinnovation.azure_speech.tools.STOP_SERVICE"
private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
/** /**
* 启动前台服务 * 启动前台服务
@ -62,6 +65,10 @@ class AudioRecordingForegroundService : Service() {
// 创建前台通知 // 创建前台通知
val notification = createNotification() val notification = createNotification()
// 前台通知显示条件:
// - 只要该 Service 被启动(AzureSpeechPlugin/AzureAsrHelper 在 enableRecord 时会启动它),就会在 onStartCommand 里立刻调用 startForeground();
// - 渠道为 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在下拉通知栏显示常驻通知;
// - Android 13+ 如果未授予通知权限 / 用户关闭通知,可能出现通知不可见或启动前台服务失败(不同 ROM 行为可能不同)。
// 必须立即调用 startForeground 以满足 Android 8.0+ 的要求 // 必须立即调用 startForeground 以满足 Android 8.0+ 的要求
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
@ -124,9 +131,14 @@ class AudioRecordingForegroundService : Service() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val title = getNativeLocalizedString("native.notification.appName", "Voitrans")
val content = getNativeLocalizedString(
"native.notification.audioRecording.recording",
"Recording audio, tap to return to app"
)
return NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Voitrans") .setContentTitle(title)
.setContentText("Recording audio, tap to return to app") .setContentText(content)
.setSmallIcon(applicationInfo.icon) .setSmallIcon(applicationInfo.icon)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setOngoing(true) // 设置为持续通知,用户无法滑动删除 .setOngoing(true) // 设置为持续通知,用户无法滑动删除
@ -136,4 +148,16 @@ class AudioRecordingForegroundService : Service() {
.setAutoCancel(false) // 点击通知后不自动取消 .setAutoCancel(false) // 点击通知后不自动取消
.build() .build()
} }
private fun getNativeLocalizedString(key: String, defaultValue: String): String {
return try {
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue
val obj = JSONObject(json)
val value = obj.optString(key, defaultValue)
if (value.isBlank()) defaultValue else value
} catch (_: Exception) {
defaultValue
}
}
} }

42
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -25,6 +25,7 @@ import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import org.json.JSONObject
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -36,6 +37,8 @@ class MusicService : MediaBrowserServiceCompat() {
private const val CHANNEL_ID = "music_media_service_channel" private const val CHANNEL_ID = "music_media_service_channel"
private const val NOTIFICATION_ID = 1002 private const val NOTIFICATION_ID = 1002
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentIdInternal: Long? = null private var currentIdInternal: Long? = null
@ -87,8 +90,10 @@ class MusicService : MediaBrowserServiceCompat() {
fun updateMediaSessionMetadata(song: Map<String, Any>) { fun updateMediaSessionMetadata(song: Map<String, Any>) {
instance?.let { service -> instance?.let { service ->
try { try {
val title = song["title"] as? String ??: "未知标题" val title = song["title"] as? String
val artist = song["artist"] as? String ??: "未知歌手" ?: service.getNativeLocalizedString("unknownSong", "未知歌曲")
val artist = song["artist"] as? String
?: service.getNativeLocalizedString("unknownArtist", "未知歌手")
val coverUrl = song["coverUrl"] as? String ??: "" val coverUrl = song["coverUrl"] as? String ??: ""
currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L
Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal")
@ -147,7 +152,14 @@ class MusicService : MediaBrowserServiceCompat() {
instance = this instance = this
isRunning.set(true) isRunning.set(true)
setupMediaSession() setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放","Voitrans","")) val waitingTitle = getNativeLocalizedString("native.notification.music.waiting", "等待播放")
val appName = getNativeLocalizedString("native.notification.appName", "Voitrans")
// 前台通知显示条件:
// - 只要 MusicService 被启动(Flutter 调用 com.yunqiinnovation.music_service 的 play/playlist 等会启动该服务),
// onCreate 会立刻调用 startForeground(),因此下拉通知栏会出现常驻通知(哪怕还没真正开始播放);
// - 渠道为 IMPORTANCE_LOW,通常不会弹出“抬头横幅(Heads-up)”,但会在通知栏里常驻;
// - stop/close 会在 completeStop() 里 stopForeground(true)+stopSelf(),通知随之消失。
startForeground(NOTIFICATION_ID, createNotification(waitingTitle, appName, ""))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 注册广播监听器 // 注册广播监听器
@ -386,6 +398,12 @@ private fun updateProgress() {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
} }
) )
val prevLabel = getNativeLocalizedString("native.notification.music.previous", "上一首")
val nextLabel = getNativeLocalizedString("native.notification.music.next", "下一首")
val playLabel = getNativeLocalizedString("native.notification.music.play", "播放")
val pauseLabel = getNativeLocalizedString("native.notification.music.pause", "暂停")
val builder = NotificationCompat.Builder(this, CHANNEL_ID) val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title) .setContentTitle(title)
.setContentText(artist) .setContentText(artist)
@ -396,13 +414,13 @@ private fun updateProgress() {
.setMediaSession(mediaSession.sessionToken) .setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2) .setShowActionsInCompactView(0, 1, 2)
) )
.addAction(R.drawable.ic_prev, "上一首", prevIntent) .addAction(R.drawable.ic_prev, prevLabel, prevIntent)
.addAction( .addAction(
if (currentStatus) R.drawable.ic_pause else R.drawable.ic_play, if (currentStatus) R.drawable.ic_pause else R.drawable.ic_play,
if (currentStatus) "暂停" else "播放", if (currentStatus) pauseLabel else playLabel,
playPauseIntent playPauseIntent
) )
.addAction(R.drawable.ic_next, "下一首", nextIntent) .addAction(R.drawable.ic_next, nextLabel, nextIntent)
.setOngoing(false) .setOngoing(false)
.setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为 .setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为
// 异步加载封面图 // 异步加载封面图
@ -430,6 +448,18 @@ private fun updateProgress() {
return builder.build() return builder.build()
} }
private fun getNativeLocalizedString(key: String, defaultValue: String): String {
return try {
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue
val obj = JSONObject(json)
val value = obj.optString(key, defaultValue)
if (value.isBlank()) defaultValue else value
} catch (_: Exception) {
defaultValue
}
}
fun mediaButtonPendingIntent(action: Long): PendingIntent { fun mediaButtonPendingIntent(action: Long): PendingIntent {
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode()) val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode())
val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {

26
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt

@ -17,6 +17,7 @@ import com.yunqiinnovation.qq_music.R
import com.yunqiinnovation.qq_music.openid.OpenIDHelper import com.yunqiinnovation.qq_music.openid.OpenIDHelper
import com.yunqiinnovation.qq_music.util.QPlayBindHelper import com.yunqiinnovation.qq_music.util.QPlayBindHelper
import com.tencent.qqmusic.third.api.contract.* import com.tencent.qqmusic.third.api.contract.*
import org.json.JSONObject
/** /**
* Created by clydeazhang on 2021/5/11 6:43 PM. * Created by clydeazhang on 2021/5/11 6:43 PM.
@ -28,6 +29,8 @@ class PlayerService : Service() {
private const val TAG = "PlayerService" private const val TAG = "PlayerService"
private const val KEY_ENTER_FOREGROUND = "KEY_ENTER_FOREGROUND" private const val KEY_ENTER_FOREGROUND = "KEY_ENTER_FOREGROUND"
private const val CHANNEL_ID = "default" private const val CHANNEL_ID = "default"
private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization"
private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json"
} }
private val qPlayBindHelper = QPlayBindHelper(this) private val qPlayBindHelper = QPlayBindHelper(this)
@ -103,16 +106,33 @@ class PlayerService : Service() {
val enterForeground = intent?.getBooleanExtra(KEY_ENTER_FOREGROUND, false) ?: false val enterForeground = intent?.getBooleanExtra(KEY_ENTER_FOREGROUND, false) ?: false
Log.d(TAG, "onStartCommand, enter foreground=$enterForeground") Log.d(TAG, "onStartCommand, enter foreground=$enterForeground")
if (enterForeground) { if (enterForeground) {
val title = getNativeLocalizedString("native.notification.appName", "Voitrans")
val content = getNativeLocalizedString(
"native.notification.qqMusic.playing",
"Playing music"
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_background) .setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("demo标题") .setContentTitle(title)
.setContentText("demo正在播放音乐") .setContentText(content)
.build() .build()
startForeground(1, notification) startForeground(1, notification)
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
private fun getNativeLocalizedString(key: String, defaultValue: String): String {
return try {
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE)
val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue
val obj = JSONObject(json)
val value = obj.optString(key, defaultValue)
if (value.isBlank()) defaultValue else value
} catch (_: Exception) {
defaultValue
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
try { try {
@ -380,4 +400,4 @@ fun Bundle.toPrintableString(): String {
sb.append("}") sb.append("}")
return sb.toString() return sb.toString()
} }

Loading…
Cancel
Save