Firebase C++ Messaging Crashing on Startup - android

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

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

com.google.firebase.messaging.cpp just listens for messages sent by FCM's service and forwards them to the Android C++ library (libmessaging.a). It looks like the problem is due to the firebase-messaging aar not being included in your application which had led to the com.google.firebase.messaging.cpp package not being loaded as it depends upon classes in the firebase-messaging aar that are not present.
For example, when building using gradle it's possible to include the aar using the following https://github.com/firebase/quickstart-cpp/blob/master/messaging/testapp/build.gradle#L93
Right now it's a little tricky to include aars in Unreal projects as described in https://groups.google.com/d/msg/firebase-talk/eGNr36dpB70/VXVqBfL1BAAJ . Basically you'll need to unzip (aars are just zip files) the firebase-messaging aar (look under ${ANDROID_HOME}/extras/google/firebase/firebase-messaging) and it's dependencies (you'll need to read each *.pom file to get the dependencies) and include the various .jars, AndroidManifest.xml files and resources extracted from each aar in your Unreal project. It's very important to make sure each AndroidManifest.xml gets merged into your final application's AndroidManifest.xml so that all services and content providers used by Firebase are included.

Related

Android: Exception when calling a static java method from cpp thread

When my java class loads, I call this jni method to set "listener" from cpp to java (I record audio using cpp and want to pass its bytes to java) :
MyJava.class
setListener(JNIEnv *env, jclass thiz);
myCpp.cpp
setListener(JNIEnv *env, jclass thiz) {
envMyClass = env;
classMyClass = thiz;
// I read that I need these 2 lines in order to connect the java thread to the cpp thread
env->GetJavaVM(&javavm);
GetJniEnv(javavm, &envCamera);
return 0;
}
bool GetJniEnv(JavaVM *vm, JNIEnv **env) {
bool did_attach_thread = false;
*env = nullptr;
// Check if the current thread is attached to the VM
auto get_env_result = vm->GetEnv((void**)env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
if (vm->AttachCurrentThread(env, NULL) == JNI_OK) {
did_attach_thread = true;
} else {
// Failed to attach thread. Throw an exception if you want to.
}
} else if (get_env_result == JNI_EVERSION) {
// Unsupported JNI version. Throw an exception if you want to.
}
return did_attach_thread;
}
and the in myCpp.cpp thread I'm trying to call:
if (envMyClass != nullptr && classMyClass != nullptr && javavm != nullptr) {
LOGD("000");
jmethodID javaMethod = envMyClass->GetStaticMethodID(classMyClass, "myJavaFunction", "()V");
LOGD("001");
envMyClass->CallStaticVoidMethod(classMyClass, javaMethod);
}
and it crashes on the line of "jmethodId javaMethod.."
myJavaFunction is a method in MyJava class:
public static void myJavaFunction() {
Log.d("my_log", "jni callback");
}
the crash:
Abort message: 'JNI DETECTED ERROR IN APPLICATION: a thread (tid 12346 is making JNI calls without being attached
in call to GetStaticMethodID'
Any idea how to fix it?
I've noticed a few things:
In setListener, I assume you are assigning jclass thiz to a global variable classMyClass. To do so, you should use classMyClass = env->NewGlobalRef(thiz), otherwise the class reference will not be valid (reference).
You are calling GetJniEnv but you do not check its result and therefore you do not actually know whether the current thread has attached successfully.
I assume you are attaching the current thread in setListener, save the env to a global variable, and then use that variable at another point in your code. Could it be that you are switching thread context somewhere in between? You could do the following to make sure the current thread is really attached:
JNIEnv *thisEnv;
int getEnvStat = jvm->GetEnv((void **)&thisEnv, JNI_VERSION_1_6 /* or whatever your JNI version is*/);
if (getEnvStat == JNI_EDETACHED)
{
if (jvm->AttachCurrentThread(&thisEnv, NULL) != 0)
{
// throw error
}
else
{
jmethodID javaMethod = thisEnv->GetStaticMethodID(classMyClass, "myJavaFunction", "()V");
thisEnv->CallStaticVoidMethod(classMyClass, javaMethod);
}
}
else if (getEnvStat == JNI_OK)
{
jmethodID javaMethod = thisEnv->GetStaticMethodID(classMyClass, "myJavaFunction", "()V");
thisEnv->CallStaticVoidMethod(classMyClass, javaMethod);
}
else if (getEnvStat == JNI_EVERSION)
{
// throw error
}
}
You can make this more efficient if you call jmethodID javaMethod = thisEnv->GetStaticMethodID(classMyClass, "myJavaFunction", "()V"); only once initially and store the jmethodID in a global variable. jmethodIDs are valid across envs.

