【问题标题】:Firebase Cloud Messaging - Handling logoutFirebase 云消息传递 - 处理注销
【发布时间】: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 大部分应该在服务器端处理,而不是客户端。

标签: android firebase firebase-cloud-messaging


【解决方案1】:

好的。所以我设法做了一些测试并得出以下结论:

  1. deleteToken()getToken(String, String) 的对应物,但不是 getToken()

仅当您传递的发件人 ID 是不同的发件人 ID(与您的 google-services.json 中可以看到的 ID 不同)时才有效。例如,您想允许不同的服务器发送到您的应用程序,您调用 getToken("THEIR_SENDER_ID", "FCM") 以授予他们授权发送到您的应用程序。这将返回一个不同的注册令牌,该令牌仅对应于该特定发件人。

以后,如果您选择删除他们的授权以发送到您的应用,那么您将不得不使用deleteToken("THEIR_SENDER_ID", "FCM")。这将使相应的令牌失效,并且当 Sender 尝试发送消息时,按照预期的行为,他们将收到 NotRegistered 错误。

  1. 为了删除自己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天的工作,我终于得到了解决方案。

  1. 在您的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();
    
  2. 在您的 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】:

        包含FirebaseInstanceIdfirebase.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;
                }
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2017-07-04
                • 1970-01-01
                • 1970-01-01
                • 2018-04-23
                • 2019-07-12
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多