足球世界杯视频

消息推送——FCM集成与测试

在开发应用中,往往避免不了需要使用消息推送的功能,本文将具体介绍Google Firebase Messaging在安卓端的集成与使用。

1. FCM的集成

集成FCM的步骤如下:

(1)使用Google账号登录Firebase,并注册App,注册成功后,需要下载对应的配置文件google-services.json,并放到项目的app目录下;

(2)在项目根目录下的builde.gradle文件中,确保添加如下内容:

buildscript {

repositories {

google()

}

dependencies {

classpath 'com.google.gms:google-services:4.3.3'

}

}

allprojects {

repositories {

google()

}

}

(3)在app/build.gradle中添加Firebase插件依赖:

implementation 'com.google.firebase:firebase-core:17.4.2'

implementation 'com.google.firebase:firebase-messaging:20.2.0'

并在文件最下方添加,然后sync project:

apply plugin: 'com.google.gms.google-services'

(4)接下来就需要编写代码了,使用FCM推送,我们首先需要自定义类并继承自FirebaseMessagingService服务类,同时重写对应的方法。获取更新的fcm token以及接收消息,显示通知,持久化数据等操作都是在这个类中完成的, 例如:

public class NotifyService extends FirebaseMessagingService {

private final String TAG = "NotifyService";

public final static String GATEWAY_LOG_PREFS = "GATEWAY_LOG_PREFS"; // 保存网关日志的XML文件名

public final static String OTHER_LOG = "OTHER_LOG"; // 其他需要在消息中心显示的日志

private int requestCode = 0;

@Override

public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {

super.onMessageReceived(remoteMessage);

// 接收到数据,执行如下操作:

// 1. 解析并持久化消息;

// 2. 创建并显示通知;

if (remoteMessage.getData().size() > 0) {

String userId = getCurrentUserId();

Log.d(TAG, "userId is " + userId);

if (!userId.equals("")) {

Map data = remoteMessage.getData();

String msgType = data.get("msg_type");

String body = data.get("body");

Log.d(TAG, "msgType is " + msgType + " body is " + body);

if (msgType != null) {

if (msgType.equals("PowerOff")) {

try {

long currentTime = System.currentTimeMillis();

JSONObject jsonObject = new JSONObject();

jsonObject.putOpt("msgType", msgType);

jsonObject.putOpt("body", body);

String GATEWAY_LOG_PREFS_WITH_USER_ID = userId + "_" + GATEWAY_LOG_PREFS; // user id + 特定名称 构造存储消息文件名

saveNotifyMsgToPrefs(GATEWAY_LOG_PREFS_WITH_USER_ID, String.valueOf(currentTime), jsonObject.toString());

} catch (JSONException e) {

e.printStackTrace();

}

} else {

try {

long currentTime = System.currentTimeMillis();

JSONObject jsonObject = new JSONObject();

jsonObject.putOpt("msgType", msgType);

jsonObject.putOpt("body", body);

String OTHER_LOG_PREFS_WITH_USER_ID = userId + "_" + OTHER_LOG; // user id + 特定名称 构造存储消息文件名

saveNotifyMsgToPrefs(OTHER_LOG_PREFS_WITH_USER_ID, String.valueOf(currentTime), jsonObject.toString());

} catch (JSONException e) {

e.printStackTrace();

}

}

}

}

}

if (remoteMessage.getNotification() != null) {

String notifyTitle = remoteMessage.getNotification().getTitle();

String notifyBody = remoteMessage.getNotification().getBody();

Log.d(TAG, "notification title is " + notifyTitle);

Log.d(TAG, "notification body is " + notifyBody);

sendNotification(notifyTitle, notifyBody);

}

}

@Override

public void onNewToken(@NonNull String refreshToken) {

super.onNewToken(refreshToken);

Log.d(TAG, "refreshed token: " + refreshToken);

// 1. 持久化生成的token,

// 2. 发送事件通知RN层,分为两种情况:

// 用户未登录,RN层不做处理(待用户登录后读取本地存储的token,并上报)

// 用户已登录,RN层获取当前用户id、token及当前语言上报服务端

SharedPreferences.Editor editor = getSharedPreferences("fcmToken", MODE_PRIVATE).edit();

editor.putString("token", refreshToken);

editor.apply();

sendRefreshTokenBroadcast(refreshToken);

}

/**

* 获取当前已登录用户的id

* @return 用户id,如果未登录则为空

*/

private String getCurrentUserId() {

SharedPreferences prefs = getSharedPreferences("userMsg", MODE_PRIVATE);

return prefs.getString("userId", "");

}

/**

* 发送通知

* @param contentTitle 通知标题

* @param contentText 通知内容

*/

private void sendNotification(String contentTitle, String contentText) {

requestCode++;

String channel_id = getString(R.string.default_notify_channel_id);

String channel_name = getString(R.string.default_notify_channel_name);

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

Intent intent = new Intent(this, MainActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(this, requestCode, intent, PendingIntent.FLAG_ONE_SHOT);

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channel_id);

notificationBuilder.setContentTitle(contentTitle)

.setContentText(contentText)

.setSmallIcon(R.mipmap.ic_launcher)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))