How to properly and simply use callbacks in JNI NDK Android

I am trying to have Java call a C function in Android and some caveats that I am met with is the documentation for callbacks in general seem very unclear and most answers on SO are just talking about theory and posting broken links with no actual code. So please post code in your answer.
I am trying to create a joystick in java and have it send input to the NDK to perform actions.
if I can just have Java call a C function and pass a int to it ,
that would be good. but I think that wont work because C in running
it's own main loop.
perhaps I need to use C to call a java
function to get controller state and execute code from there.
I did manage to find some code Here but it seems the code while it does compile by itself, but doesn't appear to work when moved to modern code .
How should I implement call backs in a Java C relationship on Android? It seems that from the code example above that it is C that is calling Java functions and these need to be used like getters functions.
Here is some code that I have and I appear to be missing somethings because it is unclear to me how I should have these call backs performed. I will mark this code with function name and where is it located, if java or C++ (CPP) .
I think there are two ways that I would like this to work.
My Javafunction that I want to call
public int Cat(){
Log.e("JniHandler", "CAT WAS CALLED!!!");
return 333;
}
Structure information CPP . Not sure if it is needed.
#include <string.h>
#include <inttypes.h>
#include <pthread.h>
#include <jni.h>
#include <android/log.h>
#include <assert.h>
typedef struct tick_context {
JavaVM *javaVM;
jclass jniHelperClz;
jobject jniHelperObj;
jclass mainActivityClz;
jobject mainActivityObj;
pthread_mutex_t lock;
int done;
} TickContext;
TickContext g_ctx;
This section works. It looks that as soon as the JVM is launched, this will initialize.
JNI_OnLoad CPP
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
memset(&g_ctx, 0, sizeof(g_ctx));
SDL_Log(" DEBUG -- JNI INIT!!!");
g_ctx.javaVM = vm;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // JNI version not supported.
}
jclass clz = env->FindClass("org/foo/bar/mainactivity");
g_ctx.jniHelperClz = (jclass)env->NewGlobalRef(clz); // hack
jmethodID jniHelperCtor = env->GetMethodID(g_ctx.jniHelperClz,"<init>", "()V");
jobject handler = env->NewObject(g_ctx.jniHelperClz, jniHelperCtor);
g_ctx.jniHelperObj = env->NewGlobalRef(handler);
//queryRuntimeInfo(env, g_ctx.jniHelperObj); // This when getting version returns null.
g_ctx.done = 0;
g_ctx.mainActivityObj = NULL;
return JNI_VERSION_1_6;
}
queryRuntimeInfo CPP
void queryRuntimeInfo(JNIEnv *env, jobject instance) {
SDL_Log("DEBUG QUERYRUNTIME INTO");
// Find out which OS we are running on. It does not matter for this app
// just to demo how to call static functions.
// Our java JniHelper class id and instance are initialized when this
// shared lib got loaded, we just directly use them
// static function does not need instance, so we just need to feed
// class and method id to JNI
jmethodID versionFunc = env->GetStaticMethodID(g_ctx.jniHelperClz, "getBuildVersion", "()Ljava/lang/String;"); // This returns null.
if (!versionFunc) {
SDL_Log("DEBUG versionFunc INTO");
//LOGE("Failed to retrieve getBuildVersion() methodID # line %d",__LINE__);
return;
}
SDL_Log("DEBUG BUILD VERSION INTO");
jstring buildVersion = (jstring)env->CallStaticObjectMethod(g_ctx.jniHelperClz, versionFunc); // hack
const char *version = env->GetStringUTFChars(buildVersion, NULL);
if (!version) {
SDL_Log("DEBUG buildVersion INTO");
//LOGE("Unable to get version string # line %d", __LINE__);
return;
}
//LOGI("Android Version - %s", version);
SDL_Log("DEBUG ANDROID VERSION %s", version);
env->ReleaseStringUTFChars(buildVersion, version);
// we are called from JNI_OnLoad, so got to release LocalRef to avoid leaking
env->DeleteLocalRef(buildVersion);
// Query available memory size from a non-static public function
// we need use an instance of JniHelper class to call JNI
jmethodID memFunc = env->GetMethodID(g_ctx.jniHelperClz, "getRuntimeMemorySize", "()J");
if (!memFunc) {
//LOGE("Failed to retrieve getRuntimeMemorySize() methodID # line %d",__LINE__);
SDL_Log("DEBUG Failed to retrieve memory size");
return;
}
jlong result = env->CallLongMethod(instance, memFunc);
//LOGI("Runtime free memory size: %" PRId64, result);
SDL_Log("DEBUG RUN TIME FREE MEMORY SIZE");
(void)result; // silence the compiler warning
}
This function isn't ever called and I am not sure who is the caller. CPP
extern "C" JNIEXPORT void JNICALL InvokeFunction(JNIEnv *env, jobject instance) {
SDL_Log("JNI CALL START TICKET");
pthread_t threadInfo_;
pthread_attr_t threadAttr_;
pthread_attr_init(&threadAttr_);
pthread_attr_setdetachstate(&threadAttr_, PTHREAD_CREATE_DETACHED);
pthread_mutex_init(&g_ctx.lock, NULL);
jclass clz = env->GetObjectClass(instance);
g_ctx.mainActivityClz = (jclass)env->NewGlobalRef(clz); // hack
g_ctx.mainActivityObj = env->NewGlobalRef(instance);
int result = pthread_create( &threadInfo_, &threadAttr_, UpdateTicks, &g_ctx);
assert(result == 0);
pthread_attr_destroy(&threadAttr_);
(void)result;
}
updateticks CPP // This code is never actually called.
void* UpdateTicks(void* context) {
SDL_Log("DEBUG -- UPDATE TICKS IS CALLED");
TickContext *pctx = (TickContext*) context;
JavaVM *javaVM = pctx->javaVM;
JNIEnv *env;
jint res = javaVM->GetEnv((void**)&env, JNI_VERSION_1_6);
if (res != JNI_OK) {
res = javaVM->AttachCurrentThread(&env, NULL);
if (JNI_OK != res) {
//LOGE("Failed to AttachCurrentThread, ErrorCode = %d", res);
SDL_Log("FAILED TO ATTACH THREAD");
return NULL;
}
}
jmethodID statusId = env->GetMethodID(pctx->jniHelperClz, "updateStatus", "(Ljava/lang/String;)V");
//sendJavaMsg(env, pctx->jniHelperObj, statusId, "TickerThread status: initializing...");
// get mainActivity updateTimer function
jmethodID timerId = env->GetMethodID(pctx->mainActivityClz, "updateTimer", "()V");
struct timeval beginTime, curTime, usedTime, leftTime;
const struct timeval kOneSecond = {
(__kernel_time_t)1,
(__kernel_suseconds_t) 0
};
//sendJavaMsg(env, pctx->jniHelperObj, statusId, "TickerThread status: start ticking ...");
while(1) {
gettimeofday(&beginTime, NULL);
pthread_mutex_lock(&pctx->lock);
int done = pctx->done;
if (pctx->done) {
pctx->done = 0;
}
pthread_mutex_unlock(&pctx->lock);
if (done) {
break;
}
env->CallVoidMethod(pctx->mainActivityObj, timerId);
gettimeofday(&curTime, NULL);
timersub(&curTime, &beginTime, &usedTime);
timersub(&kOneSecond, &usedTime, &leftTime);
struct timespec sleepTime;
sleepTime.tv_sec = leftTime.tv_sec;
sleepTime.tv_nsec = leftTime.tv_usec * 1000;
if (sleepTime.tv_sec <= 1) {
nanosleep(&sleepTime, NULL);
} else {
//sendJavaMsg(env, pctx->jniHelperObj, statusId, "TickerThread error: processing too long!");
}
}
//sendJavaMsg(env, pctx->jniHelperObj, statusId, "TickerThread status: ticking stopped");
javaVM->DetachCurrentThread();
return context;
}

