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()
    }
    

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/965800.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Deepseek 【大模型】之 Ollama 与 Ollama Web UI Lite 本地部署 Deepseek 可视化UI 访问模型的简单整理

Deepseek 【大模型】之 Ollama 与 Ollama Web UI Lite 本地部署 Deepseek 可视化UI 访问模型的简单整理 目录 Deepseek 【大模型】之 Ollama 与 Ollama Web UI Lite 本地部署 Deepseek 可视化UI 访问模型部署简单整理 一、简单介绍 二、 Ollama 下载安装 三、Ollama 下载 LLM…

Excel 融合 deepseek

效果展示 代码实现 Function QhBaiDuYunAIReq(question, _Optional Authorization "your api key", _Optional Qhurl "https://qianfan.baidubce.com/v2/chat/completions")Dim XMLHTTP As ObjectDim url As Stringurl Qhurl 这里替换为你实际的URLDim …

SpringBoot开发(六)SpringBoot整合MyBatis

1. SpringBoot整合MyBatis 1.1. MyBatis介绍 MyBatis 是一款优秀的持久层Dao框架&#xff0c;它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息&#xff0c…

[数据结构] Set的使用与注意事项

目录 Set的说明 常见方法说明 注意事项 TreeSet使用案例 Set的说明 Set与Map主要的不同有两点: Set是继承自Collection的接口类,Set中只存储了Key. 常见方法说明 方法解释boolean add(E e)添加元素,但重复元素不会被添加成功void clear()清空集合boolean contains(Object…

JumpServer堡垒机管理服务器与数据库资产

第一次接触JumpServer是一位老师借给我的&#xff0c;当时想部署oceanbase 企业版V3 &#xff0c;苦于笔记本内存太小&#xff0c;后来在JumpServer上部署成功了&#xff0c;后来一直对JumpServer比较感兴趣&#xff0c;年后有时间对JumpServer进行了系统的学习 一.使用场景 我…

汽车免拆诊断案例 | 2015款奔驰R320车行驶中偶尔多个故障灯异常点亮

故障现象  一辆2015款奔驰R320车&#xff0c;搭载276 826 发动机&#xff0c;累计行驶里程约为18万km。该车行驶中&#xff0c;组合仪表上的ABS警告灯、防侧滑警告灯、发动机故障灯等多个故障灯偶尔异常点亮&#xff08;图1&#xff09;&#xff0c;且车速表不指示&#xff0…

实验3 词法分析(二)

实验3 词法分析(二) [实验目的]&#xff1a; 1 . 熟悉给定的词法分析程序&#xff1b; 2 . 改进词法分析程序。 [实验内容]&#xff1a; 1.尝试多方面改进TEST语言的文法&#xff0c;参考教材附录B词法分析程序TESTscan.c&#xff0c;在此词法分析程序的基础上改进程序&#x…

[创业之路-286]:《产品开发管理-方法.流程.工具 》-2- 人的管理是任何组织首要解决的问题 - 企业与研发组织组成、构架、组织分工

目录 一、产品开发的部门组成&#xff08;系统关键组成要素&#xff09; 1、产品开发中的市场规划部门与研发内部的市场/产品/技术预研部门的职责区别&#xff1a; 2、研发的分类&#xff1a;技术预研、平台开发、产品开发 相同点 差异点 相互联系 二、研发的组织架构 1…

使用jmeter进行压力测试

使用jmeter进行压力测试 jmeter安装 官网安装包下载&#xff0c;选择二进制文件&#xff0c;解压。 tar -xzvf apache-jmeter-x.tgz依赖jdk安装。 yum install java-1.8.0-openjdk环境变量配置&#xff0c;修改/etc/profile文件&#xff0c;添加以下内容。 export JMETER/…

matlab simulink LNG广义预测控制

1、内容简介 略 matlab simulink 120-LNG广义预测控制 可以交流、咨询、答疑 2、内容说明 略 模型分为2部分&#xff0c;一部分是simulink的结果&#xff0c;用的是pid和模糊pid控制&#xff0c;第二个模型考虑到代码计算的方便性&#xff0c;采用的m文件做仿真&#xff0…

git submodule使用

git submodule 用于关联其他独立的仓库。 它有着几点好处&#xff1a; 代码复用&#xff1a;可以将工具代码放到单独的仓库&#xff0c;再通过 submodule 关联。模块化开发&#xff1a;可以将项目拆分成多个模块&#xff0c;每个模块设置单独仓库独立开发&#xff0c;再通过 su…

python怎么求 一个数是否包含3

python求一个数包含3的方法&#xff1a; 1、使用“for i in 列表名”循环遍历列表中的每一个元素并将每个元素用str()函数转换成字符串格式 2、用“if str(3) in i”判断该元素中是否含有3 完整代码如下&#xff1a; 执行结果如下&#xff1a;

数据库系统概念第六版记录 三

外码约束&#xff08;Foreign Key Constraint&#xff09; 外码&#xff08;Foreign Key, FK&#xff09;是关系数据库中的一个约束&#xff0c;它用于保证表之间的引用完整性。外码的值必须&#xff1a; 要么存在于被引用表的主键列中&#xff0c;要么为空&#xff08;NULL&…

修改SSH登录密码,只需要登录SSH,之后输入命令即可,这里登录使用的软件为 MobaXterm1

在登入终端之后输入命令 passwd {用户名} 即可进行修改。需要注意的是&#xff0c;输入的密码不会有星号代替&#xff0c;也不会出现明文。 如果想要修改SSH的登录端口&#xff0c;比如修改为1433&#xff0c;则只需要执行以下命令即可&#xff1a; /usr/sbin/sshd -p 1433…

电商平台的设计与实现(代码+数据库+LW)

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统商品交易信息管理难度大&#xff0c;容错率低&#xff0…

【R语言】plyr包和dplyr包

一、plyr包 plyr扩展包主要是实现数据处理中的“分割-应用-组合”&#xff08;split-apply-combine&#xff09;策略。此策略是指将一个问题分割成更容易操作的部分&#xff0c;再对每一部分进行独立的操作&#xff0c;最后将各部分的操作结果组合起来。 plyr扩展包中的主要函…

oscp备考,oscp系列——VulnOSv2靶场,两种方法获取低权限shell

前言 oscp备考&#xff0c;oscp系列——VulnOSv2靶场&#xff0c;两种方法获取低权限shell 难度简单 对于低权限shell获取涉及&#xff1a;drupal 7 getshell漏洞&#xff0c;opendocman sql注入&#xff0c;ssh连接对于提权&#xff1a;内核提权 下载地址&#xff1a; http…

东方财富股吧发帖与评论爬虫

东方财富股吧发帖与评论爬虫 东方财富股吧爬虫 写在开头项目介绍主要功能文件介绍爬取逻辑 a. 爬取帖子信息b. 爬取评论信息 使用步骤 1. 下载代码2. MongoDB 安装3. Webdriver 安装4. 运行 main.py5. 查看数据 踩过的坑附录&#xff08;运行结果&#xff09; 东方财富股吧爬…

wxWidgets生成HTML文件,带图片转base64数据

编译环境大家可以看我之前的文章,CodeBlocks + msys2 + wx3.2,win10 这里功能就是生成HTML文件,没用HTML库,因为是自己固定的格式,图片是一个vector,可以动态改变数量的。 效果如下: #include <wx/string.h> #include <wx/file.h> #include <wx/ima…

网络编程 day2

题目 代码 服务器 typedef char DataType[32]; //普通节点数据类型typedef struct NODE {union{DataType data; //普通节点数据域int len; //头节点数据域};struct NODE *next; //指针域 }node,*nodePtr;struct PACK {int size; //告知 通信传输的数据的大小int type; //决定…