Firebase FCM force onTokenRefresh() to be called
Asked Answered
S

13

127

I am migrating my app from GCM to FCM.

When a new user installs my app, the onTokenRefresh() is automatically being called. The problem is that the user is not logged in yet (No user id).

How can I trigger the onTokenRefresh() after the user is logged-in?

Scrivenor answered 26/5, 2016 at 7:40 Comment(1)
A very similar question has already been asked in the following link. Check if the answer is useful to you: #37517754Hols
S
186

The onTokenRefresh() method is going to be called whenever a new token is generated. Upon app install, it will be generated immediately (as you have found to be the case). It will also be called when the token has changed.

According to the FirebaseCloudMessaging guide:

You can target notifications to a single, specific device. On initial startup of your app, the FCM SDK generates a registration token for the client app instance.

Screenshot

Source Link: https://firebase.google.com/docs/notifications/android/console-device#access_the_registration_token

This means that the token registration is per app. It sounds like you would like to utilize the token after a user is logged in. What I would suggest is that you save the token in the onTokenRefresh() method to internal storage or shared preferences. Then, retrieve the token from storage after a user logs in and register the token with your server as needed.

If you would like to manually force the onTokenRefresh(), you can create an IntentService and delete the token instance. Then, when you call getToken, the onTokenRefresh() method will be called again.

Example Code:

public class DeleteTokenService extends IntentService
{
    public static final String TAG = DeleteTokenService.class.getSimpleName();

    public DeleteTokenService()
    {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent)
    {
        try
        {
            // Check for current token
            String originalToken = getTokenFromPrefs();
            Log.d(TAG, "Token before deletion: " + originalToken);

            // Resets Instance ID and revokes all tokens.
            FirebaseInstanceId.getInstance().deleteInstanceId();

            // Clear current saved token
            saveTokenToPrefs("");

            // Check for success of empty token
            String tokenCheck = getTokenFromPrefs();
            Log.d(TAG, "Token deleted. Proof: " + tokenCheck);

            // Now manually call onTokenRefresh()
            Log.d(TAG, "Getting new token");
            FirebaseInstanceId.getInstance().getToken();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    private void saveTokenToPrefs(String _token)
    {
        // Access Shared Preferences
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = preferences.edit();

        // Save to SharedPreferences
        editor.putString("registration_id", _token);
        editor.apply();
    }

    private String getTokenFromPrefs()
    {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getString("registration_id", null);
    }
}

EDIT

FirebaseInstanceIdService

public class FirebaseInstanceIdService extends Service

This class is deprecated. In favour of overriding onNewToken in FirebaseMessagingService. Once that has been implemented, this service can be safely removed.

onTokenRefresh() is deprecated. Use onNewToken() in MyFirebaseMessagingService

public class MyFirebaseMessagingService extends FirebaseMessagingService {

@Override
public void onNewToken(String s) {
    super.onNewToken(s);
    Log.e("NEW_TOKEN",s);
    }

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    super.onMessageReceived(remoteMessage);
    }
} 
Scholz answered 1/6, 2016 at 22:36 Comment(11)
Ya, this is what i have done yesterday. just saved the token and sent it with registration.Scrivenor
Answer accepted. Even though i did not want any "workarounds", but i guess Firebase does not give the option to do other wise. Thanks!Scrivenor
Even if onTokenRefresh() is called before user is logged in, instead of doing the work of storing it locally, when the user logs in , you can retrieve the token using FirebaseInstanceId.getInstance().getToken() and send it to the server for registration. (unless you want to store it locally for deleting an old token from your server)Stockdale
FireBase is clever and it will call onTokenRefresh() method, only if there hasn't token (it is deleted or it is called for the first time) or something else happen and token has been changed. If somebody wants to call onTokenRefresh could delete the token and then call FirebaseInstanceId.getInstance().getToken(). Operation FirebaseInstanceId.getInstance().deleteInstanceId() need to be in AsyncTask or new Thread, it could not be on the MainThread!!!Venetian
Why not just call FirebaseInstanceId.getToken?Recollection
well, worked perfectly when calling in IntentService, & no need to save token in prefs, i guess. Since the value does not change until FirebaseInstanceId.getInstance().deleteInstanceId(); is called. Kinda saved my day. :)Scrope
Why save the token to shared preferences - if you can call FirebaseInstanceId.getInstance().getToken() at any time to get its value?Spontaneity
calling FirebaseInstanceId.getInstance().deleteInstanceId(); will Resets Instance ID and revokes all tokens. Does it means previous tokens become invalid and not more able to receive notification?Algor
@Stockdale (thx), also not need to store locally for deleting from server. just before call DeleteToken method remove token from your own server.Fantoccini
@AlexanderFarber getToken() is deprecated now, Any other alternative for that now? It is asking for two String args to pass it. Can You help me with that?Frederiksberg
this answer worked great with a little tweaking for my needs. Able to use adb shell am startservice to call the delete service to trigger a token refresh.Oshea
A
18

