Android应用后台/空闲时收到推送无法启动Service播放MP3
问题根因
Android 8.0(API 级别 26)起系统引入后台执行限制:应用未处于前台可见状态时,直接调用startService()启动普通后台服务会抛出IllegalStateException导致启动失败,这就是应用切后台后Service无法正常运行的核心原因。你当前使用的普通Service启动逻辑仅在应用前台时合法,后台场景必须按系统规则适配前台服务。
适配方案
1. 声明所需权限与服务属性
首先在AndroidManifest.xml中补充前台服务相关权限,Android 12及以上版本需要额外声明媒体类前台服务类型,Android 13及以上需要补充通知运行时权限:
<!-- 基础前台服务权限,API26+必需 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Android 12+ 声明媒体播放类型前台服务 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <!-- Android 13+ 通知运行时权限,需用户动态授权 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
同时在Manifest中注册MusicService时补充前台服务类型属性:
<service android:name=".MusicService" android:exported="false" android:foregroundServiceType="mediaPlayback" />
2. 修正Firebase推送中启动Service的逻辑
禁止直接调用startService(),按系统版本区分启动方式:API26及以上使用startForegroundService()启动服务,该方法允许应用在后台临时唤起服务,但要求服务启动后5秒内必须调用startForeground()绑定前台通知,否则服务会被系统直接杀死并触发ANR。
修改FirebaseMessaging类中的启动代码如下:
val resultIntent = Intent(applicationContext, MusicService::class.java).apply { putExtra("title", title) putExtra("body", body) putExtra("action_type", action_type) putExtra("deep_link_type", deep_link_type) putExtra("deep_link_id", deep_link_id) putExtra("masjid_Id", masjid_Id) putExtra("url", url) action = "com.example.mymasjid" } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(resultIntent) } else { startService(resultIntent) }
3. 改造MusicService为合规前台服务
你当前的Service虽然会发送通知,但没有调用startForeground()将通知与前台服务绑定,不符合系统要求,需要做以下修改:
- 服务启动后第一时间构建通知并调用
startForeground()绑定,确保在5秒时限内完成 - 替换同步阻塞的
prepare()为异步prepareAsync(),避免主线程阻塞触发ANR - 音频播放完成/出错时主动停止前台服务、移除通知
- 适配高版本PendingIntent的Flag要求,避免启动异常
修改onStartCommand代码:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { title = intent?.getStringExtra("title").orEmpty() body = intent?.getStringExtra("body").orEmpty() Log.i("***TAG","From Background Service") // 立刻构建前台通知并绑定服务,必须在启动后5秒内执行 val foregroundNotification = buildForegroundNotification(title, body) // 注意:startForeground的通知ID不能为0,此处固定使用1001即可 startForeground(1001, foregroundNotification) val url = "https://cdn.islamic.network/quran/audio/64/ar.alafasy/9.mp3" if (mediaPlayer != null) { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null } mediaPlayer = MediaPlayer() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mediaPlayer?.setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setLegacyStreamType(AudioManager.STREAM_MUSIC) .build() ) } else { mediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC) } try { mediaPlayer?.setDataSource(url) // 替换同步prepare为异步方法,避免主线程阻塞 mediaPlayer?.prepareAsync() mediaPlayer?.isLooping = false mediaPlayer?.setOnPreparedListener(this) mediaPlayer?.setOnCompletionListener(this) mediaPlayer?.setOnErrorListener(this) } catch (e: IOException) { e.printStackTrace() // 初始化出错直接停止服务 stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } return START_NOT_STICKY }
将原来的adhanNotification方法重构为前台通知构建方法,修正通知配置:
private fun buildForegroundNotification(title: String, body: String): Notification { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val channelId = "media_play_channel" // Android 8以上创建通知渠道 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, "音频播放通知", NotificationManager.IMPORTANCE_LOW ).apply { enableVibration(true) vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400) setSound(null, null) // 关闭通知默认声音,避免与自定义MP3播放冲突 } notificationManager.createNotificationChannel(channel) } val collapsedView = RemoteViews(packageName, R.layout.notification) val expandView = RemoteViews(packageName, R.layout.notification_expand) val time = Utility.getTime("LLL dd, KK:mm aaa") collapsedView.setTextViewText(R.id.tvNotifyTime, time) collapsedView.setTextViewText(R.id.title, title) collapsedView.setTextViewText(R.id.body, body) expandView.setTextViewText(R.id.tvNotifyTime, time) expandView.setTextViewText(R.id.title, title) expandView.setTextViewText(R.id.body, body) val onCancelIntent = Intent(this, CancelNotification::class.java) // 适配高版本PendingIntent Flag val pendingFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT } val onDismissPendingIntent = PendingIntent.getService( applicationContext, 0, onCancelIntent, pendingFlag ) return NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ic_notification) .setDeleteIntent(onDismissPendingIntent) .setOngoing(true) // 前台服务通知需设为常驻,不可被滑动删除 .setPriority(NotificationCompat.PRIORITY_LOW) .setCustomContentView(collapsedView) .setCustomBigContentView(expandView) .build() }
修改播放器回调逻辑,播放结束/出错时正确释放资源、停止服务:
override fun onPrepared(p0: MediaPlayer?) { mediaPlayer?.start() Log.i("***play", "play") } override fun onCompletion(p0: MediaPlayer?) { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null Log.i("***play", "complete") // 停止前台服务并移除通知 stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } override fun onError(p0: MediaPlayer?, p1: Int, p2: Int): Boolean { Log.i("***play", "error") mediaPlayer?.release() mediaPlayer = null stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() return true }
4. 额外适配注意事项
- Android 13及以上版本需要在应用启动时主动申请
POST_NOTIFICATIONS运行时权限,用户授权后才能正常显示前台通知,否则服务启动会失败。 - 推送配置需注意:Firebase推送如果同时携带
notification字段和data字段,应用在后台时Firebase会自动拦截推送、生成系统通知,不会触发你自定义的onMessageReceived逻辑,需要确保推送仅携带data字段,才能保证所有场景下都走自定义播放逻辑。 - 针对国内定制ROM(小米、华为、OPPO、vivo等),需要引导用户手动将应用加入后台运行白名单、开启自启动权限,否则部分ROM会在应用后台时强制拦截服务启动。
内容的提问来源于stack exchange,提问作者Abdul Rasheed
相关产品推荐
相关产品推荐