.setAutoCancel(true)

.setSound(defaultNotifySound)

.setContentIntent(pendingIntent);

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

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {

NotificationChannel notificationChannel = new NotificationChannel(channel_id, channel_name, NotificationManager.IMPORTANCE_DEFAULT);

notificationManager.createNotificationChannel(notificationChannel);

}

notificationManager.notify(requestCode, notificationBuilder.build());

}

/**

* 发送广播通知NotificationModule更新token,并发送给RN层

* @param refreshToken 更新的token

*/

private void sendRefreshTokenBroadcast(String refreshToken) {

LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this);

Intent intent = new Intent(getString(R.string.REFRESH_TOKEN_BROADCAST_ACTION));

intent.putExtra("refreshToken", refreshToken);

localBroadcastManager.sendBroadcast(intent);

}

/**

* 持久化通知消息

* @param prefsName 文件名

* @param key 键

* @param value 值

*/

private void saveNotifyMsgToPrefs(String prefsName, String key, String value) {

SharedPreferences.Editor editor = getSharedPreferences(prefsName, MODE_PRIVATE).edit();

editor.putString(key, value);

editor.apply();

}

}

服务端推送过来的消息可以分为两个部分,一个是notification对象,另一个是data对象,例如:

var message = {

notification: {

title: "Alert",

body: "description for message poweroff from DT"

},

data: {

msg_type: "PowerOff",

body: "description for message poweroff from DT"

},

token: registrationToken

};

notification对象的title和body分别是我们需要在通知栏显示的内容。data对象中的键值对我们可以自行定义,这部分数据往往需要我们在接收到通知后持久化在本地,供用户在APP内随时查看。

在实现的父类方法onMessageReceived方法中,通过RemoteMessage remoteMessage,我们可以获取到以上内容。

获取通知栏显示文本:

String notifyTitle = remoteMessage.getNotification().getTitle();

String notifyBody = reomteMessage.getNotification().getBody();

注:在显示通知时一定要注意递增requestCode,如果requestCode保持不变,即使收到多条消息也仅会有一个通知。

获取待持久化消息数据:

Map data = remoteMessage.getData();

String msgType = data.get("msg_type");

String body = data.get("body");

然后就是onNewToken回调,这个方法会在首次获取refreshToken以及更新refreshToken时执行。这个token是服务端推送指定设备的依据,所以在这个回调中,我们需要把refreshToken持久化到本地。

提到refreshToken,就不得不说到FCM中推送特定设备的方式了,FCM中推送设备支持两种方式,一种是通过refreshToken,另一种则是通过特定的主题(可以给待推送设备进行分组)。服务端既可以通过指定单个token推送到特定的设备,也可以一次性指定多个token,进行批量推送。如果客户端针对某类用户订阅了特定的主题,服务端也可以通过这个主题作为用户群体的标识进行批量推送。

在实际开发中不仅仅是refreshToken需要上报服务端,可能还需要包含其他信息,例如:登录用户的id,当前设备的语言环境(服务端需要根据不同的语言推送特定语言的notification)等。这里根据需求(登录、退出登录、多语言切换、token更新)可能会包含如下场景:

1、登录:在应用首次启动时,如果网络正常(可以翻墙,能够访问google服务器),onRefreshToken会立刻执行,返回当前设备分配的token值,此时我们需要将其保存在本地。之后,如果用户执行登录操作,在登录成功后,需要将token、userid、language(optional)一并上报给服务端,这样服务端可以将推送设备和用户进行关联(服务端会使用userid+token作为联合主键)。

2. 退出登录:当用户退出登录时,为了避免用户在未登录状态下也收到相应的消息,我们需要将设备和用户进行“解绑”。可以在退出登录前上报一个空的token值。

3. 多语言切换:如果应用内支持多语言切换的功能,需要在用户切换时,重新上报当前设备,当前用户的语言环境,以便服务端对推送消息的语言进行同步切换。

4. token更新:当token更新时,我们同样需要重新上报更新后的token,与当前登录用户重新构建绑定关系。在FCM的官方文档中,在以下情景中会更新token值:

a. 应用删除实例id;

b. 应用在新设备上恢复;

c. 用户卸载/重新安装应用;

d. 用户清除应用数据;

主题订阅:

我们可以在客户端代码中为特定的用户群订阅特定的主题,例如订阅一个weather主题:

FirebaseMessaging.getInstance().subscribeToTopic("weather")

.addOnCompleteListener(new OnCompleteListener() {

@Override

public void onComplete(@NonNull Task task) {

String msg = getString(R.string.msg_subscribed);

if (!task.isSuccessful()) {

msg = getString(R.string.msg_subscribe_failed);

}

Log.d(TAG, msg);

Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();

}

});

服务端也可以为部分token订阅主题,以node为例:

// These registration tokens come from the client FCM SDKs.

var registrationTokens = [

'YOUR_REGISTRATION_TOKEN_1',

// ...

'YOUR_REGISTRATION_TOKEN_n'

];

// Subscribe the devices corresponding to the registration tokens to the

