Firebase Cloud Messaging - Handling logout

2019-01-01 15:46发布

问题:

How do I handle situation, when user logs out of my application and I no longer want him to receive notifications to the device.

I tried

FirebaseInstanceId.getInstance().deleteToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

But I still receive the notifications to my device\'s registration_id.

I also made sure that this is the token I should delete:

FirebaseInstanceId.getInstance().getToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

or simply FirebaseInstanceId.getInstance().getToken()).

I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).

I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack. And also there\'s this answer which states that it shouldn\'t be done, but it proposes deleting the token which apparently doesn\'t work.

So what is the right method to handle this?

回答1:

Okay. So I managed to do some testing and have concluded the following:

  1. deleteToken() is the counterpart of getToken(String, String), but not for getToken().

It only works if the Sender ID you are passing is a different Sender ID (not the same ID that can be seen in your google-services.json). For example, you want to allow a different Server to send to your app, you call getToken(\"THEIR_SENDER_ID\", \"FCM\") to give them authorization to send to your app. This will return a different registration token that corresponds only to that specific sender.

In the future, if you chose to remove their authorization to send to your app, you\'ll then have to make use of deleteToken(\"THEIR_SENDER_ID\", \"FCM\"). This will invalidate the corresponding token, and when the Sender attempts to send a message, as the intended behavior, they will receive a NotRegistered error.

  1. In order to delete the token for your own Sender, the correct handling is to use deleteInstanceId().

Special mentioning this answer by @Prince, specifically the code sample for helping me with this.

As @MichałK already doing in his post, after calling the deleteInstanceId(), getToken() should be called in order to send a request for a new token. However, you don\'t have to call it the second time. So long as onTokenRefresh() onNewToken() is implemented, it should automatically trigger providing you the new token.

For short, deleteInstanceId() > getToken() > check onTokenRefresh() onNewToken().

Note: Calling deleteInstanceId() will not only delete the token for your own app. It will delete all topic subscriptions and all other tokens associated with the app instance.


Are you positive you\'re calling deleteToken() properly? The value for audience should be (also seen from my answer that you linked) is \"set to the app server\'s sender ID\". You\'re passing the getId() value which is not the same as the Sender ID (it contains the app instance id value). Also, how are you sending the message (App Server or Notifications Console)?

getToken() and getToken(String, String) returns different tokens. See my answer here.

I also tried FirebaseInstanceId.getInstance().deleteInstanceId(), but then the next time I call FirebaseInstanceId.getInstance.getToken I receive null (it works on the second try).

It\'s probably because the first time you\'re calling the getToken(), it\'s still being generated. It\'s just the intended behavior.

I guess, after deleteInstanceId I could immediately call getToken() again, but it looks like a hack.

Not really. It\'s how you\'ll get the new generated (provided that it is already generated) token. So I think it\'s fine.



回答2:

I was working on the same problem, when I had done my logout() from my application. But the problem was that after logging out, I was still getting push notifications from Firebase. I tried to delete the Firebase token. But after deleting the token in my logout() method, it is null when I query for it in my login() method. After working 2 days I finally got a solution.

  1. In your logout() method, delete the Firebase token in the background because you can not delete Firebase token from the main thread

    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. In your login() method, generate the Firebase token again.

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


回答3:

I did a brief research on what would be the most elegant solution to get back the full control (subscribe and unsubscribe to FCM) as before. Enable and disable the FCM after the user logged in or out.

Step 1. - Prevent auto initialization

Firebase now handle the InstanceID and everything else which need to generate a registration token. First of all you need to prevent auto initialization. Based on the official set-up documentation you need to add these meta-data values to your 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>

Now you disabled the automatic token request process. At the same time you have an option to enable it again at runtime by code.

Step 2. - Implement enableFCM() and disableFCM() functions

If you enable the auto initialization again then you received a new token immediately, so this is a perfect way to implement the enableFCM() method. All subscribe information assigned to InstanceID, so when you delete it then initiate to unsubscribe all topic. On this way you able to implement disableFCM() method, just turn back off auto-init before you delete it.

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

}

Step 3. - FCMService implementation - token and message receiving

In the last step you need to receive the new token and send direct to your server. Other hand you\'ll receive your data message and just do it what you want.

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

I think this solution is clear, simple and transparent. I tested in a production environment and it\'s works. I hope it was helpful.



回答4:

I know I am late for the party. deleteInstanceId() should be called from the background thread since it\'s a blocking call. Just check the method deleteInstanceId() in FirebaseInstanceId() class.

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

You can start an IntentService to delete the instance id and the data associated with it.



回答5:

Since the getToken() is deprecated, use getInstanceId() to regenerate new token instead. It has same effect.

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