From 0d90c92923b97a3da63a3b173a32f0761231f9a7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 5 Feb 2026 20:23:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(android):=20=E5=90=8C=E6=AD=A5Flutter?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=88=B0?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 NativeLocalizationSync 服务,用于将Flutter应用的本地化字符串同步到Android原生前台服务通知中。在应用启动、语言切换和应用生命周期恢复时自动同步,确保原生通知内容与当前应用语言保持一致。 主要修改: 1. 新增 native_localization_sync.dart 服务,通过MethodChannel将本地化字符串传递给Android原生 2. 在MainApp、SettingsController和AppLifecycleManager中添加同步调用 3. 更新所有Android前台服务(音频录制、BLE代理、QQ音乐、导航、音乐服务)以使用同步的本地化字符串 4. 添加中英文通知文本到翻译文件 5. 在MainActivity中实现原生端字符串接收和存储逻辑 --- .../com/saitong/voitrans/MainActivity.kt | 40 +++++++++ .../services/native_localization_sync.dart | 87 +++++++++++++++++++ lib/core/translations/language/en_us.dart | 16 ++++ lib/core/translations/language/zh_cn.dart | 13 +++ lib/core/utils/app_lifecycle_manager.dart | 4 + lib/main.dart | 4 + .../controllers/settings_controller.dart | 2 + .../agent_service/BleMediaService.kt | 24 ++++- .../amap_service/NavigationService.kt | 29 ++++++- .../tools/AudioRecordingForegroundService.kt | 24 ++++- .../music_service/MusicService.kt | 37 ++++++-- .../qq_music/pcm/PlayerService.kt | 26 +++++- 12 files changed, 289 insertions(+), 17 deletions(-) create mode 100644 lib/core/services/native_localization_sync.dart diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt index acf5653b8..135cbbf48 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt @@ -19,6 +19,7 @@ import android.net.Uri import android.provider.OpenableColumns import java.io.File import java.io.FileOutputStream +import org.json.JSONObject class MainActivity: FlutterActivity() { private val TAG = "MainActivity" @@ -34,6 +35,10 @@ class MainActivity: FlutterActivity() { private val DEEPLINK_CHANNEL = "com.saitong.voitrans/deeplink" private val DELIVERY_RETRY_DELAY_MS = 300L 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?) { Log.d(TAG, "🚀 MainActivity onCreate 开始") @@ -147,6 +152,8 @@ class MainActivity: FlutterActivity() { } else { Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") } + + setupNativeLocalizationChannel(flutterEngine) // Flutter引擎配置完成后,处理待处理的深链接 Handler(Looper.getMainLooper()).postDelayed({ @@ -172,6 +179,39 @@ class MainActivity: FlutterActivity() { 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?) { intent ?: return diff --git a/lib/core/services/native_localization_sync.dart b/lib/core/services/native_localization_sync.dart new file mode 100644 index 000000000..5c3d74820 --- /dev/null +++ b/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 nativeKeys = [ + '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 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 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', { + 'locale': localeKey, + 'strings': payload, + }); + _lastLocaleKey = localeKey; + _lastPayloadHash = payloadHash; + } finally { + _isSyncing = false; + } + } + + static Map _buildPayload(String localeKey) { + final Map> all = AppTranslations().keys; + final Map? current = all[localeKey]; + final Map? fallback = + all['en_US'] ?? all[AppTranslations.fallbackLocale.toString()]; + + final Map result = {}; + 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()}'; + } +} diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index bce2e24c8..fae882d12 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -1083,6 +1083,22 @@ const Map enUS = { 'storagePermissionPermanentDenial': '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 'permissionUtilNeed': 'Need ', 'permissionUtilPermission': ' Permission', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index a858ca3ec..3bc96f722 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -981,6 +981,19 @@ const Map zhCN = { '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 'permissionUtilNeed': '需要', 'permissionUtilPermission': '权限', diff --git a/lib/core/utils/app_lifecycle_manager.dart b/lib/core/utils/app_lifecycle_manager.dart index 5ba7e1399..3f0f744b8 100644 --- a/lib/core/utils/app_lifecycle_manager.dart +++ b/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 '../../routes/app_routes.dart'; import 'logger.dart'; +import '../services/native_localization_sync.dart'; import '../translations/app_translations.dart'; //import '../../modules/settings/controllers/settings_controller.dart'; @@ -36,6 +37,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { // 注册应用生命周期观察者 WidgetsBinding.instance.addObserver(this); Logger.info('AppLifecycleManager 已初始化'); + unawaited(NativeLocalizationSync.syncToNative(force: true)); unawaited(refreshBleWakeupPolicy()); } @@ -174,6 +176,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { // 延迟执行,确保系统语言变化处理完成 Future.delayed(const Duration(milliseconds: 100), () { Get.updateLocale(userLocale); + unawaited(NativeLocalizationSync.syncToNative(force: true)); Logger.info('已恢复用户设置的语言: ${userLocale.toString()}'); }); } else { @@ -183,6 +186,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { Locale? newLocale = AppTranslations.deviceLocale(); if (newLocale != null) { Get.updateLocale(newLocale); + unawaited(NativeLocalizationSync.syncToNative(force: true)); // 同步更新设置控制器中的语言状态 try { diff --git a/lib/main.dart b/lib/main.dart index 758f45e4a..6d6509671 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; import 'core/bindings/initial_binding.dart'; import 'core/utils/logger.dart'; +import 'core/services/native_localization_sync.dart'; import 'data/services/login_service.dart'; import 'modules/meeting/controllers/meeting_home_controller.dart'; import 'modules/splash/controllers/splash_controller.dart'; @@ -165,6 +166,9 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { // 在应用构建时立即初始化深链接处理 _initDeepLinkHandler(); + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(NativeLocalizationSync.syncToNative()); + }); // 获取保存的主题设置 final themeMode = ThemeService().getThemeMode(); diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 4cbc4de90..78f1f078b 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/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 '../../../core/themes/theme_service.dart'; +import '../../../core/services/native_localization_sync.dart'; import '../../../core/translations/app_translations.dart'; import '../../../data/services/network/dio_manager.dart'; import '../../../routes/app_routes.dart'; @@ -128,6 +129,7 @@ class SettingsController extends GetxController { Get.updateLocale(targetLocale); Logger().i("使用fallback语言: ${targetLocale.toString()}"); } + await NativeLocalizationSync.syncToNative(force: true); // 保存用户选择 currentLanguage.value = selectedLanguage; diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 5c6415e49..2e9c91893 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -12,6 +12,7 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import android.util.Log +import org.json.JSONObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -47,6 +48,8 @@ class BleMediaService : Service(), CoroutineScope { private const val CHANNEL_ID = "ble_agent_service_channel" private const val NOTIFICATION_ID = 1002 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" @@ -882,11 +885,16 @@ private var companionManager: BleCompanionManager? = null } 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) - .setContentTitle("Voitrans") - .setContentText("Voice assistant agent is running") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(appIconResId) .setOngoing(true) .setOnlyAlertOnce(true) @@ -899,4 +907,16 @@ private var companionManager: BleCompanionManager? = null 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 + } + } } diff --git a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt b/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt index c926010b0..6ec4d9e2e 100644 --- a/local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt +++ b/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 androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import org.json.JSONObject import com.amap.api.navi.AMapNavi import com.amap.api.navi.AMapNaviListener 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_Name = "导航服务" 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 var startpos: String = "" @@ -135,9 +138,14 @@ class NavigationService: Service(),AMapNaviListener { manager.createNotificationChannel(channel) } + val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中") + val content = getNativeLocalizedString( + "native.notification.navigation.keepRunning", + "正在导航中,请保持应用运行" + ) val notification = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("导航进行中") - .setContentText("正在导航中,请保持应用运行") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(R.drawable.ic_navigation) // 使用你自己的图标资源 .setOngoing(true) .build() @@ -146,8 +154,9 @@ class NavigationService: Service(),AMapNaviListener { } private fun updateNotification(contentText: String, iconBitmap: Bitmap?) { + val title = getNativeLocalizedString("native.notification.navigation.title", "导航进行中") val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("导航进行中") + .setContentTitle(title) .setContentText(contentText) .setSmallIcon(R.drawable.ic_navigation) // 通知栏图标(小) .setOnlyAlertOnce(true) @@ -162,6 +171,18 @@ class NavigationService: Service(),AMapNaviListener { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 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 { val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888) val canvas = android.graphics.Canvas(result) @@ -435,4 +456,4 @@ class NavigationService: Service(),AMapNaviListener { isRunning.set(false) super.onDestroy() } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt index 620c08dda..77f3fbacb 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt +++ b/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 androidx.core.app.NotificationCompat 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_NAME = "音频录制服务" 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" /** * 启动前台服务 @@ -124,9 +127,14 @@ class AudioRecordingForegroundService : Service() { 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) - .setContentTitle("Voitrans") - .setContentText("Recording audio, tap to return to app") + .setContentTitle(title) + .setContentText(content) .setSmallIcon(applicationInfo.icon) .setContentIntent(pendingIntent) .setOngoing(true) // 设置为持续通知,用户无法滑动删除 @@ -136,4 +144,16 @@ class AudioRecordingForegroundService : Service() { .setAutoCancel(false) // 点击通知后不自动取消 .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 + } + } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 0bda0f3e1..d1dad6aaa 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -25,6 +25,7 @@ import androidx.media.MediaBrowserServiceCompat import androidx.media.session.MediaButtonReceiver import android.util.Log import android.view.KeyEvent +import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL 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 NOTIFICATION_ID = 1002 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 var currentIdInternal: Long? = null @@ -87,8 +90,10 @@ class MusicService : MediaBrowserServiceCompat() { fun updateMediaSessionMetadata(song: Map) { instance?.let { service -> try { - val title = song["title"] as? String ??: "未知标题" - val artist = song["artist"] as? String ??: "未知歌手" + val title = song["title"] as? String + ?: service.getNativeLocalizedString("unknownSong", "未知歌曲") + val artist = song["artist"] as? String + ?: service.getNativeLocalizedString("unknownArtist", "未知歌手") val coverUrl = song["coverUrl"] as? String ??: "" currentIdInternal = (song["id"] as? Number)?.toLong() ?: 0L Log.i(TAG, "updateMediaSessionMetadata currentIdInternal: $currentIdInternal") @@ -147,7 +152,9 @@ class MusicService : MediaBrowserServiceCompat() { instance = this isRunning.set(true) setupMediaSession() - startForeground(NOTIFICATION_ID, createNotification("等待播放","Voitrans","")) + val waitingTitle = getNativeLocalizedString("native.notification.music.waiting", "等待播放") + val appName = getNativeLocalizedString("native.notification.appName", "Voitrans") + startForeground(NOTIFICATION_ID, createNotification(waitingTitle, appName, "")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager // 注册广播监听器 @@ -386,6 +393,12 @@ private fun updateProgress() { 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) .setContentTitle(title) .setContentText(artist) @@ -396,13 +409,13 @@ private fun updateProgress() { .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0, 1, 2) ) - .addAction(R.drawable.ic_prev, "上一首", prevIntent) + .addAction(R.drawable.ic_prev, prevLabel, prevIntent) .addAction( if (currentStatus) R.drawable.ic_pause else R.drawable.ic_play, - if (currentStatus) "暂停" else "播放", + if (currentStatus) pauseLabel else playLabel, playPauseIntent ) - .addAction(R.drawable.ic_next, "下一首", nextIntent) + .addAction(R.drawable.ic_next, nextLabel, nextIntent) .setOngoing(false) .setDeleteIntent(deletePendingIntent) // 设置通知被滑动删除时的行为 // 异步加载封面图 @@ -430,6 +443,18 @@ private fun updateProgress() { 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 { val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, action.toKeyCode()) val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt index f3152ff3e..c172e3a0c 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt +++ b/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.util.QPlayBindHelper import com.tencent.qqmusic.third.api.contract.* +import org.json.JSONObject /** * 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 KEY_ENTER_FOREGROUND = "KEY_ENTER_FOREGROUND" 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) @@ -103,16 +106,33 @@ class PlayerService : Service() { val enterForeground = intent?.getBooleanExtra(KEY_ENTER_FOREGROUND, false) ?: false Log.d(TAG, "onStartCommand, enter foreground=$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) .setSmallIcon(R.drawable.ic_launcher_background) - .setContentTitle("demo标题") - .setContentText("demo正在播放音乐") + .setContentTitle(title) + .setContentText(content) .build() startForeground(1, notification) } 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() { super.onDestroy() try { @@ -380,4 +400,4 @@ fun Bundle.toPrintableString(): String { sb.append("}") return sb.toString() -} \ No newline at end of file +}