Firebase FCM 强制调用 onTokenRefresh() 方法

126

我正在将我的应用从GCM迁移到FCM。

当新用户安装我的应用时,onTokenRefresh()自动调用。问题在于用户尚未登录(没有用户ID)。

如何在用户登录后触发onTokenRefresh()


2
一个非常类似的问题已经在以下链接中被问到过。 请检查答案是否对您有用:https://dev59.com/6loU5IYBdhLWcg3wYWOx - Diego Giorgini
13个回答

185

onTokenRefresh()方法将在生成新令牌时调用。应用程序安装后,它将立即生成(正如您已经发现的那样)。当令牌发生更改时,也会调用该方法。

根据 FirebaseCloudMessaging 指南:

您可以将通知定向到单个特定设备。在应用程序初始启动时,FCM SDK 为客户端应用程序实例生成注册令牌。

屏幕截图

来源链接: https://firebase.google.com/docs/notifications/android/console-device#access_the_registration_token

这意味着令牌注册是针对应用程序的。您想在用户登录后利用令牌。我建议您在 onTokenRefresh() 方法中将令牌保存到内部存储或共享首选项中。然后,在用户登录后从存储中检索令牌,并根据需要向您的服务器注册令牌。

如果您想手动强制触发 onTokenRefresh(),可以创建 IntentService 并删除令牌实例。然后,当您调用 getToken 时,onTokenRefresh() 方法将再次被调用。

示例代码:

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

编辑

FirebaseInstanceIdService

public class FirebaseInstanceIdService extends Service

此类已被弃用,建议重写FirebaseMessagingService的onNewToken方法。 实现后,可以安全地删除此服务。

onTokenRefresh() 已被弃用。请使用 MyFirebaseMessagingService 中的 onNewToken() 方法。

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

34
即使在用户登录之前调用了onTokenRefresh(),而不是将其存储在本地,当用户登录时,您可以使用FirebaseInstanceId.getInstance().getToken()检索令牌并将其发送到服务器进行注册。(除非您想将其存储在本地以删除服务器上的旧令牌) - geekoraul
10
FireBase很聪明,只有在没有令牌(已删除或首次调用)或发生其他更改并更改了令牌时,它才会调用onTokenRefresh()方法。如果有人想调用onTokenRefresh,可以删除令牌,然后调用FirebaseInstanceId.getInstance().getToken()。FirebaseInstanceId.getInstance().deleteInstanceId()操作需要在AsyncTask或新线程中完成,不能在主线程中执行!!! - Stoycho Andreev
3
为什么不直接调用FirebaseInstanceId.getToken方法? - esong
8
为什么要将令牌保存到共享首选项中 - 如果你随时都可以调用[FirebaseInstanceId.getInstance().getToken()] (https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId) 来获取它的值呢? - Alexander Farber
1
@AlexanderFarber getToken()现在已经过时了,现在有其他替代方法吗?它要求传递两个字符串参数。你能帮我解决这个问题吗? - GiridharaSPK
显示剩余6条评论

18

尝试实现FirebaseInstanceIdService以获取刷新令牌。

访问注册令牌:

您可以通过扩展FirebaseInstanceIdService来访问令牌值。确保已将服务添加到您的清单文件中,然后在onTokenRefresh上下文中调用getToken并按如下所示记录该值:

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

完整代码:

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

请看我在这里的回答。

编辑:

你不应该自己启动FirebaseInstanceIdService

当系统确定需要刷新令牌时,它会调用此服务。应用程序应调用getToken()并将令牌发送到所有应用程序服务器。

这不会经常被调用,因为它需要进行密钥旋转,并处理由于以下原因导致的实例ID更改:

  • 应用程序已删除实例ID
  • 应用程序在新设备上恢复用户
  • 卸载/重新安装应用程序
  • 用户清除应用程序数据

系统会对所有设备的刷新事件进行限制,以避免向应用程序服务器发送过多的令牌更新。

尝试以下方法:

您可以在主线程之外的任何位置调用FirebaseInstanceID.getToken()(无论是服务、AsyncTask等),将返回的令牌存储在本地并将其发送到您的服务器。然后,每当调用onTokenRefresh()时,您都将再次调用FirebaseInstanceID.getToken(),获取一个新令牌并将其发送到服务器(可能还包括旧令牌,以便您的服务器可以将其删除并用新令牌替换它)。


