// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "LaunchPrivatePCH.h" #include "ExceptionHandling.h" #include "AndroidPlatformCrashContext.h" #include "AndroidJNI.h" #include #include #define JNI_CURRENT_VERSION JNI_VERSION_1_6 #define USE_JNI_HELPER 0 JavaVM* GJavaVM; jobject GJavaGlobalThis = NULL; // Pointer to target widget for virtual keyboard contents static IVirtualKeyboardEntry *VirtualKeyboardWidget = NULL; extern FString GFilePathBase; extern FString GFontPathBase; extern bool GOBBinAPK; #if USE_JNI_HELPER ////////////////////////////////////////////////////////////////////////// // FJNIHelper // Caches access to the environment, attached to the current thread class FJNIHelper : public TThreadSingleton { public: static JNIEnv* GetEnvironment() { return Get().CachedEnv; } private: JNIEnv* CachedEnv = NULL; private: friend class TThreadSingleton; FJNIHelper() : CachedEnv(nullptr) { check(GJavaVM); GJavaVM->GetEnv((void **)&CachedEnv, JNI_CURRENT_VERSION); const jint AttachResult = GJavaVM->AttachCurrentThread(&CachedEnv, nullptr); if (AttachResult == JNI_ERR) { FPlatformMisc::LowLevelOutputDebugString(TEXT("FJNIHelper failed to attach thread to Java VM!")); check(false); } } ~FJNIHelper() { check(GJavaVM); const jint DetachResult = GJavaVM->DetachCurrentThread(); if (DetachResult == JNI_ERR) { FPlatformMisc::LowLevelOutputDebugString(TEXT("FJNIHelper failed to detach thread from Java VM!")); check(false); } CachedEnv = nullptr; } }; DECLARE_THREAD_SINGLETON( FJNIHelper ); #endif // USE_JNI_HELPER JNIEnv* GetJavaEnv(bool bRequireGlobalThis) { //@TODO: ANDROID: Remove the other version if the helper works well #if USE_JNI_HELPER if (!bRequireGlobalThis || (GJavaGlobalThis != nullptr)) { return FJNIHelper::GetEnvironment(); } else { return nullptr; } #else JNIEnv* Env = NULL; GJavaVM->GetEnv((void **)&Env, JNI_CURRENT_VERSION); jint AttachResult = GJavaVM->AttachCurrentThread(&Env, NULL); if (AttachResult == JNI_ERR) { FPlatformMisc::LowLevelOutputDebugString(L"UNIT TEST -- Failed to get the JNI environment!"); check(false); return nullptr; } return (!bRequireGlobalThis || (GJavaGlobalThis != nullptr)) ? Env : nullptr; #endif } ////////////////////////////////////////////////////////////////////////// //Declare all the static members of the class defs jclass JDef_GameActivity::ClassID; jmethodID JDef_GameActivity::AndroidThunkJava_KeepScreenOn; jmethodID JDef_GameActivity::AndroidThunkJava_Vibrate; jmethodID JDef_GameActivity::AndroidThunkJava_ShowConsoleWindow; jmethodID JDef_GameActivity::AndroidThunkJava_ShowVirtualKeyboardInput; jmethodID JDef_GameActivity::AndroidThunkJava_LaunchURL; jmethodID JDef_GameActivity::AndroidThunkJava_ResetAchievements; jmethodID JDef_GameActivity::AndroidThunkJava_ShowAdBanner; jmethodID JDef_GameActivity::AndroidThunkJava_HideAdBanner; jmethodID JDef_GameActivity::AndroidThunkJava_CloseAdBanner; jmethodID JDef_GameActivity::AndroidThunkJava_GetAssetManager; jmethodID JDef_GameActivity::AndroidThunkJava_Minimize; jmethodID JDef_GameActivity::AndroidThunkJava_ForceQuit; jmethodID JDef_GameActivity::AndroidThunkJava_GetFontDirectory; jmethodID JDef_GameActivity::AndroidThunkJava_IsMusicActive; DEFINE_LOG_CATEGORY_STATIC(LogEngine, Log, All); //Game-specific crash reporter void EngineCrashHandler(const FGenericCrashContext& GenericContext) { const FAndroidCrashContext& Context = static_cast< const FAndroidCrashContext& >( GenericContext ); static int32 bHasEntered = 0; if (FPlatformAtomics::InterlockedCompareExchange(&bHasEntered, 1, 0) == 0) { const SIZE_T StackTraceSize = 65535; ANSICHAR* StackTrace = (ANSICHAR*)FMemory::Malloc(StackTraceSize); StackTrace[0] = 0; // Walk the stack and dump it to the allocated memory. FPlatformStackWalk::StackWalkAndDump(StackTrace, StackTraceSize, 0, Context.Context); UE_LOG(LogEngine, Error, TEXT("%s"), ANSI_TO_TCHAR(StackTrace)); FMemory::Free(StackTrace); GError->HandleError(); FPlatformMisc::RequestExit(true); } } void AndroidThunkCpp_KeepScreenOn(bool Enable) { if (JNIEnv* Env = GetJavaEnv()) { // call the java side Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_KeepScreenOn, Enable); } } void AndroidThunkCpp_Vibrate(int64_t Duration) { if (JNIEnv* Env = GetJavaEnv()) { // call the java side Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_Vibrate, Duration); } } void AndroidThunkCpp_ShowConsoleWindow() { if (JNIEnv* Env = GetJavaEnv()) { // figure out all the possible texture formats that are allowed TArray PossibleTargetPlatforms; FPlatformMisc::GetValidTargetPlatforms(PossibleTargetPlatforms); // separate the format suffixes with commas FString ConsoleText; for (int32 FormatIndex = 0; FormatIndex < PossibleTargetPlatforms.Num(); FormatIndex++) { const FString& Format = PossibleTargetPlatforms[FormatIndex]; int32 UnderscoreIndex; if (Format.FindLastChar('_', UnderscoreIndex)) { if (ConsoleText != TEXT("")) { ConsoleText += ", "; } ConsoleText += Format.Mid(UnderscoreIndex+1); } } // call the java side jstring ConsoleTextJava = Env->NewStringUTF(TCHAR_TO_UTF8(*ConsoleText)); Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_ShowConsoleWindow, ConsoleTextJava); Env->DeleteLocalRef(ConsoleTextJava); } } void AndroidThunkCpp_ShowVirtualKeyboardInput(TSharedPtr TextWidget, int32 InputType, const FString& Label, const FString& Contents) { if (JNIEnv* Env = GetJavaEnv()) { // remember target widget for contents VirtualKeyboardWidget = &(*TextWidget); // call the java side jstring LabelJava = Env->NewStringUTF(TCHAR_TO_UTF8(*Label)); jstring ContentsJava = Env->NewStringUTF(TCHAR_TO_UTF8(*Contents)); Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_ShowVirtualKeyboardInput, InputType, LabelJava, ContentsJava); Env->DeleteLocalRef(ContentsJava); Env->DeleteLocalRef(LabelJava); } } //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeVirtualKeyboardResult(bool update, String contents);" extern "C" void Java_com_epicgames_ue4_GameActivity_nativeVirtualKeyboardResult(JNIEnv* jenv, jobject thiz, jboolean update, jstring contents) { // update text widget with new contents if OK pressed if (update == JNI_TRUE) { if (VirtualKeyboardWidget != NULL) { const char* javaChars = jenv->GetStringUTFChars(contents, 0); VirtualKeyboardWidget->SetTextFromVirtualKeyboard(FText::FromString(FString(UTF8_TO_TCHAR(javaChars)))); //Release the string jenv->ReleaseStringUTFChars(contents, javaChars); } } // release reference VirtualKeyboardWidget = NULL; } void AndroidThunkCpp_LaunchURL(const FString& URL) { if (JNIEnv* Env = GetJavaEnv()) { jstring Argument = Env->NewStringUTF(TCHAR_TO_UTF8(*URL)); Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_LaunchURL, Argument); Env->DeleteLocalRef(Argument); } } void AndroidThunkCpp_ResetAchievements() { if (JNIEnv* Env = GetJavaEnv()) { Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_ResetAchievements); } } void AndroidThunkCpp_ShowAdBanner(const FString& AdUnitID, bool bShowOnBottomOfScreen) { if (JNIEnv* Env = GetJavaEnv()) { jstring AdUnitIDArg = Env->NewStringUTF(TCHAR_TO_UTF8(*AdUnitID)); Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_ShowAdBanner, AdUnitIDArg, bShowOnBottomOfScreen); Env->DeleteLocalRef(AdUnitIDArg); } } void AndroidThunkCpp_HideAdBanner() { if (JNIEnv* Env = GetJavaEnv()) { Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_HideAdBanner); } } void AndroidThunkCpp_CloseAdBanner() { if (JNIEnv* Env = GetJavaEnv()) { Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_CloseAdBanner); } } namespace { jobject GJavaAssetManager = NULL; AAssetManager* GAssetManagerRef = NULL; } AAssetManager * AndroidThunkCpp_GetAssetManager() { if (!GAssetManagerRef) { if (JNIEnv* Env = GetJavaEnv()) { GJavaAssetManager = Env->CallObjectMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_GetAssetManager); Env->NewGlobalRef(GJavaAssetManager); GAssetManagerRef = AAssetManager_fromJava(Env, GJavaAssetManager); } } return GAssetManagerRef; } void AndroidThunkCpp_Minimize() { if (JNIEnv* Env = GetJavaEnv()) { Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_Minimize); } } void AndroidThunkCpp_ForceQuit() { if (JNIEnv* Env = GetJavaEnv()) { Env->CallVoidMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_ForceQuit); } } bool AndroidThunkCpp_IsMusicActive() { bool active = false; if (JNIEnv* Env = GetJavaEnv()) { active = (bool)Env->CallObjectMethod(GJavaGlobalThis, JDef_GameActivity::AndroidThunkJava_IsMusicActive); } return active; } //The JNI_OnLoad function is triggered by loading the game library from //the Java source file. // static // { // System.loadLibrary("MyGame"); // } // // Use the JNI_OnLoad function to map all the class IDs and method IDs to their respective // variables. That way, later when the Java functions need to be called, the IDs will be ready. // It is much slower to keep looking up the class and method IDs. #if UE_BUILD_SHIPPING #define CHECK_JNI_RESULT( Id ) #else #define CHECK_JNI_RESULT( Id ) \ if ( Id == 0 ) \ { \ FPlatformMisc::LowLevelOutputDebugString(TEXT("JNI_OnLoad: Failed to find " #Id)); \ } #endif JNIEXPORT jint JNI_OnLoad(JavaVM* InJavaVM, void* InReserved) { FPlatformMisc::LowLevelOutputDebugString(L"In the JNI_OnLoad function"); JNIEnv* env = NULL; InJavaVM->GetEnv((void **)&env, JNI_CURRENT_VERSION); // if you have problems with stuff being missing esspecially in distribution builds then it could be because proguard is stripping things from java // check proguard-project.txt and see if your stuff is included in the exceptions GJavaVM = InJavaVM; JDef_GameActivity::ClassID = env->FindClass("com/epicgames/ue4/GameActivity"); CHECK_JNI_RESULT( JDef_GameActivity::ClassID ); JDef_GameActivity::AndroidThunkJava_KeepScreenOn = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_KeepScreenOn", "(Z)V"); CHECK_JNI_RESULT(JDef_GameActivity::AndroidThunkJava_KeepScreenOn); JDef_GameActivity::AndroidThunkJava_Vibrate = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_Vibrate", "(J)V"); CHECK_JNI_RESULT(JDef_GameActivity::AndroidThunkJava_Vibrate); JDef_GameActivity::AndroidThunkJava_ShowConsoleWindow = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_ShowConsoleWindow", "(Ljava/lang/String;)V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_ShowConsoleWindow ); JDef_GameActivity::AndroidThunkJava_ShowVirtualKeyboardInput = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_ShowVirtualKeyboardInput", "(ILjava/lang/String;Ljava/lang/String;)V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_ShowVirtualKeyboardInput ); JDef_GameActivity::AndroidThunkJava_LaunchURL = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_LaunchURL", "(Ljava/lang/String;)V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_LaunchURL ); JDef_GameActivity::AndroidThunkJava_ResetAchievements = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_ResetAchievements", "()V"); JDef_GameActivity::AndroidThunkJava_ShowAdBanner = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_ShowAdBanner", "(Ljava/lang/String;Z)V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_ShowAdBanner ); JDef_GameActivity::AndroidThunkJava_HideAdBanner = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_HideAdBanner", "()V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_HideAdBanner ); JDef_GameActivity::AndroidThunkJava_CloseAdBanner = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_CloseAdBanner", "()V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_CloseAdBanner ); JDef_GameActivity::AndroidThunkJava_GetAssetManager = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_GetAssetManager", "()Landroid/content/res/AssetManager;"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_GetAssetManager ); JDef_GameActivity::AndroidThunkJava_Minimize = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_Minimize", "()V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_Minimize ); JDef_GameActivity::AndroidThunkJava_ForceQuit = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_ForceQuit", "()V"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_ForceQuit ); JDef_GameActivity::AndroidThunkJava_GetFontDirectory = env->GetStaticMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_GetFontDirectory", "()Ljava/lang/String;"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_GetFontDirectory ); JDef_GameActivity::AndroidThunkJava_IsMusicActive = env->GetMethodID(JDef_GameActivity::ClassID, "AndroidThunkJava_IsMusicActive", "()Z"); CHECK_JNI_RESULT( JDef_GameActivity::AndroidThunkJava_IsMusicActive ); // hook signals #if UE_BUILD_DEBUG if( GAlwaysReportCrash ) #else if( !FPlatformMisc::IsDebuggerPresent() || GAlwaysReportCrash ) #endif { FPlatformMisc::SetCrashHandler(EngineCrashHandler); } // Cache path to external storage jclass EnvClass = env->FindClass("android/os/Environment"); jmethodID getExternalStorageDir = env->GetStaticMethodID(EnvClass, "getExternalStorageDirectory", "()Ljava/io/File;"); jobject externalStoragePath = env->CallStaticObjectMethod(EnvClass, getExternalStorageDir, nullptr); jmethodID getFilePath = env->GetMethodID(env->FindClass("java/io/File"), "getPath", "()Ljava/lang/String;"); jstring pathString = (jstring)env->CallObjectMethod(externalStoragePath, getFilePath, nullptr); const char *nativePathString = env->GetStringUTFChars(pathString, 0); // Copy that somewhere safe GFilePathBase = FString(nativePathString); // then release... env->ReleaseStringUTFChars(pathString, nativePathString); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Path found as '%s'\n"), *GFilePathBase); // Next we check to see if the OBB file is in the APK jmethodID isOBBInAPKMethod = env->GetStaticMethodID(JDef_GameActivity::ClassID, "isOBBInAPK", "()Z"); GOBBinAPK = (bool)env->CallStaticBooleanMethod(JDef_GameActivity::ClassID, isOBBInAPKMethod, nullptr); // Get the system font directory jstring fontPath = (jstring)env->CallStaticObjectMethod(JDef_GameActivity::ClassID, JDef_GameActivity::AndroidThunkJava_GetFontDirectory); const char * nativeFontPathString = env->GetStringUTFChars(fontPath, 0); GFontPathBase = FString(nativeFontPathString); env->ReleaseStringUTFChars(fontPath, nativeFontPathString); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Font Path found as '%s'\n"), *GFontPathBase); // Wire up to core delegates, so core code can call out to Java DECLARE_DELEGATE_OneParam(FAndroidLaunchURLDelegate, const FString&); extern CORE_API FAndroidLaunchURLDelegate OnAndroidLaunchURL; OnAndroidLaunchURL = FAndroidLaunchURLDelegate::CreateStatic(&AndroidThunkCpp_LaunchURL); FPlatformMisc::LowLevelOutputDebugString(L"In the JNI_OnLoad function 5"); return JNI_CURRENT_VERSION; } //Native-defined functions //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeSetGlobalActivity();" extern "C" void Java_com_epicgames_ue4_GameActivity_nativeSetGlobalActivity(JNIEnv* jenv, jobject thiz) { if (!GJavaGlobalThis) { GJavaGlobalThis = jenv->NewGlobalRef(thiz); if(!GJavaGlobalThis) { FPlatformMisc::LowLevelOutputDebugString(L"Error setting the global GameActivity activity"); check(false); } } } extern "C" bool Java_com_epicgames_ue4_GameActivity_nativeIsShippingBuild(JNIEnv* LocalJNIEnv, jobject LocalThiz) { #if UE_BUILD_SHIPPING return JNI_TRUE; #else return JNI_FALSE; #endif }