Try to implement FirebaseInstanceIdService to get refresh token.

Access the registration token:

You can access the token's value by extending FirebaseInstanceIdService. Make sure you have added the service to your manifest, then call getToken in the context of onTokenRefresh, and log the value as shown:

     @Override
public void onTokenRefresh() {
    // Get updated InstanceID token.
    String refreshedToken = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + refreshedToken);

    // TODO: Implement this method to send any registration to your app's servers.
    sendRegistrationToServer(refreshedToken);
}

Full Code:

   import android.util.Log;

import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;


public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {

    private static final String TAG = "MyFirebaseIIDService";

    /**
     * Called if InstanceID token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the InstanceID token
     * is initially generated so this is where you would retrieve the token.
     */
    // [START refresh_token]
    @Override
    public void onTokenRefresh() {
        // Get updated InstanceID token.
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken);

        // TODO: Implement this method to send any registration to your app's servers.
        sendRegistrationToServer(refreshedToken);
    }
    // [END refresh_token]

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM InstanceID token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private void sendRegistrationToServer(String token) {
        // Add custom implementation, as needed.
    }
}

See my answer here.

EDITS:

You shouldn't be starting a FirebaseInstanceIdService yourself.

It will Called when the system determines that the tokens need to be refreshed. The application should call getToken() and send the tokens to all application servers.

This will not be called very frequently, it is needed for key rotation and to handle Instance ID changes due to:

  • App deletes Instance ID
  • App is restored on a new device User
  • uninstalls/reinstall the app
  • User clears app data

The system will throttle the refresh event across all devices to avoid overloading application servers with token updates.

Try below way:

you'd call FirebaseInstanceID.getToken() anywhere off your main thread (whether it is a service, AsyncTask, etc), store the returned token locally and send it to your server. Then whenever onTokenRefresh() is called, you'd call FirebaseInstanceID.getToken() again, get a new token, and send that up to the server (probably including the old token as well so your server can remove it, replacing it with the new one).

Albescent answered 29/5, 2016 at 12:12 Comment(5)
I did implement FirebaseInstanceIdService, the problem is onTokenRefresh() is being called almost immediately after user installs the app. i need it to be called after performing a Login/SignupScrivenor
So deleting the FirebaseInstanceId will refresh the token, thanks!Mitsukomitt
after GCM to FCM, FirebaseInstanceId.getInstance().getToken(); always return null. Any solution?Headroom
@TareKhoury You can call this method where ever required to get token. FirebaseInstanceId.getInstance().getToken();Thralldom
@Albescent in case of client app update will onTokenRefresh() get called ?Monmouth
J
4

For those who are looking for a way to force the refresh of your token and get this way onNewToken getting called because a new token was generated, you just need to call this whenever you need to do it:

