8

I'm trying to subscribe to an FCM (Firebase Cloud Messaging) topic for a secondary Firebase App and according to the documentation this could be done by the overloaded getInstance which takes the secondary FirebaseApp instance as a parameter:

https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/FirebaseMessaging#public-static-synchronized-firebasemessaging-getinstance-firebaseapp-app

public static synchronized FirebaseMessaging getInstance (FirebaseApp app)

Gets the FirebaseMessaging instance for the specified FirebaseApp.

I'm using Kotlin and I'm pulling in the package in build.gradle like this:

implementation "com.google.firebase:firebase-messaging:20.2.0"

But when I try to instantiate the FirebaseMessaging with the overloaded getInstance, I get an error stating that it's not accessible. When I look at the package source, the decompilation shows that the overloaded constructor is not public like the parameterless getInstance:

public class FirebaseMessaging {
    public static final String INSTANCE_ID_SCOPE = "FCM";
    private final Context zzb;
    private final FirebaseInstanceId zzc;
    private final Task<zzab> zzd;
    @Nullable
    @SuppressLint({"FirebaseUnknownNullness"})
    @VisibleForTesting
    static TransportFactory zza;

    @NonNull
    public static synchronized FirebaseMessaging getInstance() {
        return getInstance(FirebaseApp.getInstance());
    }

