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 = tokenUserDataUtils.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" // 通知通道 IDval channelName = "General Notifications" // 通知通道名称// 判断目标 Activityval 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_TOPif (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.BLUEenableVibration(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 ActivityManagerval appProcesses = activityManager.runningAppProcessesappProcesses?.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

<serviceandroid: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 IDUserDataUtils.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.mzph.cn/diannao/69465.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于深度学习的人工智能量化衰老模型构建与全流程应用研究

一、引言 1.1 研究背景与意义 1.1.1 人口老龄化现状与挑战 人口老龄化是当今全球面临的重要社会趋势之一,其发展态势迅猛且影响深远。根据联合国的相关数据,1980 年,全球 65 岁及以上人口数量仅为 2.6 亿,到 2021 年,这一数字已翻番,达到 7.61 亿,而预计到 2050 年,…

UnityShader学习笔记——深度与法线纹理

——内容源自唐老狮的shader课程 目录 1.概述 1.1.分别指什么 1.2.如何获取 1.2.1.对摄像机赋值 1.2.2.在Shader中声明 1.2.3.获取深度值 1.2.4.获取法线纹理 1.3.背后的原理 1.3.1.深度纹理中存储的是什么信息 1.3.2.法线纹理中存储的是什么信息 1.3.3.unity是如何…

OnlyOffice docker 运行(详细)

下载镜像 推荐使用 GitHub Action 方式下载&#xff1a; Action 地址&#xff1a;https://github.com/Shixuebin/DockerTarBuilder 教程地址&#xff1a;https://www.bilibili.com/video/BV1EZ421M7mL/ docker 镜像安装 docker load -i xxx.tar镜像运行 docker run -i -t -…

mysql 存储过程和自定义函数 详解

首先创建存储过程或者自定义函数时&#xff0c;都要使用use database 切换到目标数据库&#xff0c;因为存储过程和自定义函数都是属于某个数据库的。 存储过程是一种预编译的 SQL 代码集合&#xff0c;封装在数据库对象中。以下是一些常见的存储过程的关键字&#xff1a; 存…

基于STM32的智能鱼缸水质净化系统设计

&#x1f91e;&#x1f91e;大家好&#xff0c;这里是5132单片机毕设设计项目分享&#xff0c;今天给大家分享的是智能鱼缸水质净化系统。 目录 1、设计要求 2、系统功能 3、演示视频和实物 4、系统设计框图 5、软件设计流程图 6、原理图 7、主程序 8、总结 1、设计要求…

如何打造一个更友好的网站结构?

在SEO优化中&#xff0c;网站的结构往往被忽略&#xff0c;但它其实是决定谷歌爬虫抓取效率的关键因素之一。一个清晰、逻辑合理的网站结构&#xff0c;不仅能让用户更方便地找到他们需要的信息&#xff0c;还能提升搜索引擎的抓取效率 理想的网站结构应该像一棵树&#xff0c;…

尝试在Excel里调用硅基流动上的免费大语言模型

我个人觉得通过api而不是直接浏览器客户端聊天调用大语言模型是使用人工智能大模型的一个相对进阶的阶段。 于是就尝试了一下。我用的是老师木 袁进辉博士新创的硅基流动云上的免费的大模型。——虽然自己获赠了不少免费token&#xff0c;但测试阶段用不上。 具体步骤如下&am…

mongodb 使用内存过大分析

os 分析 内存使用 ps aux|head -1;ps aux|grep -v PID|sort -rn -k 4|head -10swap 使用 for i in $(ls /proc | grep "^[0-9]" | awk $0>100); do awk /Swap:/{aa$2}END{print "$i",a/1024"M"} /proc/$i/smaps;done| sort -k2nr | headmo…

整理:熟悉MySQL的使用和运行原理,掌握索引、事务、锁等机制。了解存储引擎、读写分离、分库分表。

系列博客目录 文章目录 系列博客目录1.MySQL的运行原理2.MySQL的使用步骤 1&#xff1a;创建数据库步骤 2&#xff1a;创建表2.1 创建 users 表2.2 创建 products 表 步骤 3&#xff1a;插入数据3.1 向 users 表插入数据3.2 向 products 表插入数据 步骤 4&#xff1a;查询数据…

“公路养护新利器!公路 AI 智慧巡检系统