FirebaseMessaging.getInstance().deleteToken().addOnSuccessListener {
  FirebaseMessaging.getInstance().token
}

I wrote it as a static function in MyFirebaseMessagingService like this for semplicity:

class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        if (remoteMessage.data.isNotEmpty()) {
            Log.d(TAG, "Message data payload: ${remoteMessage.data}")
        }

        // do your stuff.
    }

    override fun onNewToken(token: String) {
        Log.d(TAG, "FCM token changed: $token")
        
        // send it to your backend.
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"

        fun refreshFcmToken() {
            FirebaseMessaging.getInstance().deleteToken().addOnSuccessListener {
                FirebaseMessaging.getInstance().token
            }
        }
    }
}

Calling just deleteToken() is not enough since the new token will be generated only when it will be requested; of course it is requested everytime you open your app, so if you just call deleteToken() the new token will be generated the next time the user will open the app, but this can lead to problems if you need to send notifications right during or after his first use of the app.

And calling token() right after deleteToken() causes concurrency problems since they're both asynch operations and token() will end the execution always before deleteToken() (because it sees that the token already exist, since it is not deleted yet, and does not even try to generate a new one because of that, while deleteToken() is requesting to Firebase servers to delete the current token).

This is why you need to call token() after the deleteToken() thread has been successfully completed.

Juliannajulianne answered 7/7, 2021 at 14:20 Comment(0)
P
3

Guys it has very simple solution

https://developers.google.com/instance-id/guides/android-implementation#generate_a_token

Note: If your app used tokens that were deleted by deleteInstanceID, your app will need to generate replacement tokens.

In stead of deleting instance Id, delete only token:

String authorizedEntity = PROJECT_ID;
String scope = "GCM";
InstanceID.getInstance(context).deleteToken(authorizedEntity,scope);
Polydipsia answered 13/2, 2017 at 20:43 Comment(1)
Didn't work for me. After calling deleteToken(), getToken() returns the same token as before and onTokenRefresh wasn't called.Swadeshi
R
3
FirebaseMessaging.getInstance().getToken().addOnSuccessListener(new 
OnSuccessListener<String>() {
    @Override
    public void onSuccess(String newToken) {
        ....
    }
});
Ransdell answered 9/12, 2020 at 5:2 Comment(1)
Insane, this code solved it, not deprecated! Please remember to initialize first: FirebaseApp.initializeApp(context);Confessedly
I
2

I am maintaining one flag in shared pref which indicates whether gcm token sent to server or not. In Splash screen every time I am calling one method sendDevicetokenToServer. This method checks if user id is not empty and gcm send status then send token to server.

public static void  sendRegistrationToServer(final Context context) {

if(Common.getBooleanPerf(context,Constants.isTokenSentToServer,false) ||
        Common.getStringPref(context,Constants.userId,"").isEmpty()){

    return;
}

String token =  FirebaseInstanceId.getInstance().getToken();
String userId = Common.getUserId(context);
if(!userId.isEmpty()) {
    HashMap<String, Object> reqJson = new HashMap<>();
    reqJson.put("deviceToken", token);
    ApiInterface apiService =
            ApiClient.getClient().create(ApiInterface.class);

    Call<JsonElement> call = apiService.updateDeviceToken(reqJson,Common.getUserId(context),Common.getAccessToken(context));
    call.enqueue(new Callback<JsonElement>() {
        @Override
        public void onResponse(Call<JsonElement> call, Response<JsonElement> serverResponse) {

            try {
                JsonElement jsonElement = serverResponse.body();
                JSONObject response = new JSONObject(jsonElement.toString());
                if(context == null ){
                    return;
                }
                if(response.getString(Constants.statusCode).equalsIgnoreCase(Constants.responseStatusSuccess)) {

                    Common.saveBooleanPref(context,Constants.isTokenSentToServer,true);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        @Override
        public void onFailure(Call<JsonElement> call, Throwable throwable) {

            Log.d("", "RetroFit2.0 :getAppVersion: " + "eroorrrrrrrrrrrr");
            Log.e("eroooooooorr", throwable.toString());
        }
    });

}

}

In MyFirebaseInstanceIDService class

    @Override
public void onTokenRefresh() {
    // Get updated InstanceID token.
    String refreshedToken = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + refreshedToken);

    // If you want to send messages to this application instance or
    // manage this apps subscriptions on the server side, send the
    // Instance ID token to your app server.
    Common.saveBooleanPref(this,Constants.isTokenSentToServer,false);
    Common.sendRegistrationToServer(this);
    FirebaseMessaging.getInstance().subscribeToTopic("bloodRequest");
}
Inverson answered 29/9, 2016 at 12:55 Comment(0)
A
2

FirebaseInstanceIdService

This class is deprecated. In favour of overriding onNewToken in FirebaseMessagingService. Once that has been implemented, this service can be safely removed.

The new way to do this would be to override the onNewToken method from FirebaseMessagingService

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.e("NEW_TOKEN",s);
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
    }
} 

