Firebase 云消息传递 - 处理注销
Posted
技术标签:
【中文标题】Firebase 云消息传递 - 处理注销【英文标题】:Firebase Cloud Messaging - Handling logout 【发布时间】:2017-08-28 19:50:04 【问题描述】:当用户退出我的应用程序并且我不再希望他接收到设备的通知时,我该如何处理。
我试过了
FirebaseInstanceId.getInstance().deleteToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)
但我仍然会通过设备的registration_id
收到通知。
我还确定这是我应该删除的令牌:
FirebaseInstanceId.getInstance().getToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)
或者只是FirebaseInstanceId.getInstance().getToken()
)。
我也尝试了FirebaseInstanceId.getInstance().deleteInstanceId()
,但是下次我调用FirebaseInstanceId.getInstance.getToken
时收到 null(它在第二次尝试时有效)。
我想,在deleteInstanceId
之后,我可以立即再次调用getToken()
,但它看起来像个黑客。还有this answer 声明不应该这样做,但它建议删除显然不起作用的令牌。
那么处理这个问题的正确方法是什么?
【问题讨论】:
在深入实施这些解决方案之一之前,请务必查看底部的 Dan Alboteanu 答案; TL;DR 大部分应该在服务器端处理,而不是客户端。 【参考方案1】:好的。所以我设法做了一些测试并得出以下结论:
deleteToken()
是 getToken(String, String)
的对应物,但不是 getToken()
。
仅当您传递的发件人 ID 是不同的发件人 ID(与您的 google-services.json 中可以看到的 ID 不同)时才有效。例如,您想允许不同的服务器发送到您的应用程序,您调用 getToken("THEIR_SENDER_ID", "FCM")
以授予他们授权发送到您的应用程序。这将返回一个不同的注册令牌,该令牌仅对应于该特定发件人。
以后,如果您选择删除他们的授权以发送到您的应用,那么您将不得不使用deleteToken("THEIR_SENDER_ID", "FCM")
。这将使相应的令牌失效,并且当 Sender 尝试发送消息时,按照预期的行为,他们将收到 NotRegistered
错误。
-
为了删除自己Sender的token,正确的处理方式是使用
deleteInstanceId()
。
特别提到这个answer by @Prince,特别是帮助我解决这个问题的代码示例。
正如@MichałK 在他的帖子中所做的那样,在调用deleteInstanceId()
之后,应该调用getToken()
以发送对新令牌的请求。但是,您不必第二次调用它。只要实现了 onTokenRefresh()
onNewToken()
,它应该会自动触发为您提供新令牌。
简称deleteInstanceId()
> getToken()
> 勾选onTokenRefresh()
onNewToken()
.
注意:调用deleteInstanceId()
不仅会删除您自己应用的令牌。它将删除与应用实例关联的所有主题订阅和所有其他令牌。
你确定你打电话给deleteToken()
正确吗? Audience 的值应该是(也可以从您链接的我的答案中看到)是“设置为应用服务器的发件人 ID”。您传递的 getId()
值与发件人 ID 不同(它包含应用程序实例 ID 值)。另外,您如何发送消息(应用服务器或通知控制台)?
getToken()
和 getToken(String, String)
返回不同的令牌。见我的回答here。
我也尝试了
FirebaseInstanceId.getInstance().deleteInstanceId()
,但是下次我调用FirebaseInstanceId.getInstance.getToken
时收到 null(它在第二次尝试时有效)。
这可能是因为您第一次调用getToken()
时,它仍在生成中。这只是预期的行为。
我想,在
deleteInstanceId
之后我可以立即再次调用getToken()
,但它看起来像个黑客。
不是真的。这是您获取新生成的(假设它已经生成)令牌的方式。所以我觉得还可以。
【讨论】:
这是唯一一个在我调用 getToken 或 deleteToken 时没有抛出错误的“发件人 ID”。当我使用我的 firebase 控制台中的文本项目 ID 时,两种方法都抛出了。然后我使用了在 googleservices.json 中找到的数字 id,它看起来很有效。然后通过 getId() 并且它也没有抛出。所以我想就是这样。 至于 hack,我必须在 deleteInstanceId 之后立即调用它,所以它第一次返回 null,然后在登录时调用它才能工作。这就是为什么我认为这是一个 hack。 我会尝试看看我是否可以稍后进行一些测试并复制该行为。如果我有时间会回到这里。干杯! 感谢您的检查!老实说,我很惊讶这个 API 有多么糟糕和记录不充分……不过,我会试一试,谢谢! 有没有办法在离线时停止监听 FCM,因为在这种情况下 deleteInstanceId 会返回一个 SERVICE_NOT_AVAILABLE_ERROR,而你仍然会被注册【参考方案2】:我做了一个简短的研究,研究什么是最优雅的解决方案,可以像以前一样重新获得完全控制(订阅和取消订阅 FCM)。在用户登录或注销后启用和禁用 FCM。
第 1 步 - 防止自动初始化
Firebase 现在可以处理 InstanceID
以及需要生成注册令牌的所有其他内容。首先你需要防止自动初始化。根据official set-up documentation,您需要将这些元数据值添加到您的androidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<application>
<!-- FCM: Disable auto-init -->
<meta-data android:name="firebase_messaging_auto_init_enabled"
android:value="false" />
<meta-data android:name="firebase_analytics_collection_enabled"
android:value="false" />
<!-- FCM: Receive token and messages -->
<service android:name=".FCMService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
</application>
现在您禁用了自动令牌请求过程。同时,您可以选择在运行时通过代码再次启用它。
第 2 步 - 实现 enableFCM()
和 disableFCM()
函数
如果您再次启用自动初始化,那么您会立即收到一个新令牌,因此这是实现enableFCM()
方法的完美方式。
所有订阅信息都分配给 InstanceID,因此当您删除它时,将启动取消订阅所有主题。这样你就可以实现disableFCM()
方法,在你删除它之前关闭自动初始化。
public class FCMHandler
public void enableFCM()
// Enable FCM via enable Auto-init service which generate new token and receive in FCMService
FirebaseMessaging.getInstance().setAutoInitEnabled(true);
public void disableFCM()
// Disable auto init
FirebaseMessaging.getInstance().setAutoInitEnabled(false);
new Thread(() ->
try
// Remove InstanceID initiate to unsubscribe all topic
// TODO: May be a better way to use FirebaseMessaging.getInstance().unsubscribeFromTopic()
FirebaseInstanceId.getInstance().deleteInstanceId();
catch (IOException e)
e.printStackTrace();
).start();
第 3 步 - FCMService
实现 - 令牌和消息接收
在最后一步中,您需要接收新令牌并直接发送到您的服务器。 另一方面,您将收到您的数据消息,然后按照您的意愿去做。
public class FCMService extends FirebaseMessagingService
@Override
public void onNewToken(String token)
super.onNewToken(token);
// TODO: send your new token to the server
@Override
public void onMessageReceived(RemoteMessage remoteMessage)
super.onMessageReceived(remoteMessage);
String from = remoteMessage.getFrom();
Map data = remoteMessage.getData();
if (data != null)
// TODO: handle your message and data
sendMessageNotification(message, messageId);
private void sendMessageNotification(String msg, long messageId)
// TODO: show notification using NotificationCompat
我认为这个解决方案是清晰、简单和透明的。我在生产环境中进行了测试,并且可以正常工作。希望对您有所帮助。
【讨论】:
你好 Janos,如果我没有通过调用“FirebaseMessaging.getInstance().setAutoInitEnabled(true);”来启用自动初始化有什么影响?应用是否会收到“onNewToken”回调? 哦..对不起我迟到的答案。是的,再次启用后,您通过调用 onNewToken() 在 FCMService 中获得一个新的令牌。 我认为这是一个很好的实现。还要考虑清理不再有效的令牌(在服务器端) - 当它收到 - error.code === 'messaging/invalid-registration-token' || error.code === '消息/注册-令牌-未注册'。检查此代码实验室示例github.com/firebase/friendlychat-web/blob/master/… 所以观察身份验证状态和 if(user == null) disableFCM() ...FirebaseInstanceId
已被弃用。 FirebaseMessaging.deleteToken
任务应该用于移除令牌。【参考方案3】:
当我从我的应用程序中完成我的logout()
时,我正在解决同样的问题。但问题是退出后,我仍然收到来自 Firebase 的推送通知。我尝试删除 Firebase 令牌。但是在我的logout()
方法中删除令牌后,当我在我的login()
方法中查询它时,它是null
。经过2天的工作,我终于得到了解决方案。
在您的logout()
方法中,在后台删除 Firebase 令牌,因为您无法从主线程中删除 Firebase 令牌
new AsyncTask<Void,Void,Void>()
@Override
protected Void doInBackground(Void... params)
try
FirebaseInstanceId.getInstance().deleteInstanceId();
catch (IOException e)
e.printStackTrace();
return null;
@Override
protected void onPostExecute(Void result)
// Call your Activity where you want to land after log out
.execute();
在您的 login()
方法中,再次生成 Firebase 令牌。
new AsyncTask<Void,Void,Void>()
@Override
protected Void doInBackground(Void... params)
String token = FirebaseInstanceId.getInstance().getToken();
// Used to get firebase token until its null so it will save you from null pointer exeption
while(token == null)
token = FirebaseInstanceId.getInstance().getToken();
return null;
@Override
protected void onPostExecute(Void result)
.execute();
【讨论】:
while(token == null) busy loop 对你的电池来说真的很糟糕......最好是注册一个回调。另一个(不好但更好)是在等待令牌时执行 Thread.sleep。 @AmirUval,感谢您的建议,我一定会尝试并让您知道【参考方案4】:开发人员绝不应将客户端应用程序取消注册为一种机制 注销或切换用户,原因如下:
注册令牌未与特定登录用户关联。如果客户端应用程序取消注册然后重新注册,该应用程序可以 接收相同的注册令牌或不同的注册令牌。取消注册和重新注册可能需要最多五分钟才能传播。在此期间,邮件可能会被拒绝,因为 未注册状态,消息可能会发送给错误的用户。做 确保消息发送给目标用户:
应用服务器可以维护当前用户和注册令牌之间的映射。
然后,客户端应用程序可以检查以确保它收到的消息与登录用户匹配。
此引用来自已弃用的谷歌文档
但有理由相信这仍然是正确的 - 即使上面的文档已被弃用。
您可以在此处观察 - 在此代码实验室 https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js 中查看他们是如何做到的
这里 https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js
【讨论】:
您链接并引用了已弃用的 GCM 文档。我找不到 FCM 的类似信息。 我有理由相信这也适用于 FCM。看看他们在这个 codelab codelabs.developers.google.com/codelabs/… 中是如何做到的。 Codelab 已更新和维护。他们从不 deleteInstanceId()。相反,他们使用 authStateObserver(user) github.com/firebase/friendlychat-web/blob/master/… 中的令牌 - userID(uid) 对更新服务器(Firestore),同时清理服务器上不再有效的令牌 @DanAlboteanu 但您链接的代码示例未显示用户注销时会发生什么。根据我所见,服务器没有更新... 链接已失效【参考方案5】:由于getToken()
已弃用,请改用getInstanceId()
重新生成新令牌。效果一样。
public static void resetInstanceId()
new Thread(new Runnable()
@Override
public void run()
try
FirebaseInstanceId.getInstance().deleteInstanceId();
FirebaseInstanceId.getInstance().getInstanceId();
Helper.log(TAG, "InstanceId removed and regenerated.");
catch (IOException e)
e.printStackTrace();
).start();
【讨论】:
【参考方案6】:另一种清除 firebase 令牌并使用 FirebaseMessaging.getInstance()
重新生成新令牌的便捷方法fun clearFirebaseToken()
FirebaseMessaging.getInstance().apply
deleteToken().addOnCompleteListener it ->
Log.d("TAG++", "firebase token deleted $it.result")
token.addOnCompleteListener
Log.d("TAG++", "firebase token generated $it.result")
if (it.result != null) saveTokenGenerated(it.result!!)
【讨论】:
【参考方案7】:只需在注销时在后台线程上调用 deleteToken 方法:
https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#public-void-deletetoken-string-senderid,-string-scope
FirebaseInstanceId.getInstance().deleteToken(getString(R.string.gcm_defaultSenderId), "FCM")
第一个参数采用 FireBaseConsole 中定义的 SenderID
更新需要几秒钟的时间 - 之后,您将不再收到 FCM 通知。
【讨论】:
【参考方案8】:使用这种方法。 这是我的解决方案,我在here 提到了这个 注册时,使用 initFirebaseMessage,。以及注销或删除时 使用 removeFirebaseMessage()。
private fun removeFirebaseMessage()
CoroutineScope(Dispatchers.Default).launch
FirebaseMessaging.getInstance().isAutoInitEnabled = false
FirebaseInstallations.getInstance().delete()
FirebaseMessaging.getInstance().deleteToken()
private fun initFirebaseMessage()
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = true
fcm.subscribeToTopic("all")
fcm.subscribeToTopic("")
【讨论】:
正在使用更新的 firebase 版本。谢谢【参考方案9】:我知道我参加聚会迟到了。 deleteInstanceId()
应该从后台线程调用,因为它是一个阻塞调用。只需检查FirebaseInstanceId() 类中的方法deleteInstanceId()
。
@WorkerThread
public void deleteInstanceId() throws IOException
if (Looper.getMainLooper() == Looper.myLooper())
throw new IOException("MAIN_THREAD");
else
String var1 = zzh();
this.zza(this.zzal.deleteInstanceId(var1));
this.zzl();
您可以启动一个 IntentService 来删除实例 id 以及与之关联的数据。
【讨论】:
这是真的,但在这里无关紧要(其他答案也将其包装在 asynctask 中,基本相同)【参考方案10】:包含FirebaseInstanceId
的firebase.iid
包是now deprecated。自动初始化已从 Firebase 实例 ID 迁移到 Firebase Cloud Messaging。它的行为也略有改变。以前,如果启用了自动初始化,对deleteInstanceId()
的调用将自动生成一个新令牌。现在,新令牌仅在下一次应用启动或显式调用 getToken()
时生成。
private suspend fun loginFCM() = withContext(Dispatchers.Default)
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = true
fcm.token.await()
private suspend fun logoutFCM() = withContext(Dispatchers.Default)
val fcm = FirebaseMessaging.getInstance()
fcm.isAutoInitEnabled = false // To prevent a new token to be generated automatically in the next app-start (remove if you don't care)
fcm.deleteToken().await()
如果您想完全退出 Firebase,您可以在之后删除整个安装:
private suspend fun logoutFirebase() = withContext(Dispatchers.Default)
logoutFCM()
val firebase = FirebaseInstallations.getInstance()
firebase.delete().await()
【讨论】:
【参考方案11】:最后,使用后台线程删除 instanceID,下次登录时请留意 Firestore/Realtime DB(如果您将令牌保存在那里),它们会刷新
public void Logout()
new Thread()
@Override
public void run()
super.run();
try
FirebaseInstanceId.getInstance().deleteInstanceId();
FirebaseInstanceId.getInstance().getInstanceId();
catch (final IOException e)
runOnUiThread(new Runnable()
@Override
public void run()
Toast.makeText(Flags.this, e.getMessage(), Toast.LENGTH_SHORT).show();
);
.start();
FirebaseMessaging.getInstance().setAutoInitEnabled(false);
FirebaseAuth.getInstance().signOut();
SharedPreferences sharedPreferences = getDefaultSharedPreferences(Flags.this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.apply();
startActivity(new Intent(Flags.this, MainActivity.class));
Flags.this.finish();
【讨论】:
【参考方案12】:我使用了下面的这段代码,它对我有帮助,我使用 Kotlin 协程而不是 Thread(Runnable).start(),因为它比创建新线程对象的成本更低
private fun logoutFromFCM()
GlobalScope.launch(Dispatchers.IO)
FirebaseInstallations.getInstance().delete()
FirebaseMessaging.getInstance().deleteToken()
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener task ->
if (!task.isSuccessful)
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
// Get new FCM registration token
val token = task.result
saveFirebaseToken(token)
Log.w(TAG, "Token Updated - newToken> $token")
)
【讨论】:
【参考方案13】:对于许多通知要求很简单的情况,处理注销的问题可以更容易地实现。例如,在我的例子中,每个用户只订阅了两个主题:
全球alerts
话题
定义为用户电子邮件的用户特定主题(将 @
替换为 -
,因为主题字符串中不允许使用 @
)
对于这种简单的场景,只需在注销时取消订阅不需要的主题:
Future<void> signOut() async
messaging.unsubscribeFromTopic(emailToTopic(_firebaseAuth.currentUser.email));
await _firebaseAuth.signOut();
当然,只有在成功登录或注册后才能订阅主题:
Future<String> signIn(String email, String password) async
try
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
messaging.subscribeToTopic(emailToTopic(email));
return "Signed in";
on FirebaseAuthException catch (e)
return e.message;
【讨论】:
以上是关于Firebase 云消息传递 - 处理注销的主要内容,如果未能解决你的问题,请参考以下文章
如何向 FCM(Firebase 云消息传递)令牌的特定用户发送消息?
如何在 Flutter 中删除 Firebase 云消息传递令牌
用于 Flutter 的 Firebase 云消息传递 - 可选择处理后台消息错误
Flutter - Firebase 云消息传递,iOS 上未收到数据消息