// topic.

admin.messaging().subscribeToTopic(registrationTokens, topic)

.then(function(response) {

// See the MessagingTopicManagementResponse reference documentation

// for the contents of response.

console.log('Successfully subscribed to topic:', response);

})

.catch(function(error) {

console.log('Error subscribing to topic:', error);

});

还可以通过unsubscribeFromTopic退订主题,具体的可以官网查看。

回到我们的NotifyService类中,接收消息的逻辑编写完成后,需要在AndroidManifest.xml中注册这个服务:

android:name=".notification.NotifyService"

android:exported="false">

这样,客户端的集成工作基本就完成了。

2. FCM服务端推送

在编写完客户端代码后,我们可能跃跃欲试,希望能够看看整个推送流程能否跑通。但是实际推送需要服务端来完成,每推送一次可能就需要麻烦一下后端研发,这样会导致调试的效率降低。所幸,FCM提供了支持不同服务端语言版本的插件,我们可以在本地构建推送服务,在与后端约定好推送消息的格式后,本地模拟推送。然后在代码自测基本没问题后,再与服务端进行线上的联调。这里以node为例:

在本地新建一个node项目,安装firebase-admin-node插件:

npm init

npm install --save firebase-admin

然后需要回到Firebase控制台,下载对应的服务端配置文件:

接着创建index.js,编写消息推送脚本:

var admin = require("firebase-admin");

var serviceAccount = require("./multirouter-xxxxx-firebase-adminsdk-g8jqq-a4cf04a792.json");

admin.initializeApp({

credential: admin.credential.cert(serviceAccount)

});

var registrationToken = 'dnV7QVGUQjOgvPk1ZCKflA:APA91bGIn09-QyDoGObbjmZLjlbhP5P4tonw9wnuAP6iKcScWZWmpWReVL8476IzEyVAsvrb0r9z0s-a_Xzlme7RlHUns0Vo0EA_6apZI2jJSDvQ7HUGnODKcYJE54MXpqY_A1joWdyt';

var message = {

notification: {

title: "Alert test background",

body: "description for message poweroff from DT"

},

data: {

msg_type: "PowerOff",

body: "description for message poweroff from DT background"

},

token: registrationToken

};

admin.messaging().send(message)

.then((response) => {

console.log('Successfully sent message:', response);

})

.catch((error) => {

console.log('Error sending message:', error);

});

我们只需要将测试机上App生成的token,拷贝到脚本中,运行以上脚本就会立即发送消息至App上。还是非常简单的。

3. 实际使用中的问题与解决方法:

在实际使用FCM的过程中,发现存在如下问题(现象),测试手机是Redmi K20Pro,系统是MIUI11:

(1)使用国内运营商的网络很可能无法获取到FCM token(国内需要翻墙);

(2)App如果处于未运行状态,无法收到推送的通知,但是在app启动后,大概等待几分钟,可以收到;

(3)App处于后台运行状态时,能够接收到通知并显示notification字段的内容,但是onMessageReceived回调未执行,这会导致消息因无法持久化而丢失;

(4)App处于后台运行状态时,如果用户手动清空通知栏的通知,没有点击触发pendingIntent,这会导致在启动的 Activity 里通过getIntent().getExtras()获取的bundle中不会包含接收到的消息;

针对问题(1),考虑到国内的网络环境,实际上是挺无解的。所以之前在做APP的过程中,国内的推送服务都是采用的极光推送(还是非常好用的)。而这次开发的APP需要上架google应用市场,并在俄罗斯使用,才选用的google的FCM作为推送方案。

针对问题(2),这个应该是正常的现象。毕竟App未启动状态下,代码是无法执行的。至于像微信这些大厂的应用,能够随时收到消息可能是和系统厂商有合作吧(瞎猜的 ̄□ ̄)。

针对问题(3),查了一下stackoverflow,应该也属于正常的情况,而且data数据也并没有丢失,如果用户点击通知栏,唤醒应用,我们可以在拉起的Activity的onResume生命周期方法中,通过getIntent().getExtras()获取的bundle对象中拿到。例如:

@Override

protected void onResume() {

super.onResume();

Log.d(TAG, "onResume");

Bundle bundle = getIntent().getExtras();

if (bundle != null) {

String msgType = bundle.getString("msg_type");

String body = bundle.getString("body");

Log.d(TAG, "onResume msgType is " + msgType + " body is " + body);

}

}

不过还有一个小问题,就是如果Activity没有被回收,bundle对象中保存的data数据会一直存在,这可能在某些情况下导致写入重复的消息内容,所以建议在持久化数据完成后,清空bundle中的这部分数据。

getIntent().removeExtra("msg_type");

getIntent().removeExtra("body");

针对问题(4),这个还没有找到比较好的解决办法,这种情况会导致应用内展示历史消息的消息中心丢失部分消息(消息仅持久化在APP本地为前提)。

以上就是本文的全部内容了,也是我在实际使用FCM的个人总结,如有错误,欢迎大家指正哈。FCM这部分内容官方文档还是非常详细的,而且也提供了对应的demo,大家还是优先阅读官方文档吧。