Also dont forget to add the service in the Manifest.xml

<service
    android:name=".MyFirebaseMessagingService"
    android:stopWithTask="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
Alewife answered 14/6, 2019 at 7:41 Comment(0)
U
1

This is in RxJava2 in scenario when one user logout from your app and other users login (Same App) To regerate and call login (If user's device didn't have internet connection earlier at the time of activity start and we need to send token in login api )

Single.fromCallable(() -> FirebaseInstanceId.getInstance().getToken())
            .flatMap( token -> Retrofit.login(userName,password,token))
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(simple -> {
                if(simple.isSuccess){
                    loginedSuccessfully();
                }
            }, throwable -> Utils.longToast(context, throwable.getLocalizedMessage()));

Login

@FormUrlEncoded
@POST(Site.LOGIN)
Single<ResponseSimple> login(@Field("username") String username,
                         @Field("password") String pass,
                         @Field("token") String token

);
Ulrike answered 21/6, 2018 at 10:5 Comment(0)
I
0

This answer does not destroy instance id, instead it is able to get current one. It also store refreshed one in Shared preferences.

Strings.xml

<string name="pref_firebase_instance_id_key">pref_firebase_instance_id</string>
<string name="pref_firebase_instance_id_default_key">default</string>

Utility.java (any class where you want to set/get preferences)

public static void setFirebaseInstanceId(Context context, String InstanceId) {
    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
    SharedPreferences.Editor editor;
    editor = sharedPreferences.edit();
    editor.putString(context.getString(R.string.pref_firebase_instance_id_key),InstanceId);
    editor.apply();
}

public static String getFirebaseInstanceId(Context context) {
    SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
    String key = context.getString(R.string.pref_firebase_instance_id_key);
    String default_value = context.getString(R.string.pref_firebase_instance_id_default_key);
    return sharedPreferences.getString(key, default_value);
}

MyFirebaseInstanceIdService.java (extends FirebaseInstanceIdService)

@Override
public void onCreate()
{
    String CurrentToken = FirebaseInstanceId.getInstance().getToken();

    //Log.d(this.getClass().getSimpleName(),"Inside Instance on onCreate");
    String savedToken = Utility.getFirebaseInstanceId(getApplicationContext());
    String defaultToken = getApplication().getString(R.string.pref_firebase_instance_id_default_key);

    if(CurrentToken != null && !savedToken.equalsIgnoreCase(defaultToken))
    //currentToken is null when app is first installed and token is not available
    //also skip if token is already saved in preferences...
    {
        Utility.setFirebaseInstanceId(getApplicationContext(),CurrentToken);
    }
    super.onCreate();
}

@Override
public void onTokenRefresh() {
     .... prev code
      Utility.setFirebaseInstanceId(getApplicationContext(),refreshedToken);
     ....

}