JNI on Android: Callback from native with Integer parameter works, with String or Void it doesn't. Why?

I have a callback class for doing callbacks from native C++ code to Kotlin (not sure if Kotlin/Java makes a difference here, if so, is there any documentation on that?).
I have a working callback with an integer parameter, that I call from different native threads without a problem. Now I want to add a second one that sends a String, but for some reason that doesn't work.
My callback class implementation looks like this:
jclass target;
jmethodID id;
Callback::Callback(JavaVM &jvm, jobject object) : g_jvm(jvm), g_object(object) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (g_env != NULL) {
target = g_env->GetObjectClass(g_object);
id = g_env->GetMethodID(target, "integerCallback", "(I)V");
}
}
void Callback::call(int integerValue, const char *stringValue) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
if (g_jvm.AttachCurrentThread(&g_env, NULL) != 0) {
LOGD("GetEnv: Failed to attach");
}
} else if (getEnvStat == JNI_OK) {
LOGD("GetEnv: JNI_OK");
} else if (getEnvStat == JNI_EVERSION) {
LOGD("GetEnv: version not supported");
}
g_env->CallVoidMethod(g_object, id, (jint) integerValue);
}
It gets instantiated in my native-lib.cpplike this:
extern "C" {
std::unique_ptr<Callback> callback;
JavaVM *g_jvm = nullptr;
static jobject myJNIClass;
jint JNI_OnLoad(JavaVM *pJvm, void *reserved) {
g_jvm = pJvm;
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_com_my_app_common_jni_JniBridge_loadNative(JNIEnv *env, jobject instance,
jstring URI, jboolean isWaitMode) {
myJNIClass = env->NewGlobalRef(instance);
if (callback == nullptr) {
callback = std::make_unique<Callback>(*g_jvm, myJNIClass);
}
}
The callback method it talks to is in my JniBridge.kt (the threading part is probbaly irrelevant to the problem):
object JniBridge {
init {
System.loadLibrary("native-lib")
Timber.d("Native Lib loaded!")
}
fun load(fileName: String, isWaitMode: Boolean) {
loadNative(fileName, isWaitMode)
}
fun integerCallback(value: Int) {
someListener?.invoke(value)
}
private external fun loadNative(fileName: String, isWaitMode: Boolean)
}
So now if my native code triggers the call() method in my callback, my integerCallback() in JniBridge.kt gets called correctly with an integer.
But here's what I don't get: If I change my callback to send a String it doesn't work. If I change it like this:
// in JniBridge.kt
fun stringCallback(value: String) {
someListener?.invoke(value)
}
// in Callback.cpp
//getting the method
id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String)V");
//making the call
g_env->CallVoidMethod(g_object, id, (jstring) stringValue);
Now my app crashes with this error:
JNI DETECTED ERROR IN APPLICATION: JNI GetStringUTFChars called with pending exception java.lang.NoSuchMethodError: no non-static method "Lcom/my/app/common/jni/JniBridge;.stringCallback(Ljava/lang/String)V"
The same happens if I try calling a void method (like this: id = g_env->GetMethodID(target, "voidCallback", "(V)V");or calling one that takes two integers (like this: id = g_env->GetMethodID(target, "twoIntegerCallback", "(I;I)V");, of course with having the corresponding methods in JniBridge.kt in place.
Why does this happen and how can I fix it?
Note: For clarity I have omitted all parts of my code that I believe are not related to the problem, if something crucial is missing, please let me know and I fix it.
You're missing a semi-colon in the method signature that you pass to GetMethodID.
It should be:
id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String;)V");
Note the semi-colon after Ljava/lang/String.
See the part about Type Signatures in Oracle's JNI documentation.
Regarding your other variations:
A Java void function() would have the signature ()V.
A Java void function(int i, int j) would have the signature (II)V.
As a side note, you really ought to verify the return value and check for exceptions after every JNI call.

C++ Callback to Java -> Why can't I retrieve JNIEnv in callback class when coming from a different thread?

I'm trying to build a callback class to make a call to a Java method from different threads in my native code, on Android. I have read a lot about how to that, and as long as I'm on the same thread, it all works. But from a different thread I can't retrieve the JNIEnv properly, and I can't figure out what I'm doing wrong.
I'm not very experienced with C++ and the JNI so it's quite possible this is some beginner's problem...but I've spetd days on it and can't see what it is.
This is my Callback class, .h and .cpp file:
class AudioCallback {
public:
explicit AudioCallback(JavaVM&, jobject);
void playBackProgress(int progressPercentage);
private:
JavaVM& g_jvm;
jobject g_object;
};
jclass target = NULL;
jmethodID id = NULL;
AudioCallback::AudioCallback(JavaVM &jvm, jobject object) : g_jvm(jvm), g_object(object) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (g_env != NULL) {
target = g_env->GetObjectClass(g_object);
id = g_env->GetMethodID(target, "integerCallback", "(I)V");
//This is a test call to see if I can call my java method. It works.
g_env->CallVoidMethod(g_object, id, (jint) 103);
}
}
// this method is calles from other threads, so I want to attach to the current thread once I got my JNIEnv, but I can't since it's null...
void AudioCallback::playBackProgress(int progressPercentage) {
JNIEnv *g_env;
// This is null and I don't know why!
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (g_env == NULL) {
LOGE("JNIEnv in callback method is null");
} else {
LOGD("Env Stat: %d", getEnvStat);
JavaVMAttachArgs vmAttachArgs;
if (getEnvStat == JNI_EDETACHED) {
LOGD("GetEnv: not attached - attaching");
if (g_jvm.AttachCurrentThread(&g_env, &vmAttachArgs) != 0) {
LOGD("GetEnv: Failed to attach");
}
} else if (getEnvStat == JNI_OK) {
LOGD("GetEnv: JNI_OK");
} else if (getEnvStat == JNI_EVERSION) {
LOGD("GetEnv: version not supported");
}
g_env->CallVoidMethod(g_object, id, (jint) progressPercentage);
//thread gets detached elsewhere
}
}
This is my native_lib, where I get the JavaVM and instantiate the callback class:
std::unique_ptr<AudioEngine> audioEngine;
std::unique_ptr<AudioCallback> callback;
JavaVM *g_jvm = nullptr;
static jobject myJNIClass;
jint JNI_OnLoad(JavaVM *pJvm, void *reserved) {
g_Jvm = pJvm;
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_com_my_appy_common_jni_JniBridge_playFromJNI(JNIEnv *env, jobject instance,jstring URI) {
myJNIClass = env->NewGlobalRef(instance);
callback = std::make_unique<AudioCallback>(*gJvm, myJNIClass);
// this test call to my callback works
callback->playBackProgress(104);
const char *uri = env->GetStringUTFChars(URI, NULL);
//... urelated code is left out here ...
//audioEngine gets the callback and uses it from threads it creates
audioEngine = std::make_unique<AudioEngine>(*extractor, *callback);
audioEngine->setFileName(uri);
audioEngine->start();
}
I've shortened the code and removed all unrelated/unnecessary parts. If something crucial is missing, please comment and I'll add it.
Solution: As per the suggestions #Michael made in his answer, I made these edits to the playbackProgressmethod in my callback class to make it work:
void AudioCallback::playBackProgress(int progressPercentage) {
JNIEnv *g_env;
int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
LOGD("GetEnv: not attached - attaching");
if (g_jvm.AttachCurrentThread(&g_env, NULL) != 0) {
LOGD("GetEnv: Failed to attach");
}
} else if (getEnvStat == JNI_OK) {
LOGD("GetEnv: JNI_OK");
} else if (getEnvStat == JNI_EVERSION) {
LOGD("GetEnv: version not supported");
}
g_env->CallVoidMethod(g_object, id, (jint) progressPercentage);
// mJvm.DetachCurrentThread();
}
Now it checks for the value of getEnvStat directly, the null check of g_env before was wrong. I also had to replace the JavaVMAttachArgswith NULLto make it work.
Your logic in playBackProgress wrong.
The only time your try to attach the current thread is when g_env is non-NULL. But if g_env is non-NULL then GetEnv probably succeeded (you should of course also check that getEnvStat == JNI_OK) and AttachCurrentThread isn't necessary.
The case in which you need to call AttachCurrentThread is when g_env is NULL and getEnvStat is JNI_EDETACHED.
You also need to keep track of whether you did call AttachCurrentThread, since you should call DetachCurrentThread at some point in those cases. See this answer for more info on that.

Flurry Android events not logging through NDK but logging through java

I am trying to integrate flurry into a NativeActivity (no-java based code) Android application, and am not having any success. I have set up a sister Java-based flurry test activity that works well, but the native calls seem to be the ones not getting through
In the manifest file, I have the permissions:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
package com.myTest.flurry;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import com.flurry.android.FlurryAgent;
public class FlurryActivity extends Activity {
//code has correct api key here
private static final String appKey = "xxxxxxxxxxxxxxxxxxxx";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flurry);
FlurryAgent.onStartSession(this, appKey);
FlurryAgent.setLogEnabled(true);
FlurryAgent.setLogLevel(Log.VERBOSE);
FlurryAgent.setLogEvents(true);
FlurryAgent.logEvent("Starting up!");
}
#Override
protected void onDestroy() {
super.onDestroy();
FlurryAgent.onEndSession(this);
}
}
Which simply enough makes the events show up on the flurry website. However, when calling the same functions through my native code, the events do not show up
#include "flurry.h"
#include <jni.h>
#include <android/native_activity.h>
#include <android_native_app_glue.h>
#include "log.h"
JNIEnv *GetEnv(JavaVM *jvm);
void DetachEnv(JavaVM *jvm);
Flurry::Flurry()
: state_(0) {
}
jclass Flurry::GetFlurryAgentClass(JNIEnv *env) {
// Extract the FlurryAgent class
jobject nativeActivity = state_->activity->clazz;
jclass acl = env->GetObjectClass(nativeActivity);
// Get the classloader
jmethodID getClassLoader = env->GetMethodID(acl, "getClassLoader", "()Ljava/lang/ClassLoader;");
jobject cls = env->CallObjectMethod(nativeActivity, getClassLoader);
jclass classLoader = env->FindClass("java/lang/ClassLoader");
// find the loadclass function
jmethodID findClass = env->GetMethodID(classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
// Run the loadClass function for the flurry agent
jstring strClassName = env->NewStringUTF("com/flurry/android/FlurryAgent");
const char *path = env->GetStringUTFChars(strClassName, 0);
// The following line will crash the app because it can't find the class files
jclass flurry_agent_class = (jclass)(env->CallObjectMethod(cls, findClass, strClassName));
env->DeleteLocalRef(strClassName);
if (env->ExceptionCheck() || flurry_agent_class == 0) {
Log("Failed to load flurry class!");
env->ExceptionDescribe();
env->ExceptionClear();
return 0;
}
return flurry_agent_class;
}
void Flurry::SetStateAndStartSession(android_app *state) {
state_ = state;
JNIEnv *env = GetEnv(state->activity->vm);
jclass flurry_agent_class = GetFlurryAgentClass(env);
if (flurry_agent_class == 0) {
return;
}
// Start session
jmethodID onStartSession_method = env->GetStaticMethodID(flurry_agent_class, "onStartSession", "(Landroid/content/Context;Ljava/lang/String;)V");
if (onStartSession_method) {
jstring api_key_string = env->NewStringUTF("xxxxxxxxxxxxxxxxxxxx");
Log("Starting native flurry");
env->CallStaticVoidMethod(flurry_agent_class, onStartSession_method, state->activity->clazz, api_key_string);
env->DeleteLocalRef(api_key_string);
}
// Set logging
jmethodID setLogEnabled_method = env->GetStaticMethodID(flurry_agent_class, "setLogEnabled", "(Z)V");
if (setLogEnabled_method) {
env->CallStaticVoidMethod(flurry_agent_class, setLogEnabled_method, JNI_TRUE);
}
// Set logging
jmethodID setLogLevel_method = env->GetStaticMethodID(flurry_agent_class, "setLogLevel", "(I)V");
if (setLogLevel_method) {
env->CallStaticVoidMethod(flurry_agent_class, setLogLevel_method, 2);
}
// Set events logging
jmethodID setLogEvents_method = env->GetStaticMethodID(flurry_agent_class, "setLogEvents", "(Z)V");
if (setLogEvents_method) {
env->CallStaticVoidMethod(flurry_agent_class, setLogEvents_method, JNI_TRUE);
}
// Set https
jmethodID setUseHttps_method = env->GetStaticMethodID(flurry_agent_class, "setUseHttps", "(Z)V");
if (setUseHttps_method) {
env->CallStaticVoidMethod(flurry_agent_class, setUseHttps_method, JNI_TRUE);
}
// Send the event
const char *eventname = "Native interface start";
jmethodID logEvent_method = env->GetStaticMethodID(flurry_agent_class, "logEvent", "(Ljava/lang/String;)V");
if (logEvent_method) {
Log("Sending Flurry Event: %s", eventname);
jstring event_name = env->NewStringUTF(eventname);
env->CallStaticVoidMethod(flurry_agent_class, logEvent_method, state_->activity->clazz, event_name);
env->DeleteLocalRef(event_name);
}
//LogEvent("Starting up native interface");
}
void Flurry::EndSession() {
// End session
JNIEnv *env = GetEnv(state_->activity->vm);
jclass flurry_agent_class = GetFlurryAgentClass(env);
if (flurry_agent_class == 0) {
return;
}
jmethodID onEndSession_method = env->GetStaticMethodID(flurry_agent_class, "onEndSession", "(Landroid/content/Context;)V");
if (onEndSession_method) {
Log("Ending flurry");
env->CallStaticVoidMethod(flurry_agent_class, onEndSession_method, state_->activity->clazz);
}
}
The output log for the flurry calls seems consistent with the java based calls:
Sample Log output:
I/FlurryAgent(25878): Agent cache file doesn't exist.
I/MyApp(25878): Starting native flurry
I/MyApp(25878): Sending Flurry Event: Native interface start
D/FlurryAgent(25878): generating report
D/FlurryAgent(25878): Sending report to: https://data.flurry.com/aap.do
D/FlurryAgent(25878): Report successful
D/FlurryAgent(25878): Done sending initial agent report
D/FlurryAgent(25878): Ending session
I am at a complete loss why the events from Java are showing up while the Native calls and events are not. Please help? Thanks
It appears that you are calling the logEvent method with the wrong parameters:
env->CallStaticVoidMethod(flurry_agent_class, logEvent_method, state_->activity->clazz, event_name);
should be
env->CallStaticVoidMethod(flurry_agent_class, logEvent_method, event_name);
because the FlurryAgent.logEvent method only takes in a string, not a context.

Categories

Resources