Flutter:前台运行时,持续的通知声音和振动可用,但应用被关闭时则不可用(使用flutter_foreground_task)

移动开发 2026-07-12

我正在为特定通知类型(订单已接收)实现一个 持续提示音和震动

需求

收到订单时:

  • 播放持续循环的声音
  • 开启重复震动
  • 即使应用被完全终止也必须工作
  • 仅在用户交互(接受/忽略)时停止

现有设置

我使用:

  • firebase_messaging
  • flutter_local_notifications
  • 原生Android ForegroundService(Kotlin)
  • MediaPlayerUSAGE_ALARM
  • WakeLock
  • Vibrator 波形

当前行为

✅ 在前台时工作
✅ 在后台时工作
❌ 在应用被终止时不工作

当应用被终止时:

  • 通知到达
  • 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: high
  • content_available: true
  • 在Android 13上测试过
  • 前台服务通知使用:

  • IMPORTANCE_HIGH

  • CATEGORY_ALARM
  • setOngoing(true)

问题

  1. Android是否在应用完全终止时限制音频播放?
  2. 在被终止状态下,前台服务能否从 FirebaseMessagingService 可靠地启动?
  3. 是否有更合适的方法实现像Swiggy/Zomato那样的警报式持续通知?
  4. 是否应该使用全屏意图+ 警报类别来实现?

目标

我希望通知的行为像真正的 警报式到达订单提醒,即使在以下情况下也能工作:

  • 应用被终止
  • 屏幕关闭
  • 设备被锁定

如果有人在Flutter中实现了来自FCM的持久警报通知(处于终止状态),请指点。

解决方案

是的——你看到的这是Android的预期行为。

你的实现大体正确,但 现代Android在应用被终止时对后台执行有很大限制,尤其是音频播放。问题不是Flutter或 FCM——而是 Android的后台启动策略+ 音频策略+ 厂商电源限制

下面是实际发生的情况以及正确的做法。


为什么应用被终止时声音不会播放

  1. FCM投递工作正常
  2. onMessageReceived() 可以在终止状态下对高优先级数据消息触发。
  3. 因此你的服务启动请求是有效的。

  4. 但Android限制从后台启动的内容
    自Android 12及以上版本:

  5. 后台服务不能自由启动音频播放

  6. 从后台启动的前台服务是允许的,但媒体播放仍可能被抑制
  7. Doze模式+ 厂商电源管理器可能会延迟音频输出
  8. 音频焦点可能不会被即时授予

  9. 如果用户强制停止应用
    → 将没有任何内容运行。FCM将根本不会启动你的进程。

这是Android的一条硬性规则。


真正的根本问题

你的方法是在试图模拟一个 警报应用,但Android将以下内容视为不同的执行类别:

  • 通知
  • 前台服务
  • 警报应用

作为 不同的执行类别

Swiggy/Zomato风格的提醒是通过 警报式通知 实现的,而不仅仅是在服务中使用MediaPlayer。


实现警报式到达订单提醒的正确方法

你应该将 三种Android机制结合起来:


使用全屏意图通知

这就是出行和外卖应用使用的做法。

它会:

  • 唤醒屏幕
  • 绕过后台限制
  • 获得即时音频权限
  • 即使在被终止状态下也能工作

你的通知构建器必须包含:

.setCategory(NotificationCompat.CATEGORY_CALL) 
.setPriority(NotificationCompat.PRIORITY_MAX)
.setFullScreenIntent(pendingIntent, true)

使用 CATEGORY_CALLCATEGORY_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官方认可的正确方式

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章