首页 > Java > java教程 > 正文

解决Android 13+ FCM数据消息不显示通知的问题:权限与通道管理

碧海醫心
发布: 2025-07-09 18:32:01
原创
579人浏览过

解决Android 13+ FCM数据消息不显示通知的问题:权限与通道管理

本教程旨在解决Android应用中Firebase Cloud Messaging (FCM)数据消息已接收但通知未显示的问题。核心原因通常是Android 13 (API 33)引入的运行时通知权限缺失,以及Android 8.0 (API 26)及以上版本中通知通道未正确配置。文章将详细指导如何声明和请求通知权限,并演示如何创建和使用通知通道来确保FCM数据消息能够成功在用户设备上展示为系统通知。

在使用firebase cloud messaging (fcm)时,开发者常常会遇到一个令人困惑的问题:后台日志显示fcm数据消息已成功接收,但用户设备上却迟迟不显示通知。这通常不是因为fcm消息未送达,而是客户端应用在接收到数据后,未能正确地创建并展示系统通知。本文将深入探讨导致此问题的主要原因,并提供一套完整的解决方案。

理解FCM消息类型

在深入解决方案之前,有必要简要回顾FCM的两种主要消息类型:

  1. 通知消息 (Notification Message):由FCM SDK自动处理,当应用在后台或被杀死时,FCM会自动在系统托盘中显示通知。当应用在前台时,onMessageReceived() 回调会被触发,但 remoteMessage.getNotification() 会返回 null,需要手动构建通知。
  2. 数据消息 (Data Message):完全由客户端应用处理。无论应用处于前台、后台还是被杀死,onMessageReceived() 回调都会被触发。开发者需要从 remoteMessage.getData() 中提取数据,并手动构建并显示系统通知。

本教程主要关注数据消息,因为它们提供了更大的灵活性,但也要求开发者承担更多的通知显示责任。

Android 13 (API 33) 及以上:运行时通知权限

自Android 13 (API 33) 起,应用程序在发送通知之前,必须获得用户的显式许可。这是为了提供用户对其通知体验的更大控制权。如果应用未获得 POST_NOTIFICATIONS 权限,即使您在代码中正确构建了通知,系统也不会显示它。

1. 在 AndroidManifest.xml 中声明权限

首先,您需要在应用的 AndroidManifest.xml 文件中声明此权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 其他权限 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <!-- 其他权限 -->

    <application ...>
        <!-- 应用组件 -->
    </application>
</manifest>
登录后复制

2. 在运行时请求权限

由于这是一个运行时权限,您需要在用户首次启动应用或在需要发送通知时,向用户请求此权限。通常,这会在应用的启动 Activity 或首次需要显示通知的组件中完成。

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

public class MainActivity extends AppCompatActivity {

    private ActivityResultLauncher<String> requestPermissionLauncher;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化权限请求启动器
        requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
            if (isGranted) {
                // 权限已授予,可以发送通知了
                // Log.d("Permissions", "通知权限已授予");
            } else {
                // 权限被拒绝,可能需要解释为什么需要此权限
                // Log.d("Permissions", "通知权限被拒绝");
            }
        });

        // 检查并请求通知权限
        checkAndRequestNotificationPermission();
    }

    private void checkAndRequestNotificationPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // API 33
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
                // 权限已授予
                // Log.d("Permissions", "通知权限已授予 (之前)");
            } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
                // 解释为什么需要此权限(可选)
                // Log.d("Permissions", "需要解释通知权限的原因");
                // 在此处显示一个对话框或UI来解释
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
            } else {
                // 直接请求权限
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
            }
        }
    }
}
登录后复制

Android 8.0 (API 26) 及以上:通知通道 (Notification Channels)

除了权限问题,另一个常见且容易被忽视的原因是未正确配置通知通道。自Android 8.0 (API 26) Oreo 起,所有通知都必须分配到一个通知通道。如果没有为通知指定通道,或者通道未被创建,通知将不会显示。

1. 创建通知通道

您应该在应用程序启动时,或者在首次尝试发送通知之前,创建通知通道。通常,在 Application 类的 onCreate() 方法中或 FirebaseMessagingService 的初始化方法中完成。

Revid AI
Revid AI

AI短视频生成平台

Revid AI 96
查看详情 Revid AI
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;

public class NotificationHelper {

    public static final String FCM_CHANNEL_ID = "fcm_default_channel";
    public static final String FCM_CHANNEL_NAME = "FCM Notifications";
    public static final String FCM_CHANNEL_DESCRIPTION = "General notifications from FCM";

    public static void createNotificationChannel(Context context) {
        // 仅在 Android 8.0 (API 26) 及更高版本上创建通知通道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    FCM_CHANNEL_ID,
                    FCM_CHANNEL_NAME,
                    NotificationManager.IMPORTANCE_DEFAULT // 您可以根据需要选择不同的重要性级别
            );
            channel.setDescription(FCM_CHANNEL_DESCRIPTION);

            // 注册通道到系统
            NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
            }
        }
    }
}
登录后复制

您可以在 FirebaseMessagingService 的 onCreate() 方法中调用 NotificationHelper.createNotificationChannel(this);。

2. 在构建通知时指定通道ID

在构建 NotificationCompat.Builder 时,务必通过构造函数或 setChannelId() 方法指定通道ID。

整合客户端代码:FirebaseMessagingService