2
我已经实现了FirebaseInstanceIdService,但问题是onTokenRefresh()在用户安装应用后几乎立即被调用。我需要它在执行登录/注册后才被调用。 - TareK Khoury
1
因此,删除FirebaseInstanceId将刷新令牌,谢谢! - Louis CAD
在从GCM迁移到FCM之后,FirebaseInstanceId.getInstance().getToken()总是返回null。有什么解决方案吗? - Govinda P
@TareKhoury 您可以在需要的任何地方调用此方法以获取令牌。 FirebaseInstanceId.getInstance().getToken(); - sssvrock
如果客户端应用程序更新,@pRaNaY,onTokenRefresh()会被调用吗? - Piyush Kukadiya

4

对于那些想要强制刷新令牌并通过这种方式调用 onNewToken 函数的人,只需在需要时调用以下内容:

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

为了简单起见,我将其编写成了MyFirebaseMessagingService中的静态函数:

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

仅调用 deleteToken() 不够,因为新令牌只有在被请求时才会生成; 当然,每次打开应用程序时都会请求它,因此如果你只调用 deleteToken() ,那么下一次用户打开应用程序时就会生成新的令牌,但如果你需要在他第一次使用应用程序期间或之后发送通知,则可能会导致问题。

而在 deleteToken() 后立即调用 token() 会导致并发问题,因为它们都是异步操作,而 token() 总是会在 deleteToken() 之前结束执行(因为它看到令牌已经存在,因为它尚未被删除,而且不会尝试生成新令牌,因为存在这种情况,而 deleteToken() 正在请求 Firebase 服务器删除当前令牌)。

这就是为什么你需要在成功完成 deleteToken() 线程之后调用 token()。


3

各位,这有一个非常简单的解决方案。

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

注意:如果您的应用程序使用了被deleteInstanceID删除的令牌,则您的应用程序需要生成替代令牌。

不要删除实例ID,只需删除令牌:

String authorizedEntity = PROJECT_ID;
String scope = "GCM";
InstanceID.getInstance(context).deleteToken(authorizedEntity,scope);

2
对我没用。在调用deleteToken()之后,getToken()返回的令牌与之前相同,并且onTokenRefresh没有被调用。 - Valeriya

3
FirebaseMessaging.getInstance().getToken().addOnSuccessListener(new 
OnSuccessListener<String>() {
    @Override
    public void onSuccess(String newToken) {
        ....
    }
});

疯狂,这段代码解决了它,没有过时!请记得先初始化:FirebaseApp.initializeApp(context); - Ezequiel Adrian

2

我在共享偏好设置中维护一个标记,指示是否将GCM令牌发送到服务器。在启动画面中,每次我都会调用一个名为sendDevicetokenToServer的方法。该方法检查用户ID不为空且GCM发送状态,则将令牌发送到服务器。

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

}

在我的FirebaseInstanceIDService类中

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

2

FirebaseInstanceIdService

此类已被弃用。建议重写 FirebaseMessagingService 中的 onNewToken 方法。一旦实现了该方法,就可以安全地移除此服务。

新的做法是从 FirebaseMessagingService 中重写 onNewToken 方法。

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

同时不要忘记在Manifest.xml文件中添加该服务。最初的回答希望您能在Manifest.xml文件中添加该服务。
<service
    android:name=".MyFirebaseMessagingService"
    android:stopWithTask="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

1

这是在RxJava2中的场景,当一个用户从你的应用程序注销,其他用户登录(同一应用程序)。为了重新生成和调用登录(如果用户的设备之前在活动开始时没有互联网连接,并且我们需要在登录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()));

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

);

0
作为解决这些问题的一般方法: 我无法通过所有stackoverflow文章解决我的版本的这个问题。然而,帮助我的是使用Android Studio-Tools-Firebase中的助手。在我的情况下,构建cradle文件中缺少了库。

0
简短的回答是,如果您在onTokenRefresh()内想要执行的操作需要用户已登录,则将所有操作都包装在一个中:
if (FirebaseAuth.instance.currentUser != null) {
  // All code that requires a logged in user
}

这样只有在用户登录后才会发生任何事情,此时该方法将再次触发并执行相关操作。 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接