家人们&#xff0c;咱日常开车出行&#xff0c;最烦的就是遇到路面坑洼、道路破损的情况&#xff0c;不仅颠簸难受&#xff0c;还存在安全隐患。其实&#xff0c;这些问题都得靠公路养护人员及时发现并处理。但以往的公路巡检工作可不容易&#xff0c;现在好了&#xff0c;有了…

mysql mvcc 锁 关系

多版本并发控制&#xff08;MVCC&#xff09;是一种用于数据库并发控制的机制&#xff0c;它可以在保证数据一致性的同时&#xff0c;提高数据库的并发性能。下面结合 MVCC 机制&#xff0c;详细阐述常见的四种事务隔离级别&#xff08;读未提交、读已提交、可重复读、串行化&a…

2502vim,vim文本对象中文文档

介绍 文本块用户(textobj-user)是一个可帮助你毫不费力地创建自己的文本对象的Vim插件. 因为有许多陷阱需要处理,很难创建文本对象.此插件隐藏了此类细节,并提供了声明式定义文本对象的方法. 你可用正则式来定义简单的文本对象,或使用函数来定义复杂的文本对象.如… 文本对…

AUTOSAR面试题集锦(1)

最基础概念 目录 最基础概念 什么是AUTOSAR?AUTOSAR到底做了什么? AUTOSAR的结构是什么样的?分为哪几层? MCAL开发 什么是MCAL? 使用过EB吗?怎样使用EB配置MCAL工程? autosar4.3.1和4.4.0有什么区别? 什么是复杂驱动? 为什么使用多核?AUTOSAR提供了几种多核…

【算法】动态规划专题⑧ —— 分组背包问题 python

目录 前置知识进入正题实战演练总结 前置知识 【算法】动态规划专题⑤ —— 0-1背包问题 滚动数组优化 python 进入正题 分组背包问题的详细解析 1. 问题定义 在 分组背包问题 中&#xff0c;物品被划分为若干组&#xff0c;每组内的物品 互斥&#xff08;只能选择其中一个或…

LLM:DeepSeek 系列(二)

原文链接 3、DeepSeek-V2 DeepSeek-V2 发布于 2024 年 5 月&#xff0c;为多领域专家&#xff08;MoE&#xff09;语言模型&#xff0c;包含总共 2360 亿个参数&#xff0c;其中每个词元激活 210 亿个参数&#xff0c;并支持 12.8 万个词元的上下文长度。DeepSeek-V2 采用包括…

AtCoder Beginner Contest 391(A~E题题解)

A - Lucky Direction 思路&#xff1a;纯模拟的一个水题 #include <bits/stdc.h> using namespace std; #define int long long string s; signed main() { cin>>s;for(int i0;i<s.size();i){char cs[i];if(cN){cout<<"S";}else if(c…

redis中的hash结构

hash类型也叫散列&#xff0c;其中value是一个无序字典&#xff0c;不用像string类型中的value用jason结构去存储&#xff0c;他的value可以将对象中的每个字段独立存储&#xff0c;而且有个好处&#xff0c;方便修改value值 类似于这样 hash类型的常见命令&#xff1a;

USB子系统学习(四)使用libusb读取鼠标数据

文章目录 1、声明2、HID协议2.1、描述符2.2、鼠标数据格式 3、应用程序4、编译应用程序5、测试 1、声明 本文是在学习韦东山《驱动大全》USB子系统时&#xff0c;为梳理知识点和自己回看而记录&#xff0c;全部内容高度复制粘贴。 韦老师的《驱动大全》&#xff1a;商品详情 …

02.08 多路文件IO

思维导图1&#xff1a; 思维导图2&#xff1a; 高效处理多路文件IO&#xff1a;select、poll和epoll模型详解 在现代网络编程中&#xff0c;高效地监视多个文件描述符的IO状态&#xff08;如可读、可写、异常&#xff09;是至关重要的。本文将详细介绍三种常用的多路文件IO模…

opentelemetry-collector 配置elasticsearch

一、修改otelcol-config.yaml receivers:otlp:protocols:grpc:endpoint: 0.0.0.0:4317http:endpoint: 0.0.0.0:4318 exporters:debug:verbosity: detailedotlp/jaeger: # Jaeger supports OTLP directlyendpoint: 192.168.31.161:4317tls:insecure: trueotlphttp/prometheus: …