当前位置: 首页 > article >正文

Android FCM推送及通知栏展示

需求:

实现FIrebase Cloud Message推送功能,用户收到通知后,可以悬浮通知,自定义的大/小通知展示在通知栏,判断前台/后台,点击后进行跳转。

步骤:

一、配置及接入依赖库

1.下载 google-services.json 并放入 app/ 目录

2.项目里:

dependencies {
    classpath 'com.google.gms:google-services:4.4.0' // 确保使用最新版本
}

3.app的build.gradle 

plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
}

dependencies {
    implementation 'com.google.firebase:firebase-messaging:23.2.1' // 最新版本
}

tips:Android 13及以后,必须动态申请通知权限哦,不然不给展示 

二、FirebaseMessagingService实现接收通知

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.e("FCM", "New Token:$token")
        BaseApp.pushId = token
        UserDataUtils.toUpdate(token)//上传给服务器
    }


    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        LogUtils.e("FCM", "remoteMessage: $remoteMessage")
        remoteMessage.notification?.let {
            Log.e("FCM", "Message Notification Title: ${it.title}")
            Log.e("FCM", "Message Notification Body: ${it.body}")
        }
        remoteMessage.data.let {
            Log.e("FCM", "Message Data Payload: $it")
        }
        sendNotification2(remoteMessage.data)//我这里使用的是data里的数据
    }

    private fun sendNotification2(data: MutableMap<String, String>) {
        val channelId = "default_channel" // 通知通道 ID
        val channelName = "General Notifications" // 通知通道名称

        // 判断目标 Activity
        val targetActivity: Class<*> = if (isAppAlive(this, packageName)) {
            LogUtils.e("FCM", "isAppAlive ${isAppAlive(this, packageName)} isBackGround ${BaseApp.isBackGround}")
            if (BaseApp.isBackGround) SplashAc::class.java else PlayDetailAc::class.java
        } else {
            SplashAc::class.java
        }

        val shortData = data["payload"]
        val gson = Gson()
        val shortPlay = gson.fromJson(shortData, ShortPlay::class.java)

        // 跳转逻辑
        val intent = Intent(this, targetActivity).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
            if (targetActivity == PlayDetailAc::class.java) {
                putExtra(EXTRA_SHORT_PLAY, shortPlay)
            } else {
                putExtra(AppConstants.SHORTPLAY_ID, shortPlay) // 冷启动时传递自定义数据
            }
        }
        val pendingIntent = PendingIntent.getActivity(
            this, System.currentTimeMillis().toInt(), intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        // 获取通知管理器
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 创建通知通道(适配 Android 8.0+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply {
                description = "This channel is used for general notifications"
                enableLights(true)
                lightColor = Color.BLUE
                enableVibration(true)
            }
            notificationManager.createNotificationChannel(channel)
        }

        val radiusPx = (12 * resources.displayMetrics.density).toInt()
        val requestOptions = RequestOptions()
            .transform(CenterCrop(), RoundedCorners(radiusPx))
            .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
            .placeholder(R.mipmap.card_default)
            .error(R.mipmap.card_default)

        val customView = RemoteViews(packageName, R.layout.custom_notification_layout).apply {
            setTextViewText(R.id.notification_title, shortPlay.title ?: "Chill Shorts")
            setTextViewText(R.id.notification_message, shortPlay.desc ?: "Chill Shorts")
        }

        val smallCustomView = RemoteViews(packageName, R.layout.custom_notification_small_layout).apply {
            setTextViewText(R.id.notification_title, shortPlay.title ?: "Chill Shorts")
            setTextViewText(R.id.notification_message, shortPlay.desc ?: "Chill Shorts")
        }

        // 使用 Glide 加载图片并构建通知
        Glide.with(this).asBitmap().apply(requestOptions).load(shortPlay.coverImage).into(object : CustomTarget<Bitmap>() {
            override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                // 设置图片到自定义视图
                customView.setImageViewBitmap(R.id.notification_icon, resource)
                smallCustomView.setImageViewBitmap(R.id.notification_icon, resource)
                // 构建通知
                val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId)
                    .setStyle(NotificationCompat.DecoratedCustomViewStyle())
                    .setCustomHeadsUpContentView(smallCustomView)
                    .setSmallIcon(R.mipmap.app_logo_round)
                    .setCustomContentView(smallCustomView)
                    .setCustomBigContentView(customView)
                    .setPriority(NotificationCompat.PRIORITY_HIGH)
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true)
                val notification = notificationBuilder.build()
                notification.flags = Notification.FLAG_AUTO_CANCEL
                // 发送通知
                notificationManager.notify(System.currentTimeMillis().toInt(), notification)
            }

            override fun onLoadFailed(errorDrawable: Drawable?) {
                // 图片加载失败,使用默认占位图
                customView.setImageViewResource(R.id.notification_icon, R.mipmap.card_default)
                smallCustomView.setImageViewResource(R.id.notification_icon, R.mipmap.card_default)

                // 构建通知
                val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId)
                    .setStyle(NotificationCompat.DecoratedCustomViewStyle())
                    .setCustomHeadsUpContentView(smallCustomView)
                    .setSmallIcon(R.mipmap.app_logo_round)
                    .setCustomContentView(smallCustomView)
                    .setCustomBigContentView(customView)
                    .setPriority(NotificationCompat.PRIORITY_HIGH)
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true)
                // 发送通知
                notificationManager.notify(System.currentTimeMillis().toInt(), notificationBuilder.build())
            }

            override fun onLoadCleared(placeholder: Drawable?) {
                // 清理资源时无操作
            }
        })
    }


    private fun isAppAlive(context: Context, packageName: String): Boolean {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val appProcesses = activityManager.runningAppProcesses
        appProcesses?.forEach {
            if (it.processName == packageName && it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                return true
            }
        }
        return false
    }


}

