Browse Source

feat(android): 同步Flutter本地化字符串到原生前台服务通知

添加 NativeLocalizationSync 服务,用于将Flutter应用的本地化字符串同步到Android原生前台服务通知中。在应用启动、语言切换和应用生命周期恢复时自动同步,确保原生通知内容与当前应用语言保持一致。

主要修改:
1. 新增 native_localization_sync.dart 服务,通过MethodChannel将本地化字符串传递给Android原生
2. 在MainApp、SettingsController和AppLifecycleManager中添加同步调用
3. 更新所有Android前台服务(音频录制、BLE代理、QQ音乐、导航、音乐服务)以使用同步的本地化字符串
4. 添加中英文通知文本到翻译文件
5. 在MainActivity中实现原生端字符串接收和存储逻辑
newdev_shunjiawei
lxm 8 months ago
parent
commit
0d90c92923
  1. 40
      android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt
  2. 87
      lib/core/services/native_localization_sync.dart
  3. 16
      lib/core/translations/language/en_us.dart
  4. 13
      lib/core/translations/language/zh_cn.dart
  5. 4
      lib/core/utils/app_lifecycle_manager.dart
  6. 4
      lib/main.dart
  7. 2
      lib/modules/settings/controllers/settings_controller.dart
  8. 24
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  9. 27
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt
  10. 24
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt
  11. 37
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  12. 24
      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 开始")
@ -148,6 +153,8 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") Log.d(TAG, "⚠️ 插件已注册,跳过重复注册")
} }
setupNativeLocalizationChannel(flutterEngine)
// Flutter引擎配置完成后,处理待处理的深链接 // Flutter引擎配置完成后,处理待处理的深链接
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
pendingShareIntent?.let { intent -> pendingShareIntent?.let { intent ->
@ -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()}';
}
}

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',

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': '权限',

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 {

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();

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;

24
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"
@ -882,11 +885,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 +907,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
}
}
} }

27
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 = ""
@ -135,9 +138,14 @@ 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()
@ -146,8 +154,9 @@ class NavigationService: Service(),AMapNaviListener {
} }
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 +171,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)

24
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"
/** /**
* 启动前台服务 * 启动前台服务
@ -124,9 +127,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 +144,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
}
}
} }

37
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,9 @@ 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")
startForeground(NOTIFICATION_ID, createNotification(waitingTitle, appName, ""))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 注册广播监听器 // 注册广播监听器
@ -386,6 +393,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 +409,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 +443,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 {

24
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 {

Loading…
Cancel
Save