Android 2.0 and above onCreate of service is not invoked when started automatically (source). Instead onStartCommand is overridden and used. But in actual FirebaseInstanceIdService it is declared as final and can't be overridden. However, when we start service using startService(), if service is already running, its original instance is used (which is good). Our onCreate() (defined above) also got invoked!.

Use this in begining of MainActivity or at whichever point you think you need instance id.

MyFirebaseInstanceIdService myFirebaseInstanceIdService = new MyFirebaseInstanceIdService();
Intent intent= new Intent(getApplicationContext(),myFirebaseInstanceIdService.getClass());
//Log.d(this.getClass().getSimpleName(),"Starting MyFirebaseInstanceIdService");
startService(intent); //invoke onCreate

And Finally,

Utility.getFirebaseInstanceId(getApplicationContext())

Note, you can futher enhance this by trying to move startservice() code to getFirebaseInstanceId method.

Ignatius answered 23/8, 2016 at 19:45 Comment(1)
If you reset app /run first time, it takes some time for it to refresh token. So you will get string "default" for a min or two.Ignatius
C
0
    [Service]
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
class MyFirebaseIIDService: FirebaseInstanceIdService
{
    const string TAG = "MyFirebaseIIDService";
    NotificationHub hub;

    public override void OnTokenRefresh()
    {
        var refreshedToken = FirebaseInstanceId.Instance.Token;
        Log.Debug(TAG, "FCM token: " + refreshedToken);
        SendRegistrationToServer(refreshedToken);
    }

    void SendRegistrationToServer(string token)
    {
        // Register with Notification Hubs
        hub = new NotificationHub(Constants.NotificationHubName,
                                    Constants.ListenConnectionString, this);
        Employee employee = JsonConvert.DeserializeObject<Employee>(Settings.CurrentUser);
        //if user is not logged in 
        if (employee != null)
        {
            var tags = new List<string>() { employee.Email};
            var regID = hub.Register(token, tags.ToArray()).RegistrationId;

            Log.Debug(TAG, $"Successful registration of ID {regID}");
        }
        else
        {
            FirebaseInstanceId.GetInstance(Firebase.FirebaseApp.Instance).DeleteInstanceId();
            hub.Unregister();
        }
    }
}
Corrody answered 18/2, 2019 at 10:26 Comment(0)
V
0

How I update my deviceToken

First when I login I send the first device token under the user collection and the current logged in user.

After that, I just override onNewToken(token:String) in my FirebaseMessagingService() and just update that value if a new token is generated for that user

class MyFirebaseMessagingService: FirebaseMessagingService() {
    override fun onMessageReceived(p0: RemoteMessage) {
        super.onMessageReceived(p0)
    }

    override fun onNewToken(token: String) {
    super.onNewToken(token)
    val currentUser= FirebaseAuth.getInstance().currentUser?.uid
    if(currentUser != null){
        FirebaseFirestore.getInstance().collection("user").document(currentUser).update("deviceToken",token)
    }
 }
} 

Each time your app opens it will check for a new token, if the user is not yet signed in it will not update the token, if the user is already logged in you can check for a newToken

Virendra answered 11/4, 2020 at 14:36 Comment(0)
P
0

As a general approach to solve these issues: I wasn't able to solve my version of this problem with all the stackoverflow articles. What helped me though was to use the assistant in Android Studio-Tools-Firebase. In my case libraries had been missing from the build cradle files.

Phytophagous answered 12/9, 2021 at 11:28 Comment(0)
I
0

A short answer would be that if what you want to do inside your onTokenRefresh() requires that the user be logged in, then wrap all of it inside an:

if (FirebaseAuth.instance.currentUser != null) {
  // All code that requires a logged in user
}

That way nothing will happen until the user is logged in, at which point the method will fire again and stuff will happen. :)

Infidelity answered 25/4, 2022 at 18:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.