Flutter:前台运行时,持续的通知声音和振动可用,但应用被关闭时则不可用(使用flutter_foreground_task)
我正在为特定通知类型(订单已接收)实现一个 持续提示音和震动。
需求
收到订单时:
- 播放持续循环的声音
- 开启重复震动
- 即使应用被完全终止也必须工作
- 仅在用户交互(接受/忽略)时停止
现有设置
我使用:
firebase_messagingflutter_local_notifications- 原生Android
ForegroundService(Kotlin) MediaPlayer与USAGE_ALARMWakeLockVibrator波形
当前行为
✅ 在前台时工作
✅ 在后台时工作
❌ 在应用被终止时不工作
当应用被终止时:
- 通知到达
onMessageReceived()被触发- 前台服务启动
- 但持续声音不会播放
FCM处理(Android)
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
val sellerScreen = data["sellerScreen"]
val driverScreen = data["driverScreen"]
val orderId = data["orderId"]
val shouldPlaySound =
sellerScreen == "5" || driverScreen == "1"
if (shouldPlaySound) {
playNotificationSound(orderId)
}
}
private fun playNotificationSound(orderId: String?) {
val intent = Intent(this, OrderAlertForegroundService::class.java).apply {
action = OrderAlertForegroundService.ACTION_START
putExtra(OrderAlertForegroundService.EXTRA_ORDER_ID, orderId)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
}
前台服务
class OrderAlertForegroundService : Service() {
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(9999, createNotification())
startContinuousSound("order")
return START_NOT_STICKY
}
private fun startContinuousSound(soundFileName: String) {
val resourceId = resources.getIdentifier(
soundFileName,
"raw",
packageName
)
mediaPlayer = MediaPlayer.create(this, resourceId)?.apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
isLooping = true
start()
}
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val pattern = longArrayOf(0, 1000, 500)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator?.vibrate(
VibrationEffect.createWaveform(pattern, 0)
)
} else {
vibrator?.vibrate(pattern, 0)
}
}
override fun onDestroy() {
mediaPlayer?.stop()
mediaPlayer?.release()
vibrator?.cancel()
super.onDestroy()
}
}
Dart端(简化)
FirebaseMessaging.onMessage.listen(_handleForeground);
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackground);
FirebaseMessaging.instance.getInitialMessage().then(_handleInitial);
后台处理程序(当前已禁用):
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await OrderAlertChannel.startOrderAlert(
alertType: 'background_fcm',
orderId: message.data['orderId'] ?? 'unknown',
);
}
重要说明
- 有效载荷为 仅数据消息
priority: highcontent_available: true- 在Android 13上测试过
-
前台服务通知使用:
-
IMPORTANCE_HIGH CATEGORY_ALARMsetOngoing(true)
问题
- Android是否在应用完全终止时限制音频播放?
- 在被终止状态下,前台服务能否从
FirebaseMessagingService可靠地启动? - 是否有更合适的方法实现像Swiggy/Zomato那样的警报式持续通知?
- 是否应该使用全屏意图+ 警报类别来实现?
目标
我希望通知的行为像真正的 警报式到达订单提醒,即使在以下情况下也能工作:
- 应用被终止
- 屏幕关闭
- 设备被锁定
如果有人在Flutter中实现了来自FCM的持久警报通知(处于终止状态),请指点。
解决方案
是的——你看到的这是Android的预期行为。
你的实现大体正确,但 现代Android在应用被终止时对后台执行有很大限制,尤其是音频播放。问题不是Flutter或 FCM——而是 Android的后台启动策略+ 音频策略+ 厂商电源限制。
下面是实际发生的情况以及正确的做法。
为什么应用被终止时声音不会播放
- FCM投递工作正常
onMessageReceived()可以在终止状态下对高优先级数据消息触发。-
因此你的服务启动请求是有效的。
-
但Android限制从后台启动的内容
自Android 12及以上版本: -
后台服务不能自由启动音频播放
- 从后台启动的前台服务是允许的,但媒体播放仍可能被抑制
- Doze模式+ 厂商电源管理器可能会延迟音频输出
-
音频焦点可能不会被即时授予
-
如果用户强制停止应用
→ 将没有任何内容运行。FCM将根本不会启动你的进程。
这是Android的一条硬性规则。
真正的根本问题
你的方法是在试图模拟一个 警报应用,但Android将以下内容视为不同的执行类别:
- 通知
- 前台服务
- 警报应用
作为 不同的执行类别
Swiggy/Zomato风格的提醒是通过 警报式通知 实现的,而不仅仅是在服务中使用MediaPlayer。
实现警报式到达订单提醒的正确方法
你应该将 三种Android机制结合起来:
使用全屏意图通知
这就是出行和外卖应用使用的做法。
它会:
- 唤醒屏幕
- 绕过后台限制
- 获得即时音频权限
- 即使在被终止状态下也能工作
你的通知构建器必须包含:
.setCategory(NotificationCompat.CATEGORY_CALL)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setFullScreenIntent(pendingIntent, true)
使用 CATEGORY_CALL 或 CATEGORY_ALARM。
通过通知本身播放声音
不要仅依赖MediaPlayer。
相反:
.setSound(alarmUri)
.setVibrate(pattern)
Android将通知声音与服务音频进行不同处理。
这也是为什么警报应用在被终止时仍然能工作。
仅将ForegroundService作为备份
在通知触发后再启动服务。
让通知唤醒设备 → 然后服务继续循环声音。
这个顺序很重要。
推荐的架构
步骤1 — 传入FCM
→ 构建一个 带声音的全屏警报通知
步骤2 — 通知唤醒设备
→ 用户能立即看到订单界面
步骤3 — 启动前台服务
→ 继续循环声音/震动,直到用户采取行动
这正是以下应用的实现方式:
- Swiggy驾驶员应用
- Uber驾驶员
- Foodpanda快递员
实现到达订单提醒。
重要AndroidManifest增强
你很可能需要:
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
For Android 14+ also declare foreground service type:
<service
android:name=".OrderAlertForegroundService"
android:foregroundServiceType="mediaPlayback" />
最终建议
是的——你应该切换为:
✔ 全屏通知
✔ 警报类别通知声音
✔ 将服务仅作为次级循环
✔ 避免仅依赖MediaPlayer
这就是构建持久订单提醒的 Android官方认可的正确方式。