Search code examples
androidc++firebasefirebase-cloud-messagingunreal-engine4

Firebase C++ Messaging Crashing on Startup


So, I'm trying to use the Firebase c++ library in my Unreal project, but I'm getting some very consistent crashes: it crashes the first time I run it after a new uninstalling it, and works fine afterwards

Here's the stack trace I've got from firebase crash logging:

E/art ( 7271): No implementation found for void com.google.firebase.messaging.cpp.RegistrationIntentService.nativeOnTokenReceived(java.lang.String) (tried Java_com_google_firebase_messaging_cpp_RegistrationIntentService_nativeOnTokenReceived and Java_com_google_firebase_messaging_cpp_RegistrationIntentService_nativeOnTokenReceived__Ljava_lang_String_2)

E/UncaughtException( 7271):

E/UncaughtException( 7271): java.lang.UnsatisfiedLinkError: No implementation found for void com.google.firebase.messaging.cpp.RegistrationIntentService.nativeOnTokenReceived(java.lang.String) (tried Java_com_google_firebase_messaging_cpp_RegistrationIntentService_nativeOnTokenReceived and Java_com_google_firebase_messaging_cpp_RegistrationIntentService_nativeOnTokenReceived__Ljava_lang_String_2)

E/UncaughtException( 7271): at com.google.firebase.messaging.cpp.RegistrationIntentService.nativeOnTokenReceived(Native Method)

E/UncaughtException( 7271): at com.google.firebase.messaging.cpp.RegistrationIntentService.onHandleIntent(RegistrationIntentService.java:31)

E/UncaughtException( 7271): at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)

E/UncaughtException( 7271): at android.os.Handler.dispatchMessage(Handler.java:102)

E/UncaughtException( 7271): at android.os.Looper.loop(Looper.java:145)

E/UncaughtException( 7271): at android.os.HandlerThread.run(HandlerThread.java:61)

It says there's no implementation for nativeOnTokenReceived, but it is implemented in the firebase c++ sdk library. The crash happens when RegistrationIntentService is sent an intent from FcmInstanceIDListenerService, which happens when firebase gives a new token, which always happens on app startup after reinstalling it or clearing it's app data (I'm not sure if it's possible to make it happen at a different time than startup).

However, RegistrationIntentService has onHandleIntent activated, and calls nativeOnTokenReceived without any problems when my c++ listener class is initialized, during the course of the app. Does anybody know what might be causing this crash?

It might be relavent that Unreal's build process packages the static .a libraries from the sdk into a single .so before using ndk-build.

Here's the code for RegistrationIntentService and FcmInstanceIDListenerService extracted from the sdk's libmessaging_java.jar

FcmInstanceIDListenerService.java
    package com.google.firebase.messaging.cpp;

     import android.content.Intent;
     import com.google.firebase.iid.FirebaseInstanceIdService;

     public class FcmInstanceIDListenerService
       extends FirebaseInstanceIdService
     {
       public void onTokenRefresh()
      {
        Intent intent = new Intent(this, RegistrationIntentService.class);
         startService(intent);
       }
     }

RegistrationIntentService.java
    package com.google.firebase.messaging.cpp;

    import android.app.IntentService;
    import android.content.Intent;
    import com.google.firebase.iid.FirebaseInstanceId;

    public class RegistrationIntentService
       extends IntentService
     {
       private static final String TAG = "FirebaseRegService";

       public RegistrationIntentService()
       {
         super("FirebaseRegService");
       }

       protected void onHandleIntent(Intent intent)
       {
         DebugLogging.log("FirebaseRegService", String.format("onHandleIntent token=%s", new Object[] {
           FirebaseInstanceId.getInstance().getToken() }));
         String token = FirebaseInstanceId.getInstance().getToken();
         if (token != null) {
           nativeOnTokenReceived(token);
         }
       }

       private static native void nativeOnTokenReceived(String paramString);
     }