现在,我们将结合权限请求和通知通道管理,优化 FirebaseMessagingService 中的通知显示逻辑。

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "MyFCMService";
    private static final int NOTIFICATION_ID = 1234; // 唯一的通知ID

    // 在此处定义您的通知通道ID
    private static final String CHANNEL_ID = NotificationHelper.FCM_CHANNEL_ID;

    @Override
    public void onCreate() {
        super.onCreate();
        // 在服务创建时确保通知通道已创建
        NotificationHelper.createNotificationChannel(this);
    }

    @Override
    public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        // 检查消息是否包含数据负载
        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());

            String notificationTitle = remoteMessage.getData().get("title");
            String notificationBody = remoteMessage.getData().get("body");
            String clickAction = remoteMessage.getData().get("click_action"); // 可能是URL或Activity action

            Log.d(TAG, "Notification Data: Title=" + notificationTitle + ", Body=" + notificationBody + ", ClickAction=" + clickAction);

            // 确保标题和内容不为空,然后发送本地通知
            if (notificationTitle != null && notificationBody != null) {
                sendLocalNotification(notificationTitle, notificationBody, clickAction);
            }
        }

        // 检查消息是否包含通知负载 (通常用于FCM自动处理的通知消息)
        // 对于数据消息,remoteMessage.getNotification() 通常为 null
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
            // 如果同时发送了通知和数据负载,这里也可以处理通知部分
        }
    }

    private void sendLocalNotification(String notificationTitle, String notificationBody, String clickAction) {
        Intent intent;
        PendingIntent pendingIntent;

        if (clickAction != null && !clickAction.isEmpty()) {
            // 如果提供了click_action,尝试解析为URL
            try {
                Uri uri = Uri.parse(clickAction);
                intent = new Intent(Intent.ACTION_VIEW, uri);
            } catch (Exception e) {
                // 如果click_action不是有效的URL,则回退到打开应用
                Log.e(TAG, "Invalid URL in click_action: " + clickAction + ", opening app instead.", e);
                intent = new Intent(this, YourMainActivity.class); // 替换为您的主Activity
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            }
        } else {
            // 没有click_action,默认打开应用
            intent = new Intent(this, YourMainActivity.class); // 替换为您的主Activity
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        }

        // PendingIntent.FLAG_IMMUTABLE 是 Android S (API 31) 引入的要求
        int flags = PendingIntent.FLAG_ONE_SHOT;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // For API 23 and above
            flags |= PendingIntent.FLAG_IMMUTABLE;
        }

        pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) // 指定通道ID
                .setSmallIcon(R.mipmap.ic_launcher) // 替换为您的应用图标
                .setContentTitle(notificationTitle)
                .setContentText(notificationBody)
                .setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT); // 设置优先级

        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        if (notificationManager != null) {
            notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
        }
    }

    // 您可能还需要覆盖 onNewToken() 来处理设备令牌刷新
    @Override
    public void onNewToken(@NonNull String token) {
        super.onNewToken(token);
        Log.d(TAG, "Refreshed token: " + token);
        // 将令牌发送到您的应用服务器
    }
}
登录后复制

重要提示:

  • 将 YourMainActivity.class 替换为您应用的主 Activity。
  • 将 R.mipmap.ic_launcher 替换为您应用实际的通知小图标。
  • PendingIntent.FLAG_IMMUTABLE 是 Android 12 (API 31) 及更高版本的要求,确保 PendingIntent 的不变性。

服务器端数据负载示例

确保您的服务器发送的是 data 类型的消息,结构如下:

<?php
// 假设您已经设置了FCM服务器密钥和设备令牌
$serverKey = "YOUR_FCM_SERVER_KEY"; // 替换为您的FCM服务器密钥
$deviceToken = $_POST["deviceToken"]; // 从客户端获取的设备令牌

$url = 'https://fcm.googleapis.com/fcm/send';

$notifData = [
    'title' => "Hey, " . $_POST["employee"],
    'body' => $body, // 您的消息内容
    'click_action' => "URL HERE" // 可选,可以是URL或自定义Activity Action
];

$apiBody = [
    'data' => $notifData,
    'time_to_live' => 2000, // 消息存活时间(秒)
    'to' => $deviceToken
];

$headers = [
    'Authorization: key=' . $serverKey,
    'Content-Type: application/json'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($apiBody));

$response = curl_exec($ch);
if ($response === FALSE) {
    die('Curl failed: ' . curl_error($ch));
}
curl_close($ch);

echo $response;
?>
登录后复制

注意事项与最佳实践

  • 日志调试: 始终使用 Logcat 监控应用的日志输出。onMessageReceived 中的 Log.d 语句是验证消息是否到达客户端的关键。
  • 测试不同Android版本: 务必在不同版本的Android设备上测试您的通知功能,特别是Android 8.0+ 和 Android 13+。
  • 用户体验: 在请求通知权限时,考虑提供清晰的解释,说明为什么您的应用需要发送通知,以提高用户授权的可能性。
  • 通知优先级: NotificationCompat.Builder.setPriority() 可以帮助系统决定如何呈现您的通知。对于重要通知,可以使用 PRIORITY_HIGH 或 PRIORITY_MAX。
  • 通知小图标: setSmallIcon() 是必需的,且图标应遵循Android设计指南(纯白色,透明背景)。
  • 错误处理: 在解析 click_action 为 URL 时,添加 try-catch 块以处理无效 URL,确保应用不会崩溃并能提供回退行为(例如打开主 Activity)。

总结

FCM数据消息不显示通知的问题,通常归结为Android系统版本带来的新要求。对于Android 13 (API 33) 及以上设备,核心在于请求 POST_NOTIFICATIONS 运行时权限;而对于Android 8.0 (API 26) 及以上设备,则必须正确创建和使用通知通道。通过遵循本教程中提供的步骤和代码示例,您可以确保您的FCM数据消息能够稳定、可靠地以系统通知的形式呈现在用户面前,从而提供更好的用户体验。

以上就是解决Android 13+ FCM数据消息不显示通知的问题:权限与通道管理的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号