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.
Related
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;
}
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.
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.
I a Library in C that I'm leveraging for an Android application. This library has an audio stream that it occasionally flushes. When this happens it calls a write callback function of my design.
My intent is to have that C callback call a method on a specific Java Object which will handle stuff with the strem.
Currently I have code like so:
methodID compressionHandler=0;
jobject compressionHandlerClass;
int audioBufferChunkSize;
static JavaVM *gJavaVM;
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
gJavaVM = vm;
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_com_my_code_init(JNIEnv* env, jobject obj, /*classpath of the class we want to call against*/jstring compressedAudioHandlerPath, /*class instance we want to call against*/jobject callbackClass) {
......
// this is a global ref as per:
//http://stackoverflow.com/questions/14765776/jni-error-app-bug-accessed-stale-local-reference-0xbc00021-index-8-in-a-tabl
compressionHandlerClass = (*env)->NewGlobalRef(env,callbackClass);
// name of the class
const char *classLocation;
// convert jString to c String
classLocation = (*env)->GetStringUTFChars( env, compressedAudioHandlerPath , NULL ) ;
// tmp variable for holding the class location, relates to the above issue with garbage collection
jclass clazz = (*env)->FindClass(env, classLocation);
// the actual method that we want to call, this gets used in the writeCallback
compressionHandler = (*env)->GetMethodID(env, clazz, "handleCompressedAudio", "([B)V");
......
}
The callback method looks like so:
void writeCallback(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame, void *client_data) {
JNIEnv *env;
int isAttached = 0;
if ((status = (*gJavaVM)->GetEnv(gJavaVM, (void**)&env, JNI_VERSION_1_6)) < 0) {
if ((status = (*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL)) < 0) {
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
isAttached = 1;
}
if(*env!=0 && compressionHandler!=0){
jbyteArray arr = (*env)->NewByteArray(env,bytes);
(*env)->SetByteArrayRegion(env,arr, 0, bytes, (jbyte*)buffer);
(*env)->CallVoidMethod(env,compressionHandlerClass, compressionHandler,arr);
free(arr);
free(env);
free(isAttached);
}
}
I'm getting crashes at the CallVoidMethod, that signature of which is an interface implemented by whatever object I pass in:
public interface CompressedAudioHandler {
void handleCompressedAudio(byte[] buff);
}
I suspect that I am improperly attaining/keep references to these objects, but I haven't found a great way to handle that. Any advice on how I can more correctly handle this?
I want to call Java API from NDK C++ thread, but env->FindClass() return 0. But when I call Java API in main thread, it works well. I've already call AttachCurrentThread() in the thread, can anyone help me?
Here is the source code:
JAVA CODE:
public class simple_test extends Activity {
...
// This functin will be called in C++
public void PrintNdkLog(String slog) {
Log.e(logTagNDK, slog);
return;
}
}
C++ CODE:
static JavaVM* g_JavaVM = NULL;
jobject getInstance(JNIEnv *env, jclass obj_class)
{
jmethodID c_id = env->GetMethodID(obj_class, "<init>", "()V");
jobject obj = env->NewObject(obj_class, c_id);
return obj;
}
// JNI OnLoad
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
g_JavaVM = jvm;
return JNI_VERSION_1_6;
}
// Call JAVA API "PrintNdkLog" in this function
void PrintNdkLog(char *lpLog)
{
if (g_JavaVM == NULL)
return;
JNIEnv *env = NULL;
g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6);
if (env == NULL)
return;
jclass cls = env->FindClass("com/myndk/simple_test");
if (cls != 0) // **cls will be 0 when PrintNdkLog() is called in thread**
{
LOGE("FindClass error %p", cls);
}
else
{
jmethodID mid;
jobject obj;
obj = getInstance(env, cls);
mid = env->GetMethodID(cls, "PrintNdkLog", "(Ljava/lang/String;)V");
if (mid != 0)
{
jstring jstrMSG = env->NewStringUTF(lpLog);
env->CallVoidMethod(obj, mid, jstrMSG);
}
}
}
// Call JAVA API in thread
static void* thread_test(void* ptr)
{
JNIEnv *envLocal;
int status = g_JavaVM->GetEnv((void **) &envLocal, JNI_VERSION_1_6);
if (status == JNI_EDETACHED)
{
status = g_JavaVM->AttachCurrentThread(&envLocal, NULL);
if (status != JNI_OK)
LOGE("AttachCurrentThread failed %d",status);
}
PrintNdkLog("bbb"); // This JAVA callback failed, and printed "FindClass error"
}
// Create thread
int NdkThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority)
{
PrintNdkLog("aaa"); // This JAVA callback runs well
pthread_t pid;
pthread_create(&pid, NULL, thread_test, pParam);
}
I have solved it now.
In NDK native thread, only can call static Java API. If you call env->FindClass(), it would trigger an exception.
http://android.wooyd.org/JNIExample gived the detail info.
Suggest to take a look on AttachCurrentThread.
Here is a sample code to do that:
// Global variable
JavaVM *g_jvm = NULL; //Get g_jvm from jni main thread use env->GetJavaVM(&g_jvm);
jobject g_obj = NULL; //Where the java function exist. (some activity)
//Get env in thread function and attach the env
JNIEnv *env;
if(g_jvm->AttachCurrentThread(&env, NULL) != JNI_OK)
{
LOGD("%s: AttachCurrentThread() failed", __FUNCTION__);
}
const char * fnName ="somFunctionInYourJava"; //which should be "pulic void somFunctionInYourJava(String input);"
jstring retStr = env->NewStringUTF(str);
jclass cls = env->GetObjectClass(thiz);
jmethodID messageMe = env->GetMethodID(cls, fnName, "(Ljava/lang/String;)V");
env->CallVoidMethod(thiz, messageMe, retStr);
//Detach thread and release related resource
if(g_jvm->DetachCurrentThread() != JNI_OK)
{
LOGD("%s: DetachCurrentThread() failed", __FUNCTION__);
}