Solution

  • So, I guess I'm posting my solution to this problem up here..

    Firstly, I don't really know why there is a problem in the first place, but I have a feeling it has to do with Unreal's build system. (Fun fact: I've also had unexplainable errors in Google's Protobuf libraries, when trying to use them in Unreal Engine.)

    So since, the JNI couldn't find the library defined function it needed, I wrote my own function to replace it.

    Basically, when you're using the Firebase Messaging C++ SDK, there's two components to include in your project: the c++ static library and headers, and a java library, libmessaging_java.jar The jar file defines a few classes, most of which are good fine, but a few need to be edited, which you can do if you decompile it (I used this tool)

    So, inside RegistrationIntentService I declared

    private static native void nativeOnNewToken(String paramString);
    

    and replaced the call to nativeOnTokenReceived with it

    and in ListenerService I declared

    private static native void nativeOnNewMessage(String paramString1, String paramString2, String paramString3, Map<String, String> paramMap);
    

    and added it to writeMessageToInternalStorage()

      private void writeMessageToInternalStorage(String from, String msgId, String error, Map<String, String> data)
       {
       //Added code
        nativeOnNewMessage(from, msgId, error, data);
    
        //I'm passing the message directly to the C++ code, rather than storing
        //it in a buffer, and processing every once in a while
        //like the sdk normally does; so surround this crap with if(false)
    
        if(false){
        //end added code
             try
             {
               JSONObject json = messageToJson(from, msgId, error, data);
               DebugLogging.log("FIREBASE_LISTENER", json.toString());
               writeStorageFile(json.toString());
             } catch (JSONException e) {
               e.printStackTrace();
             }
         //added code
            }
        ///end added code
       }
    

    Now, the messages and tokens are being sent to my functions, so I need to declare them:

    #include "FirebaseMessageListener.h"
    #include "stdio.h"
    
    #include <string>
    #include <map>
    
    #if PLATFORM_ANDROID
    //jni calls from the listener services
        extern "C" void Java_com_google_firebase_messaging_cpp_ListenerService_nativeOnNewMessage(JNIEnv* jenv, jobject thiz, jstring from, jstring mssgID, jstring error, jobject data) {
            UE_LOG(FirebaseLog, Log, TEXT("Entering nativeOnNewMessage *****"));
            printf("Entering nativeOnNewMessage *****");
    
            std::map<std::string, std::string> data_out;
            std::string messageID;
            std::string fromID;
    
            //code iterating through map from java, based off code from here:https://android.googlesource.com/platform/frameworks/base.git/+/a3804cf77f0edd93f6247a055cdafb856b117eec/media/jni/android_media_MediaMetadataRetriever.cpp 
            // data is a Map<String, String>.
            if (data) {
                // Get the Map's entry Set.
                jclass mapClass = jenv->FindClass("java/util/Map");
                if (mapClass == NULL) {
                    return;
                }
                jmethodID entrySet =
                    jenv->GetMethodID(mapClass, "entrySet", "()Ljava/util/Set;");
                if (entrySet == NULL) {
                    return;
                }
                jobject set = jenv->CallObjectMethod(data, entrySet);
                if (set == NULL) {
                    return;
                }
                // Obtain an iterator over the Set
                jclass setClass = jenv->FindClass("java/util/Set");
                if (setClass == NULL) {
                    return;
                }
                jmethodID iterator =
                    jenv->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
                if (iterator == NULL) {
                    return;
                }
                jobject iter = jenv->CallObjectMethod(set, iterator);
                if (iter == NULL) {
                    return;
                }
                // Get the Iterator method IDs
                jclass iteratorClass = jenv->FindClass("java/util/Iterator");
                if (iteratorClass == NULL) {
                    return;
                }
                jmethodID hasNext = jenv->GetMethodID(iteratorClass, "hasNext", "()Z");
                if (hasNext == NULL) {
                    return;
                }
                jmethodID next =
                    jenv->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");
                if (next == NULL) {
                    return;
                }
                // Get the Entry class method IDs
                jclass entryClass = jenv->FindClass("java/util/Map$Entry");
                if (entryClass == NULL) {
                    return;
                }
                jmethodID getKey =
                    jenv->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
                if (getKey == NULL) {
                    return;
                }
                jmethodID getValue =
                    jenv->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
                if (getValue == NULL) {
                    return;
                }
                // Iterate over the entry Set
                while (jenv->CallBooleanMethod(iter, hasNext)) {
                    jobject entry = jenv->CallObjectMethod(iter, next);
                    jstring key = (jstring)jenv->CallObjectMethod(entry, getKey);
                    jstring value = (jstring)jenv->CallObjectMethod(entry, getValue);
                    const char* keyStr = jenv->GetStringUTFChars(key, NULL);
                    if (!keyStr) {  // Out of memory
                        return;
                    }
                    const char* valueStr = jenv->GetStringUTFChars(value, NULL);
                    if (!valueStr) {  // Out of memory
                        jenv->ReleaseStringUTFChars(key, keyStr);
                        return;
                    }
                    data_out.insert(std::pair<std::string, std::string>(std::string(keyStr), std::string(valueStr)));
                    jenv->DeleteLocalRef(entry);
                    jenv->ReleaseStringUTFChars(key, keyStr);
                    jenv->DeleteLocalRef(key);
                    jenv->ReleaseStringUTFChars(value, valueStr);
                    jenv->DeleteLocalRef(value);
                }
            }
    
            if (from != nullptr) {
                const char* valueStr = jenv->GetStringUTFChars(from, NULL);
                if (!valueStr) {  // Out of memory
                    return;
                }
                fromID = std::string(valueStr);
                jenv->ReleaseStringUTFChars(from, valueStr);
            }
    
            if (mssgID != nullptr) {
                const char* valueStr = jenv->GetStringUTFChars(mssgID, NULL);
                if (!valueStr) {  // Out of memory
                    return;
                }
                messageID = std::string(valueStr);
                jenv->ReleaseStringUTFChars(mssgID, valueStr);
            }
    
            FirebaseMessageListener::Get()->onNewMessage(fromID, messageID, data_out);
        }
    
        extern "C" void Java_com_google_firebase_messaging_cpp_RegistrationIntentService_nativeOnNewToken(JNIEnv* jenv, jobject thiz, jstring inJNIStr) {
            UE_LOG(FirebaseLog, Log, TEXT("Entering nativeOnNewToken *****"));
            printf("Entering nativeOnNewToken *****");
            //first, put the token into a c string
            jboolean isCopy;
            const char* token = jenv->GetStringUTFChars(inJNIStr, &isCopy);
            FirebaseMessageListener::Get()->onNewToken(token);
        }
    
    #endif
    

    These pass the messages on to a singleton class I made. You can do whatever you want with them here, but I pass them along to a firebase::messaging::Listener reference, to try and keep compatibility with the Firebase SDK (incase I can get it working properly)

    class RIFT411_API FirebaseMessageListener
    {
    private:
        static FirebaseMessageListener* self;
    
        //private so that new instance can only be made through call to Get()
        FirebaseMessageListener();
    #if PLATFORM_ANDROID
        JNIEnv * env = FAndroidApplication::GetJavaEnv();
        jobject activity = FAndroidApplication::GetGameActivityThis();
    #endif
    
        //signals to return the key
        void getToken();
    
        //send the messages to the firebase sdk implemented listener, so we'll have an easier time if we ever want to move back
        firebase::messaging::Listener *listener = nullptr;
    
    public:
        static FirebaseMessageListener* Get();
    
        void onNewToken(const char* token);
        void onNewMessage(std::string, std::string, std::map<std::string, std::string> &data);
    
        void Init(firebase::messaging::Listener *listener);
    
        ~FirebaseMessageListener();
    };
    

    //

    FirebaseMessageListener* FirebaseMessageListener::self = nullptr;
    
    
    FirebaseMessageListener* FirebaseMessageListener::Get()
    {
        if (self == nullptr) {
            self = new FirebaseMessageListener();
        }
        return self;
    }
    
    void FirebaseMessageListener::getToken() {
    #if PLATFORM_ANDROID
        //This has to happen in the main thread, or some of the FindClass() calls will fail
    
        UE_LOG(FirebaseLog, Log, TEXT("Trying to grab token *****"));
        printf("Trying to grab token *****");
    
        env = FAndroidApplication::GetJavaEnv();
        activity = FAndroidApplication::GetGameActivityThis();
    
        jclass cls_intent = (env)->FindClass("android/content/Intent");
        if (NULL == cls_intent) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting cls_intent")); return; }
    
        jclass cls_service = FAndroidApplication::FindJavaClass("com/google/firebase/messaging/cpp/RegistrationIntentService");
        //jclass cls_service = (env)->FindClass("com/google/firebase/messaging/cpp/RegistrationIntentService");
        if (NULL == cls_service) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting cls_service")); }
    
    
        // Get the Method ID of the constructor which takes an int
        jmethodID mid_Init = (env)->GetMethodID(cls_intent, "<init>", "(Landroid/content/Context;Ljava/lang/Class;)V");
        if (NULL == mid_Init) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting mid_Init")); return;}
    
        // Call back constructor to allocate a new instance, with an int argument
        jobject obj_intent = (env)->NewObject(cls_intent, mid_Init, activity, cls_service);
        if (NULL == obj_intent) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting obj_intent")); return; }
    
        if (NULL == activity) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting activity")); return; }
    
        jclass cls_activity = (env)->GetObjectClass(activity);
        if (NULL == cls_activity) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting cls_activity")); return; }
    
        jmethodID mid_start = (env)->GetMethodID(cls_activity, "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;");
        if (NULL == mid_start) { UE_LOG(FirebaseLog, Error, TEXT("Failure getting mid_start")); return; }
    
    
        UE_LOG(FirebaseLog, Log, TEXT("MADE IT TO THE END!"));
        (env)->CallObjectMethod(activity, mid_start, obj_intent);
    
    #endif
    }
    
    void FirebaseMessageListener::onNewToken(const char* token) {
        UE_LOG(FirebaseLog, Log, TEXT("Recieving new token in Unreal! Hooray! ***** %s"), *FString(token));
        printf("Recieving new Message in Unreal! Hooray! *****\n");
        if (listener != nullptr) {
            listener->OnTokenReceived(token);
        }
    }
    
    void FirebaseMessageListener::onNewMessage(std::string from, std::string MessageID, std::map<std::string, std::string> &data) {
        UE_LOG(FirebaseLog, Log, TEXT("Recieving new Message in Unreal! Hooray! *****"));
        printf("Recieving new Message in Unreal! Hooray! *****\n");
        if (!data.empty()) {
            UE_LOG(FirebaseLog, Log, TEXT("data:"));
            typedef std::map<std::string, std::string>::const_iterator MapIter;
            for (MapIter it = data.begin(); it != data.end(); ++it) {
                FString s1 = FString(it->first.c_str());
                FString s2 = FString(it->second.c_str());
                UE_LOG(FirebaseLog, Log, TEXT("  %s: %s"), *s1, *s2);
            }
        }
    
        ::firebase::messaging::Message message;
    
        message.data = data;
        message.from = from;
        message.message_id = MessageID;
    
    
        if (listener != nullptr) {
            listener->OnMessage(message);
        }
    }
    
    void FirebaseMessageListener::Init(firebase::messaging::Listener *newlistener) {
        this->listener = newlistener;
        FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady([=]()
        {
            getToken();
        }, TStatId(), NULL, ENamedThreads::GameThread);
    
    }
    
    FirebaseMessageListener::FirebaseMessageListener()
    {
        self = this;
    }
    
    FirebaseMessageListener::~FirebaseMessageListener()
    {
    }
    

    One thing about this is that you won't get data from notifications that are recieved while your app is closed. That data is packaged in an Intent, and I've almost got a good way to get it, to I'll probably post that once I finish it