三、注意事项和代码逻辑

1.RemoteMessage.data获取控制台的配置的数据

2.isAppAlive判断当前App是否存活,isBackGround判断App是否处于后台

3.intent和pendingIntent要设置正确的,合适的flag

常见的 PendingIntent Flags

Flag说明
FLAG_CANCEL_CURRENT取消当前已有的 PendingIntent,并创建新的 PendingIntent(适用于要确保新的 Intent 被处理的情况)。
FLAG_UPDATE_CURRENT更新已存在的 PendingIntent,保持 IntentExtras 最新。
FLAG_NO_CREATE如果 PendingIntent 存在,则返回它;否则返回 null,不会创建新的 PendingIntent
FLAG_ONE_SHOTPendingIntent 只能使用一次,执行后自动销毁。
FLAG_IMMUTABLE (API 23+)PendingIntent 不能被修改(Android 12 及以上必须显式指定 FLAG_IMMUTABLEFLAG_MUTABLE)。
FLAG_MUTABLE (API 31+)PendingIntent 可以被 AlarmManagerNotificationManager 等修改,适用于 Foreground Service 及 Remote Input。

4.常见的 Notification Flags

Flag说明
Notification.FLAG_AUTO_CANCEL点击通知后自动取消(移除通知)
Notification.FLAG_ONGOING_EVENT使通知成为前台通知(用户不能手动清除)
Notification.FLAG_NO_CLEAR不能通过滑动或清除按钮删除通知
Notification.FLAG_FOREGROUND_SERVICE适用于前台服务的通知
Notification.FLAG_INSISTENT让通知的声音、震动等一直持续,直到用户处理

5. 记得适配NotificationChannel。

6.RemoteViews是用于设置通知的自定义View的,在上述的代码里,我设置了

val notificationBuilder = NotificationCompat.Builder(this@MyFirebaseMessagingService, channelId)
    .setStyle(NotificationCompat.DecoratedCustomViewStyle())
    .setCustomHeadsUpContentView(smallCustomView)//悬浮通知
    .setSmallIcon(R.mipmap.app_logo_round)
    .setCustomContentView(smallCustomView)//正常的自定义通知view
    .setCustomBigContentView(customView)//展开后的大通知View
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_AUTO_CANCEL
// 发送通知
notificationManager.notify(System.currentTimeMillis().toInt(), notification)

 7.在 Android 8.0 (API 26) 及更高版本,官方建议使用 NotificationChannel 控制通知行为,而不是直接使用 flags。使用.setAutoCancel(true) // 等价于 FLAG_AUTO_CANCEL,但是上面为啥我加了notification.flags = Notification.FLAG_AUTO_CANCEL,是因为设置自定义的通知,似的setAutoCancel失效了,所以又对flag进行了配置。(这个坑了我好一会儿)

四、注册 FirebaseMessagingService

manifest.xml

<service
    android:name=".MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

五、获取Token

 FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                LogUtils.e("FCM Token", "token:${task.result}")
                pushId = task.result // 这是你的 Firebase Push ID
                UserDataUtils.toUpdate(pushId.toString())//上传后端
            } else {
                LogUtils.e("FCM Token", "获取 Firebase Push ID 失败")
            }
        }

📌 案例:IM 消息推送通知的设计与优化

常见问题及其优化:

