You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin中通过通知.addAction结合PendingIntent与BroadcastReceiver停止前台服务

解决方案

一、修正PendingIntent的创建

你的PendingIntent逻辑框架没问题,但在Android 12及以上系统中,必须指定PendingIntent.FLAG_IMMUTABLE或FLAG_MUTABLE(仅当需要动态修改Intent时使用后者),否则会抛出运行时异常。另外建议将action定义为全局常量,避免拼写错误:

// 在MyForegroundServices类外部定义常量,替换成你的应用包名前缀
const val ACTION_STOP_FOREGROUND = "com.your.app.package.STOP_FOREGROUND_SERVICE"

// 修改MyForegroundServices中PendingIntent的创建代码:
val foregroundIntent = Intent(this, NotificationReceiver::class.java).apply {
    action = ACTION_STOP_FOREGROUND
}
val foregroundPendingIntent = PendingIntent.getBroadcast(
    this,
    0,
    foregroundIntent,
    PendingIntent.FLAG_IMMUTABLE // Android 12+ 必填
)

二、注册BroadcastReceiver

必须在AndroidManifest.xml中静态注册NotificationReceiver,否则系统无法识别并分发广播:

<receiver
    android:name=".NotificationReceiver"
    android:exported="false"> <!-- 仅内部使用,设为false提升安全性 -->
    <intent-filter>
        <action android:name="com.your.app.package.STOP_FOREGROUND_SERVICE" />
    </intent-filter>
</receiver>

三、实现NotificationReceiver的onReceive方法

BroadcastReceiver不能直接调用服务的内部方法,我们通过发送带特定action的Intent通知服务自行停止:

import android.content.Context
import android.content.Intent
import android.os.Build
import android.content.BroadcastReceiver

class NotificationReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        intent?.takeIf { it.action == ACTION_STOP_FOREGROUND }?.let {
            val stopServiceIntent = Intent(context, MyForegroundServices::class.java).apply {
                action = ACTION_STOP_FOREGROUND
            }
            // Android O及以上停止前台服务需调用startForegroundService
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context?.startForegroundService(stopServiceIntent)
            } else {
                context?.startService(stopServiceIntent)
            }
        }
    }
}

四、修改MyForegroundServices处理停止逻辑

  1. 将后台线程保存为成员变量,方便后续中断:
class MyForegroundServices : Service() {
    private var serviceThread: Thread? = null

    // ... 其他代码
}
  1. 完善onStartCommand方法,处理停止指令并修正线程循环逻辑:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // 优先处理停止指令
    intent?.takeIf { it.action == ACTION_STOP_FOREGROUND }?.let {
        stopForeground(true) // 移除前台通知
        stopSelf() // 销毁服务
        serviceThread?.interrupt() // 中断后台线程
        return START_NOT_STICKY
    }

    // 仅当线程未启动时创建新线程
    if (serviceThread == null || !serviceThread!!.isAlive) {
        serviceThread = Thread {
            try {
                // 用isInterrupted判断循环,确保线程能正常退出
                while (!Thread.currentThread().isInterrupted) {
                    Log.e("Service", "Service is running...")
                    Thread.sleep(2000)
                }
            } catch (e: InterruptedException) {
                Log.e("Service", "Thread interrupted, exiting...")
            }
        }
        serviceThread?.start()
    }

    // 保留原有通知创建逻辑
    val CHANNELID = "Foreground Service ID"
    val channel = NotificationChannel(
        CHANNELID,
        CHANNELID,
        NotificationManager.IMPORTANCE_LOW
    )

    val foregroundIntent = Intent(this, NotificationReceiver::class.java).apply {
        action = ACTION_STOP_FOREGROUND
    }
    val foregroundPendingIntent = PendingIntent.getBroadcast(
        this,
        0,
        foregroundIntent,
        PendingIntent.FLAG_IMMUTABLE
    )

    getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
    val notification = Notification.Builder(this, CHANNELID)
        .setContentText("Service is running")
        .setContentTitle("Service enabled")
        .setSmallIcon(R.drawable.ic_launcher_background)
        .addAction(R.drawable.ic_launcher_background, "Stop", foregroundPendingIntent)
        .build()

    startForeground(1001, notification)

    return START_STICKY
}
  1. 重写onDestroy方法,确保线程被中断:
override fun onDestroy() {
    super.onDestroy()
    serviceThread?.interrupt()
}

五、额外注意事项

  • ACTION_STOP_FOREGROUND的前缀必须和你的应用包名一致,避免与其他应用广播冲突。
  • 后台线程的循环条件必须检查Thread.currentThread().isInterrupted,否则调用interrupt()后线程会继续运行,导致内存泄漏。

内容的提问来源于stack exchange,提问作者new developer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 14:57:54