    @Keep
    @NonNull
    static synchronized FirebaseMessaging getInstance(@NonNull FirebaseApp var0) {
        return (FirebaseMessaging)var0.get(FirebaseMessaging.class);
    }

Did I miss something?


Additional note: one of my main tasks with the secondary Firebase project is to subscribe to a topic. I'd also read and write date to the Forestore database in that secondary project.

// Secondary project
firebaseMessaging.subscribeToTopic(GEO_FENCE_TOPIC)
            .addOnCompleteListener { task ->
                if (!task.isSuccessful) {
                    Timber.d("Could not subscribe to topic ${GEO_FENCE_TOPIC}")
                } else {
                    Timber.d("Subscribed to topic ${GEO_FENCE_TOPIC}")
                }
            }
Csaba Toth
  • 10,021
  • 5
  • 75
  • 121
  • That public one you linked to comes from Admin SDK `"com.google.firebase:firebase-admin:6.12.2"` that can be used on the server to send push messages, but I don't think it can be used to receive them. – arekolek Jul 02 '20 at 10:42
  • Also here's some relevant documentation https://firebase.google.com/docs/projects/multiprojects#use_multiple_projects_in_your_application that unfortunately does not help – arekolek Jul 02 '20 at 10:43
  • firebase-admin would be viable if I had a server side dedicated to that. If someone would use admin in an Android app that could pose a security risk: "Admin SDK bypasses security rules". So if someone disassembles the app (people indeed do that) then privileges could unleash. Since I'm using Firebase maybe we could use a Firebase Function to send the messages from server side, but then you'd need to secure that down somehow as well, so only the app could trigger the sending. This leads to a cat-and-mouse game. But thinking more: this wouldn't even work for secondary... – Csaba Toth Jul 03 '20 at 03:34
  • I cannot find the repository for FCM to file an issue about this. – Csaba Toth Jul 03 '20 at 03:51
  • I [contacted Firebase support](https://firebase.google.com/support/troubleshooter/report/features) and got a response that lead me to the answer I posted below. – arekolek Jul 03 '20 at 06:59

4 Answers4

23

I have verified a way to do it as I had been facing a similar issue.

I registered one of the projects using the google-services.json file.

Now as per the documentation :

public void onNewToken (String token)

Called when a new token for the default Firebase project is generated.

Here the word "default" is of key importance. It mentions that the onNewToken method in the overridden FirebaseMessagingService (eg: MyFirebaseMessagingService) will only be called for the default project.

Hence in this case the first project configured using the google-services.json will be the default project and for that the onNewToken method will be called.

For the second project, I manually configured the project using the code below following the documentation:

val options = FirebaseOptions.Builder()
        .setProjectId("my-firebase-project")
        .setApplicationId("1:27992087142:android:ce3b6448250083d1")
        .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
        .build()

The values for the parameters can be obtained from the google-services.json file of the second project. (NOTE: DON'T INCLUDE THE SECOND PROJECT'S google-services.json IN THE PROJECT)

google-services.json to manual code mapping

  1. projectId (setProjectId) : project_id key in the root of the json
  2. applicationid (setApplicationId): client > client_info > mobilesdk_app_id. Incase of multiple project make sure that the client used is of the package_name which matches the Android app
  3. apiKey (setApiKey) : client > api_key > current_key (make sure of the package name here as well.

KEY CODE

The most important part here which is tough to find in the documentation is to get the token of the second firebase project.

val app = Firebase.initialize(this, options, "ANY_FIXED_STRING_EXCEPT_DEFAULT")

val firebaseMessaging = app.get(FirebaseMessaging::class.java) as FirebaseMessaging

firebaseMessaging.token.addOnCompleteListener{
                if (!it.isSuccessful) {
                    Log.d(TAG, "Fetching FCM token failed", it.exception)

                    return@addOnCompleteListener
                }


                val token = it.result
                Log.d(TAG, "YM: $token")
                Toast.makeText(
                    activity,
                    "$TAG: Got token",
                    Toast.LENGTH_LONG
                ).show()
}
Tejas Sherdiwala
  • 750
  • 8
  • 15
  • 4
    You saved my life. Thanks. Poorly documented indeed. `val firebaseMessaging = app.get(FirebaseMessaging::class.java) as FirebaseMessaging` is exactly what I was looking for. – ahomphophone Jan 26 '21 at 19:23
  • I'm really confused, I know this is Kotlin and not Java, but what is going on at `app.get(FirebaseMessaging::class.java)`, looking at the docs of `FirebaseApp` there's no `get` method and it doesn't seem to be a thing of Kotlin (there's the getters and setters translation, but that something else). `(FirebaseMessaging) app.get(FirebaseMessaging.getClass())` seems invalid.. How would I do this in Java? Perhaps I am looking at different docs? – Avishay Matayev Dec 27 '21 at 11:16
  • Alright, by looking at the SDK's code, this is exactly how it should work, https://github.com/firebase/firebase-android-sdk/blob/master/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java#L155 `FirebaseMessaging firebaseMessaging = firebaseApp.get(FirebaseMessaging.class);` – Avishay Matayev Dec 27 '21 at 11:53
  • I do think. this is the best answer. plz, Mr. @csaba-toth accept this answer. and set tick green for that – Nader Gharibian Fard May 08 '22 at 06:10
  • This is correct way, it solves one problem. But in case of Token refresh or first time automatic token generation which now we get inside FIrebaseMessaging class using onNewToken() – Shadow Droid Mar 15 '23 at 09:46
  • sorry but What is ymFirebaseMessaging? I don't get this part – Muhammed Refaat Apr 07 '23 at 12:06
2

You can access the Firebase message instance using the get(<class>) call. This is with Firebase version 20.x.x

String appName = "FCM";
FirebaseOptions options = new FirebaseOptions.Builder()
                .setApplicationId(applicationId)
                .setApiKey(apiKey)
                .setGcmSenderId(gcmSenderId)
                .setProjectId(projectId)
                .build();

FirebaseApp app = FirebaseApp.initializeApp(context, options, appName);
FirebaseMessaging messageApp = app.get(FirebaseMessaging.class);
messageApp
     .getToken()
     .addOnFailureListener(
         ex -> {
            Log.w(TAG, "FAILED to get FCM token ", ex);
         })
     .addOnSuccessListener(
        instanceIdResult -> {
            String token = instanceIdResult;
            mPrefs.edit().putString(DEVICE_FCM_ID_KEY, token)
                         .putString(DEVICE_REG_ID_APP_VERSION_KEY, mWsiApp.getAppVersion())
                         .apply();
     });
LanDenLabs
  • 1,566
  • 16
  • 10
1

I reached into my Java background to pull a hack along the lines of "sudo make me a sandwich". Basically I forcefully obtain the other getInstance through reflection, force it to be accessible and call it anyway. But this is clearly a hack, so I'm waiting for a legit solution.

        val getInstance2: Method =
            FirebaseMessaging::class.java.getDeclaredMethod("getInstance", FirebaseApp::class.javaObjectType)
        getInstance2.setAccessible(true)  // if security settings allow this
        // null - for static methods
        val firebaseMessaging: FirebaseMessaging =
            getInstance2.invoke(null, appSingleton.firebaseApp!!) as FirebaseMessaging
        firebaseMessaging.subscribeToTopic(GEO_FENCE_TOPIC)
            .addOnCompleteListener { task ->
                if (!task.isSuccessful) {
                    Timber.d("Could not subscribe to topic ${GEO_FENCE_TOPIC}")
                } else {
                    Timber.d("Subscribed to topic ${GEO_FENCE_TOPIC}")
                }
            }

sudo make me a sandwich!!!

Csaba Toth
  • 10,021
  • 5
  • 75
  • 121
  • 1
    I guess alternatively you can make it public by delegating through your own class that you put in `package com.google.firebase.messaging`. Did you encounter any issues with this approach by the way? – arekolek Nov 05 '20 at 19:20
  • @arekolek That can be a good idea I'll look into that because that wouldn't be as much of a hack. As far as the messaging goes, the solution I show above does not error out, however I could not test it all the way since due to another bug the messages don't get into the queue yet https://github.com/DIYGPSTracker/DIYGPSMessaging/issues/1 (the Firestore added entity does not trigger the function for whatever reason). – Csaba Toth Nov 05 '20 at 22:43
  • I'm working on other projects so I could not get around to your answer either yet. – Csaba Toth Nov 05 '20 at 22:45
1

Note: this answer uses API that is deprecated in newer versions of Firebase SDK and have no replacement, nor there are any plans for replacement according to the feedback I received from Firebase support.

Answer regarding separate projects for FCM and Crashlytics:

Upon discussing with the team, they’ve verified that your use case is not supported by Firebase. You should be using a single Firebase Project/Firebase App for both FCM and Crashlytics. Thanks for understanding.

Answer regarding deprecated method to obtain a token for Sender ID:

I got an update from our engineers, currently the method FirebaseMessaging.getToken() that replaced the FirebaseInstanceId.getToken(senderId, scope) doesn't support multiple Firebase app instances. Upon checking on our end, there’s an existing feature request regarding this. With that, I’ve linked this support case to our existing feature request to support multiple instances in FIS. This will be discussed by our engineers, however, I can't specify how long this will take or exactly when (or if) this feature will be publicly released.


It is possible to receive messages from multiple senders using a different approach than instantiating multiple FirebaseApps.

You can get the Sender ID of the other project under Settings -> Cloud Messaging in Firebase Console and use it on the client to this end.

Sender ID is also what you will receive as RemoteMessage.getFrom() in FirebaseMessagingService.onMessageReceived(RemoteMessage).

Sender ID in Firebase Console

In your client app, you will have to retrieve a token for that sender, to authenticate receiving messages from the other project. Then use that token in the backend as the target of the push message.

val senderId = getSenderIdFromServer()

val token = FirebaseInstanceId.getInstance().getToken(senderId, "FCM")

sendTokenToServer(token)

Warning: FirebaseInstanceId.getToken is now deprecated and there is no replacement that I see in the SDK.

Looks like it's not possible to receive topic messages from a different project though. And currently the new server SDK lacks the ability to send to a device group.

arekolek
  • 9,128
  • 3
  • 58
  • 79
  • Thanks for all your effort. I haven't got around trying this yet, but if I can apply it I'll immediately mark it as the answer. – Csaba Toth Sep 28 '20 at 20:36