🟢 问题 1:消息推送通知丢失
🛠 问题描述
  • IM 消息是通过 FCM(Firebase Cloud Messaging)厂商推送(小米、华为、OPPO) 发送的,有时候通知收不到,比如:
    • App 进程被杀死,无法接收到推送。
    • Android 8.0+ 以后,应用后台时间过长,FCM 推送可能被系统限制。
    • 部分国产 ROM 对后台进程的管控严格,通知会被系统拦截。
🚀 解决方案
  • FCM 作为主要推送通道(Google Play 设备)。
  • 厂商通道(小米、华为、OPPO、vivo)作为备用推送通道。
  • 长连接保活(用户在线时,直接使用 WebSocket 推送)。
  • 检测推送通道是否可用

    • 如果 FCM 无法收到消息,就尝试 WebSocket轮询 获取未读消息。
  • 使用 WorkManager 确保消息送达

    • onMessageReceived() 里保存消息,防止推送丢失。
    • 结合 WorkManager 定时检查未读消息。

🟢 问题 2:重复通知、通知不合并

🛠 问题描述
  • 多条 IM 消息推送后,每条消息都会弹出 独立通知,导致通知栏很混乱。
  • 例如:
    • 5 条新消息,出现 5 个通知。
    • 点击某个通知后,其他通知还在。
🚀 解决方案
  1. 使用 setGroup() 进行通知分组

    • 单聊消息:不同用户的聊天,不同 ID(notify(userID, notification))。
    • 群聊消息:同一个群的消息,使用 setGroup() 归类。
      val groupKey = "IM_GROUP_CHAT"
      
      // 子通知
      val messageNotification = NotificationCompat.Builder(this, channelId)
          .setContentTitle("新消息")
          .setContentText("你有 3 条未读消息")
          .setSmallIcon(R.drawable.ic_message)
          .setGroup(groupKey)
          .build()
      
      // 汇总通知(id = 0,保证只有一个)
      val summaryNotification = NotificationCompat.Builder(this, channelId)
          .setContentTitle("IM 消息")
          .setContentText("你有新的消息")
          .setSmallIcon(R.drawable.ic_message)
          .setGroup(groupKey)
          .setGroupSummary(true)
          .build()
      
      notificationManager.notify(1, messageNotification)
      notificationManager.notify(0, summaryNotification)
      

🟢 问题 3:通知点击后跳转异常

🛠 问题描述
  • 用户点击通知后,应该跳转到 聊天页面,但可能会:
    • 进入应用后,未能正确跳转到聊天界面。
    • 如果 App 进程被杀死,点击通知后只能进入启动页,而不是聊天页面。
🚀 解决方案
  1. 使用 PendingIntent.FLAG_UPDATE_CURRENT 确保 intent 只创建一次

  2. App 被杀死时,恢复正确页面,

  3. SplashActivity 里判断 Intent,决定是否直接进入聊天界面:
    if (intent?.hasExtra("chatId") == true) {
        startActivity(Intent(this, ChatActivity::class.java).apply {
            putExtras(intent.extras!!)
        })
        finish()
    }
    

 


http://www.kler.cn/a/535014.html

相关文章:

  • GGML、GGUF、GPTQ 都是啥?
  • systemverilog的program和module的区别
  • 如何在自己电脑上私有化部署deep seek
  • 基于Springboot+vue的租车网站系统
  • 游戏引擎学习第88天
  • 开源安全一站式构建!开启企业开源治理新篇章
  • 04. Flink的状态管理与容错机制
  • vulnhub刷题记录(HACKSUDO: SEARCH)
  • 机器学习-数据清洗(一)
  • Docker最佳实践:安装Nacos
  • 备考蓝桥杯:枚举算法之扫雷
  • 在 Open WebUI + Ollama 上运行 DeepSeek-R1-70B 实现调用
  • RabbitMQ延迟消息的两种实现方式
  • 【JavaEE】Spring(9):Spring事务
  • 【YOLOv11改进- 注意力机制】YOLOv11+ACMix注意力机制(2021): 自注意力与卷积的聚合模块,助力YOLOv11有效涨点;
  • Apache SeaTunnel 整体架构运行原理
  • 【数据结构】循环链表
  • 最大矩阵的和
  • 《翻转组件库之发布》
  • Nexus简介及小白使用IDEA打包上传到Nexus3私服详细教程_ider2021 引用 nexus 上传
  • 怎么定义 vue-router 的动态路由?
  • 资源查找网址
  • es match 可查 而 term 查不到 问题分析
  • 前端开发知识梳理 - HTMLCSS
  • 202617读书笔记|《清溪俳句三百》——春有樱花,夏有蝉,秋有红叶,冬有雪
  • 寒假2.5