// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #if USE_ANDROID_LAUNCH #include "Misc/App.h" #include "Misc/OutputDeviceError.h" #include "LaunchEngineLoop.h" #include #include #include "Android/AndroidJNI.h" #include "Android/AndroidEventManager.h" #include "Android/AndroidInputInterface.h" #include #include #include #include #include #include #include #include #include #include "Android/AndroidWindow.h" #include "Android/AndroidDynamicRHI.h" #include "Android/AndroidApplication.h" #include "Android/AndroidPlatformStackWalk.h" #include "HAL/PlatformApplicationMisc.h" #include "IHeadMountedDisplayModule.h" #include "ISessionServicesModule.h" #include "ISessionService.h" #include "Engine/Engine.h" #include "HAL/PlatformFile.h" #include "HAL/PlatformAffinity.h" #include "HAL/PlatformInput.h" #include "HAL/ThreadHeartBeat.h" #include "Modules/ModuleManager.h" #include "IMessagingModule.h" #include "Android/AndroidStats.h" #include "MoviePlayer.h" #include "PreLoadScreenManager.h" #include "Misc/EmbeddedCommunication.h" #include "Async/Async.h" #include #include #include "HAL/ExceptionHandling.h" #include "ProfilingDebugging/LoadTimeTracker.h" #include "Android/AndroidPlatformCrashContext.h" #include "UObject/GarbageCollection.h" #if USE_ANDROID_STANDALONE #include "HAL/FileManager.h" #include "HAL/PlatformFileManager.h" #include "IPlatformFilePak.h" #endif // Function pointer for retrieving joystick events // Function has been part of the OS since Honeycomb, but only appeared in the // NDK in r19. Querying via dlsym allows its use without tying to the newest // NDK. typedef float(*GetAxesType)(const AInputEvent*, int32_t axis, size_t pointer_index); static GetAxesType GetAxes = NULL; // Define missing events for earlier NDKs #if PLATFORM_ANDROID_NDK_VERSION < 140200 #define AMOTION_EVENT_AXIS_RELATIVE_X 27 #define AMOTION_EVENT_AXIS_RELATIVE_Y 28 #endif #ifndef ANDROID_ALLOWCUSTOMTOUCHEVENT #define ANDROID_ALLOWCUSTOMTOUCHEVENT 0 #endif // List of default axes to query for each controller // Ideal solution is to call out to Java and enumerate the list of axes. static const int32_t AxisList[] = { AMOTION_EVENT_AXIS_X, AMOTION_EVENT_AXIS_Y, AMOTION_EVENT_AXIS_Z, AMOTION_EVENT_AXIS_RX, AMOTION_EVENT_AXIS_RY, AMOTION_EVENT_AXIS_RZ, //These are DPAD analogs AMOTION_EVENT_AXIS_HAT_X, AMOTION_EVENT_AXIS_HAT_Y, }; // map of all supported keycodes static TSet MappedKeyCodes; // map of always allowed keycodes static TSet AlwaysAllowedKeyCodes; // List of always allowed keycodes static const uint32 AlwaysAllowedKeyCodesList[] = { AKEYCODE_MENU, AKEYCODE_BACK, AKEYCODE_VOLUME_UP, AKEYCODE_VOLUME_DOWN }; // List of desired gamepad keycodes static const uint32 ValidGamepadKeyCodesList[] = { AKEYCODE_BUTTON_A, AKEYCODE_DPAD_CENTER, AKEYCODE_BUTTON_B, AKEYCODE_BUTTON_C, AKEYCODE_BUTTON_X, AKEYCODE_BUTTON_Y, AKEYCODE_BUTTON_Z, AKEYCODE_BUTTON_L1, AKEYCODE_BUTTON_R1, AKEYCODE_BUTTON_START, AKEYCODE_MENU, AKEYCODE_BUTTON_SELECT, AKEYCODE_BACK, AKEYCODE_BUTTON_THUMBL, AKEYCODE_BUTTON_THUMBR, AKEYCODE_BUTTON_L2, AKEYCODE_BUTTON_R2, AKEYCODE_DPAD_UP, AKEYCODE_DPAD_DOWN, AKEYCODE_DPAD_LEFT, AKEYCODE_DPAD_RIGHT, 3002 // touchpad }; // map of gamepad keycodes that should be passed forward static TSet ValidGamepadKeyCodes; // -nostdlib means no crtbegin_so.o, so we have to provide our own __dso_handle and atexit() // this is not needed now we are using stdlib (later NDK has more functionality we should keep) #if 0 extern "C" { int atexit(void (*func)(void)) { return 0; } extern void *__dso_handle __attribute__((__visibility__ ("hidden"))); void *__dso_handle; } #endif #if USE_ANDROID_STANDALONE int32 GAndroidEnableNativeResizeEvent = 0; #else int32 GAndroidEnableNativeResizeEvent = 1; #endif static FAutoConsoleVariableRef CVarEnableResizeNativeEvent( TEXT("Android.EnableNativeResizeEvent"), GAndroidEnableNativeResizeEvent, TEXT("Whether native resize event is enabled on Android.\n") TEXT(" 0: disabled\n") TEXT(" 1: enabled (default)"), ECVF_ReadOnly); int32 GAndroidEnableMouse = 0; static FAutoConsoleVariableRef CVarEnableMouse( TEXT("Android.EnableMouse"), GAndroidEnableMouse, TEXT("Whether mouse support is enabled on Android.\n") TEXT(" 0: disabled (default)\n") TEXT(" 1: enabled"), ECVF_ReadOnly); int32 GAndroidEnableHardwareKeyboard = 0; static FAutoConsoleVariableRef CVarEnableHWKeyboard( TEXT("Android.EnableHardwareKeyboard"), GAndroidEnableHardwareKeyboard, TEXT("Whether hardware keyboard support is enabled on Android.\n") TEXT(" 0: disabled (default)\n") TEXT(" 1: enabled"), ECVF_ReadOnly); extern void AndroidThunkCpp_InitHMDs(); extern void AndroidThunkCpp_ShowConsoleWindow(); extern bool AndroidThunkCpp_VirtualInputIgnoreClick(int, int); extern bool AndroidThunkCpp_IsVirtualKeyboardShown(); extern bool AndroidThunkCpp_IsWebViewShown(); extern bool AndroidThunkCpp_HasExtraIgnoreView(); extern void AndroidThunkCpp_RestartApplication(const FString& IntentString); extern FString AndroidThunkCpp_GetIntentExtrasString(const FString& Key); // Base path for file accesses extern FString GFilePathBase; /** The global EngineLoop instance */ FEngineLoop GEngineLoop; #if USE_ANDROID_STANDALONE FString GAndroidCommandLine; #endif extern void* GAndroidWindowOverride; extern int GSurfaceViewWidth; extern int GSurfaceViewHeight; static bool bDidCompleteEngineInit = false; bool GShowConsoleWindowNextTick = false; static void AndroidProcessEvents(struct android_app* state); //Event thread stuff static void* AndroidEventThreadWorker(void* param); // Name of the UE commandline append setprop static constexpr char UECommandLineSetprop[] = "debug.ue.commandline"; //Android event callback functions static int32_t HandleInputCB(struct android_app* app, AInputEvent* event); //Touch and key input events static void OnAppCommandCB(struct android_app* app, int32_t cmd); //Lifetime events static bool TryIgnoreClick(AInputEvent* event, size_t actionPointer); bool GAllowJavaBackButtonEvent = false; bool GHasInterruptionRequest = false; bool GIsInterrupted = false; // Set 'SustainedPerformanceMode' via cvar sink. static TAutoConsoleVariable CVarEnableSustainedPerformanceMode( TEXT("Android.EnableSustainedPerformanceMode"), 0, TEXT("Enable sustained performance mode, if supported. (API >= 24 req. not supported by all devices.)\n") TEXT(" 0: Disabled (default)\n") TEXT(" 1: Enabled"), ECVF_Default); extern void AndroidThunkCpp_SetSustainedPerformanceMode(bool); static void SetSustainedPerformanceMode() { static bool bSustainedPerformanceMode = false; bool bIncomingSustainedPerformanceMode = CVarEnableSustainedPerformanceMode.GetValueOnAnyThread() != 0; if (bSustainedPerformanceMode != bIncomingSustainedPerformanceMode) { bSustainedPerformanceMode = bIncomingSustainedPerformanceMode; UE_LOG(LogAndroid, Log, TEXT("Setting sustained performance mode: %d"), (int32)bSustainedPerformanceMode); AndroidThunkCpp_SetSustainedPerformanceMode(bSustainedPerformanceMode); } } FAutoConsoleVariableSink CVarEnableSustainedPerformanceModeSink(FConsoleCommandDelegate::CreateStatic(&SetSustainedPerformanceMode)); // Event for coordinating pausing of the main and event handling threads to prevent background spinning static FEvent* EventHandlerEvent = NULL; // Wait for Java onCreate to complete before resume main init static volatile bool GResumeMainInit = false; volatile bool GEventHandlerInitialized = false; static void IssueConsoleCommand(FString Command) { static TArray GPendingConsoleCommands; if (GEngine != NULL) { // Run on game thread to avoid race condition with DeferredCommands AsyncTask(ENamedThreads::GameThread, [Command]() { if (GPendingConsoleCommands.Num() > 0) { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("IssueConsoleCommand: Pending count= %d"), GPendingConsoleCommands.Num()); for (FString& pendingCommand : GPendingConsoleCommands) { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("IssuePendingConsoleCommand: %s"), *pendingCommand); GEngine->DeferredCommands.Add(pendingCommand); } GPendingConsoleCommands.Empty(); } STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("IssueConsoleCommand: %s"), *Command); GEngine->DeferredCommands.Add(Command); }); } // to avoid out of order console command issues that were overwritten in configs, for non standalone we do not store pending. #if USE_ANDROID_STANDALONE else { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Ignoring console command (too early, Queue for later): %s"), *Command); GPendingConsoleCommands.Add(MoveTemp(Command)); } #endif } // The event thread locks this whenever the window unavailable during early init, pause and resume. FCriticalSection GAndroidWindowLock; static bool bAppIsActive_EventThread = false; static bool bHasFocus = false; static bool bHasWindow = false; static bool bIsResumed = false; static bool bShouldRestartFromInterrupt = false; static bool bIgnorePauseOnDownloaderStart = false; bool bReadyToProcessEvents = false; pthread_t G_AndroidEventThread; struct android_app* GNativeAndroidApp = NULL; static uint32 EventThreadID = 0; static void SuspendApp_EventThread(); static void ActivateApp_EventThread(); #if USE_ANDROID_STANDALONE && !(UE_BUILD_SHIPPING || UE_BUILD_TEST) #define DEVELOPER_LOG_COMMANDCB_CASE(a) STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("--TRACE-- OnAppCommandCB - Case [%s] : bHasFocus = %d, bHasWindow = %d, bIsResumed = %d, bShouldRestartFromInterrupt = %d, bReadyToProcessEvents=%d, bAppIsActive_EventThread=%d"), TEXT(#a), bHasFocus, bHasWindow, bIsResumed, bShouldRestartFromInterrupt, bReadyToProcessEvents, bAppIsActive_EventThread) #else #define DEVELOPER_LOG_COMMANDCB_CASE(a) #endif void FPlatformMisc::UnlockAndroidWindow() { check(IsInGameThread()); check(FTaskGraphInterface::IsRunning()); bReadyToProcessEvents = true; if(!FAndroidMisc::UseNewWindowBehavior()) { UE_LOG(LogAndroid, Log, TEXT("Unlocking android HW window during preinit. bAppIsActive_EventThread=%d"), bAppIsActive_EventThread); GAndroidWindowLock.Unlock(); } } JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeResumeMainInit(JNIEnv* jenv, jobject thiz) { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("nativeResumeMainInit() called, prev GResumeMainInit=%d, GEventHandlerInitialized=%d"), GResumeMainInit, GEventHandlerInitialized); GResumeMainInit = true; // now wait for event handler to be set up before returning while (!GEventHandlerInitialized) { FPlatformProcess::Sleep(0.01f); FPlatformMisc::MemoryBarrier(); } } static volatile bool GHMDsInitialized = false; static TArray GHMDImplementations; void InitHMDs() { if (FParse::Param(FCommandLine::Get(), TEXT("nohmd")) || FParse::Param(FCommandLine::Get(), TEXT("emulatestereo"))) { return; } // Get a list of plugins that implement this feature GHMDImplementations = IModularFeatures::Get().GetModularFeatureImplementations(IHeadMountedDisplayModule::GetModularFeatureName()); if (GHMDImplementations.IsEmpty()) { return; } AndroidThunkCpp_InitHMDs(); while (!GHMDsInitialized) { FPlatformProcess::Sleep(0.01f); FPlatformMisc::MemoryBarrier(); } } extern AAssetManager* AndroidThunkCpp_GetAssetManager(); /** * Shuts down the engine */ void EngineExit(void) { // Make sure this is set RequestEngineExit(TEXT("EngineExit() was called")); GEngineLoop.Exit(); UE_LOG(LogAndroid, Log, TEXT("Exiting is over")); FPlatformMisc::RequestExit(1); } static void InitCommandLine() { static const uint32 CMD_LINE_MAX = 16384u; STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("LaunchAndroid::InitCommandLine called!")); // initialize the command line to an empty string FCommandLine::Set(TEXT("")); #if !UE_BUILD_SHIPPING FString CmdLine = AndroidThunkCpp_GetIntentExtrasString(TEXT("cmdline")); if (!CmdLine.IsEmpty()) { CmdLine.TrimEndInline(); FCommandLine::Append(*CmdLine); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("adb am start command line override: %s"), FCommandLine::Get()); } else #endif { AAssetManager* AssetMgr = AndroidThunkCpp_GetAssetManager(); AAsset* asset = AAssetManager_open(AssetMgr, TCHAR_TO_UTF8(TEXT("UECommandLine.txt")), AASSET_MODE_BUFFER); if (nullptr != asset) { const void* FileContents = AAsset_getBuffer(asset); int32 FileLength = AAsset_getLength(asset); char CommandLine[CMD_LINE_MAX]; FileLength = (FileLength < CMD_LINE_MAX - 1) ? FileLength : CMD_LINE_MAX - 1; memcpy(CommandLine, FileContents, FileLength); CommandLine[FileLength] = '\0'; AAsset_close(asset); // chop off trailing spaces while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1])) { CommandLine[strlen(CommandLine) - 1] = 0; } FCommandLine::Append(UTF8_TO_TCHAR(CommandLine)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("APK Commandline: %s"), FCommandLine::Get()); } #if !UE_BUILD_SHIPPING && USE_ANDROID_STANDALONE UE_SET_LOG_VERBOSITY(LogAndroid, Log); if (!GAndroidCommandLine.IsEmpty()) { FCommandLine::Set(TEXT("")); FCommandLine::Append(*GAndroidCommandLine); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Override Commandline: %s"), FCommandLine::Get()); } #endif // read in the command line text file from the sdcard if it exists FString CommandLineFilePath = GFilePathBase + FString("/UnrealGame/") + (!FApp::IsProjectNameEmpty() ? FApp::GetProjectName() : FPlatformProcess::ExecutableName()) + FString("/UECommandLine.txt"); FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r"); if (CommandLineFile == NULL) { // if that failed, try the lowercase version CommandLineFilePath = CommandLineFilePath.Replace(TEXT("UECommandLine.txt"), TEXT("uecommandline.txt")); CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r"); } if (CommandLineFile) { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Using override commandline file: %s"), *CommandLineFilePath); char CommandLine[CMD_LINE_MAX]; fgets(CommandLine, UE_ARRAY_COUNT(CommandLine) - 1, CommandLineFile); fclose(CommandLineFile); // chop off trailing spaces while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1])) { CommandLine[strlen(CommandLine) - 1] = 0; } // initialize the command line to an empty string FCommandLine::Set(TEXT("")); FCommandLine::Append(UTF8_TO_TCHAR(CommandLine)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Override Commandline: %s"), FCommandLine::Get()); } } #if !UE_BUILD_SHIPPING if (const FString* ConfigRulesCmdLineAppend = FAndroidMisc::GetConfigRulesVariable(TEXT("cmdline"))) { FCommandLine::Append(**ConfigRulesCmdLineAppend); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ConfigRules appended: %s"), **ConfigRulesCmdLineAppend); } char CommandLineSetpropAppend[CMD_LINE_MAX]; if (__system_property_get(UECommandLineSetprop, CommandLineSetpropAppend) > 0) { FCommandLine::Append(UTF8_TO_TCHAR(" ")); FCommandLine::Append(UTF8_TO_TCHAR(CommandLineSetpropAppend)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("UE setprop appended: %s"), UTF8_TO_TCHAR(CommandLineSetpropAppend)); } #endif #ifdef UE_ANDROID_COMMAND_LINE_OVERRIDE FCommandLine::Set(TEXT(UE_ANDROID_COMMAND_LINE_OVERRIDE)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("UE_ANDROID_COMMAND_LINE_OVERRIDE: %s"), TEXT(UE_ANDROID_COMMAND_LINE_OVERRIDE)); #endif #ifdef UE_ANDROID_COMMAND_LINE_APPEND FCommandLine::Append(TEXT(UE_ANDROID_COMMAND_LINE_APPEND)); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("UE_ANDROID_COMMAND_LINE_APPEND: %s"), TEXT(UE_ANDROID_COMMAND_LINE_APPEND)); #endif } extern void AndroidThunkCpp_DismissSplashScreen(); //Called in main thread for native window resizing static void OnNativeWindowResized(ANativeActivity* activity, ANativeWindow* window) { static int8_t cmd = APP_CMD_WINDOW_RESIZED; struct android_app* app = (struct android_app *)activity->instance; write(app->msgwrite, &cmd, sizeof(cmd)); } static void ApplyAndroidCompatConfigRules() { TArray AndroidCompatCVars; if (GConfig->GetArray(TEXT("AndroidCompatCVars"), TEXT("CVars"), AndroidCompatCVars, GEngineIni)) { TSet AllowedCompatCVars(AndroidCompatCVars); for (const TTuple& Pair : FPlatformMisc::GetConfigRuleVars()) { const FString& Key = Pair.Key; const FString& Value = Pair.Value; static const TCHAR AndroidCompat[] = TEXT("AndroidCompat."); if (Key.StartsWith(AndroidCompat)) { FString CVarName = Key.Mid(UE_ARRAY_COUNT(AndroidCompat)-1); if (AllowedCompatCVars.Contains(CVarName)) { auto* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); if (CVar) { // set with HF priority // configrules are therefore higher priority than deviceprofiles. (e.g. -dpcvars) CVar->Set(*Value, ECVF_SetByHotfix); UE_LOG(LogAndroid, Log, TEXT("Compat Setting %s = %s"), *CVarName, *Value); } } } } } } //Main function called from the android entry point int32 AndroidMain(struct android_app* state); // The function is used only for ASIS void* AndroidMain(void* param) { struct android_app* state = (struct android_app*)param; FTaskTagScope Scope(ETaskTag::EGameThread); if (GGameThreadId == 0) { GGameThreadId = FPlatformTLS::GetCurrentThreadId(); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidMain set current GGameThreadId=%d"), GGameThreadId); } AndroidMain(state); return nullptr; } int32 AndroidMain(struct android_app* state) { BootTimingPoint("AndroidMain"); FPlatformMisc::LowLevelOutputDebugString(TEXT("Entered AndroidMain()\n")); // Force the first call to GetJavaEnv() to happen on the game thread, allowing subsequent calls to occur on any thread FAndroidApplication::GetJavaEnv(); #if !USE_ANDROID_STANDALONE // Set window format to 8888 ANativeActivity_setWindowFormat(state->activity, WINDOW_FORMAT_RGBA_8888); #endif #if !UE_BUILD_SHIPPING && USE_ANDROID_STANDALONE STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("ENABLE LogAndroid verbosity!\n")); UE_SET_LOG_VERBOSITY(LogAndroid, Log); #endif // adjust the file descriptor limits to allow as many open files as possible rlimit cur_fd_limit; { int result = getrlimit(RLIMIT_NOFILE, & cur_fd_limit); //FPlatformMisc::LowLevelOutputDebugStringf(TEXT("(%d) Current fd limits: soft = %lld, hard = %lld"), result, cur_fd_limit.rlim_cur, cur_fd_limit.rlim_max); } { rlimit new_limit = cur_fd_limit; new_limit.rlim_cur = cur_fd_limit.rlim_max; new_limit.rlim_max = cur_fd_limit.rlim_max; int result = setrlimit(RLIMIT_NOFILE, &new_limit); //FPlatformMisc::LowLevelOutputDebugStringf(TEXT("(%d) Setting fd limits: soft = %lld, hard = %lld"), result, new_limit.rlim_cur, new_limit.rlim_max); } { int result = getrlimit(RLIMIT_NOFILE, & cur_fd_limit); //FPlatformMisc::LowLevelOutputDebugStringf(TEXT("(%d) Current fd limits: soft = %lld, hard = %lld"), result, cur_fd_limit.rlim_cur, cur_fd_limit.rlim_max); } // setup joystick support // r19 is the first NDK to include AMotionEvent_getAxisValue in the headers // However, it has existed in the so since Honeycomb, query for the symbol // to determine whether to try controller support { void* Lib = dlopen("libandroid.so",0); if (Lib != NULL) { GetAxes = (GetAxesType)dlsym(Lib, "AMotionEvent_getAxisValue"); } if (GetAxes != NULL) { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Controller interface supported\n")); } else { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Controller interface UNsupported\n")); } } // setup key filtering static const uint32 MAX_KEY_MAPPINGS(256); uint32 KeyCodes[MAX_KEY_MAPPINGS]; uint32 NumKeyCodes = FPlatformInput::GetKeyMap(KeyCodes, nullptr, MAX_KEY_MAPPINGS); for (int i = 0; i < NumKeyCodes; ++i) { MappedKeyCodes.Add(KeyCodes[i]); } const int AlwaysAllowedKeyCodesCount = sizeof(AlwaysAllowedKeyCodesList) / sizeof(uint32); for (int i = 0; i < AlwaysAllowedKeyCodesCount; ++i) { AlwaysAllowedKeyCodes.Add(AlwaysAllowedKeyCodesList[i]); } const int ValidGamepadKeyCodeCount = sizeof(ValidGamepadKeyCodesList)/sizeof(uint32); for (int i = 0; i < ValidGamepadKeyCodeCount; ++i) { ValidGamepadKeyCodes.Add(ValidGamepadKeyCodesList[i]); } FAndroidPlatformStackWalk::InitStackWalking(); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT(" wait for java activity onCreate to finish, GResumeMainInit=%d"), GResumeMainInit); // wait for java activity onCreate to finish { SCOPED_BOOT_TIMING("Wait for GResumeMainInit"); while (!GResumeMainInit) { FPlatformProcess::Sleep(0.01f); FPlatformMisc::MemoryBarrier(); } } // read the command line file InitCommandLine(); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Final commandline: %s (len %i)\n"), FCommandLine::Get(), FCString::Strlen(FCommandLine::Get())); const TCHAR* CmdLine = FCommandLine::Get(); #if !(UE_BUILD_SHIPPING) // If "-waitforattach" or "-WaitForDebugger" was specified, halt startup and wait for a debugger to attach before continuing if (FParse::Param(FCommandLine::Get(), TEXT("waitforattach")) || FParse::Param(FCommandLine::Get(), TEXT("WaitForDebugger"))) { FPlatformMisc::LowLevelOutputDebugString(TEXT("Waiting for debugger to attach...\n")); while (!FPlatformMisc::IsDebuggerPresent()); UE_DEBUG_BREAK(); } #endif EventHandlerEvent = FPlatformProcess::GetSynchEventFromPool(false); FPlatformMisc::LowLevelOutputDebugString(TEXT("Created sync event\n")); FAppEventManager::GetInstance()->SetEventHandlerEvent(EventHandlerEvent); // ready for onCreate to complete GEventHandlerInitialized = true; // Initialize file system access (i.e. mount OBBs, etc.). // We need to do this really early for Android so that files in the // OBBs and APK are found. // Have to use a special initialize if using the PersistentStorageManager IPlatformFile::GetPlatformPhysical().Initialize(nullptr, FCommandLine::Get()); if(!FAndroidMisc::UseNewWindowBehavior()) { SCOPED_BOOT_TIMING("Wait for GAndroidWindowLock.Lock()"); // wait for a valid window // Lock GAndroidWindowLock to ensure no window destroy shenanigans occur between early phase of preinit and UnlockAndroidWindow // Note: this is unlocked after Android's PlatformCreateDynamicRHI when the RHI is then able to process window changes. // We don't wait for all of preinit to complete as PreLoadScreens will need to process events during preinit. UE_LOG(LogAndroid, Log, TEXT("PreInit android HW window lock. bAppIsActive_EventThread=%d"), bAppIsActive_EventThread); GAndroidWindowLock.Lock(); } else { UE_LOG(LogAndroid, Log, TEXT("PreInit android using new window behavior. bAppIsActive_EventThread=%d"), bAppIsActive_EventThread); } FPlatformMisc::LowLevelOutputDebugString(TEXT("After GAndroidWindowLock in AndroidMain")); FDelegateHandle ConfigReadyHandle = FCoreDelegates::TSConfigReadyForUse().AddStatic(&ApplyAndroidCompatConfigRules); // initialize the engine int32 PreInitResult = GEngineLoop.PreInit(0, NULL, FCommandLine::Get()); FCoreDelegates::TSConfigReadyForUse().Remove(ConfigReadyHandle); if (PreInitResult != 0) { checkf(false, TEXT("Engine Preinit Failed")); return PreInitResult; } // register callback for native window resize if (GAndroidEnableNativeResizeEvent) { state->activity->callbacks->onNativeWindowResized = OnNativeWindowResized; } // initialize HMDs { SCOPED_BOOT_TIMING("InitHMDs"); InitHMDs(); } UE_LOG(LogAndroid, Display, TEXT("Passed PreInit()")); GLog->SetCurrentThreadAsPrimaryThread(); int32 ErrorLevel = PreInitResult; FAppEventManager::GetInstance()->SetEmptyQueueHandlerEvent(FPlatformProcess::GetSynchEventFromPool(false)); ErrorLevel = GEngineLoop.Init(); bDidCompleteEngineInit = true; UE_LOG(LogAndroid, Log, TEXT("Passed GEngineLoop.Init()")); double EngineInitializationTime = FPlatformTime::Seconds() - GStartTime; UE_LOG(LogLoad, Log, TEXT("(Engine Initialization) Total time: %.2f seconds"), EngineInitializationTime); ACCUM_LOADTIME(TEXT("EngineInitialization"), EngineInitializationTime); AndroidThunkCpp_DismissSplashScreen(); #if !UE_BUILD_SHIPPING if (FParse::Param(FCommandLine::Get(), TEXT("Messaging"))) { // initialize messaging subsystem FModuleManager::LoadModuleChecked("Messaging"); TSharedPtr SessionService = FModuleManager::LoadModuleChecked("SessionServices").GetSessionService(); SessionService->Start(); // Initialize functional testing FModuleManager::Get().LoadModule("FunctionalTesting"); } #endif FAndroidStats::Init(); BootTimingPoint("Tick loop starting"); DumpBootTiming(); // tick until done while (!IsEngineExitRequested()) { FAndroidStats::UpdateAndroidStats(); FAppEventManager::GetInstance()->Tick(); if (!FAppEventManager::GetInstance()->IsGamePaused() #if USE_ANDROID_STANDALONE //&& FAppEventManager::GetInstance()->IsGameInFocus() #endif ) { GEngineLoop.Tick(); } else { // use less CPU when paused FPlatformProcess::Sleep(0.10f); // TODO:we could continue to tick the engine loop as the new window behavior no longer blocks the RT . // if (FAndroidMisc::UseNewWindowBehavior()) // { // GEngineLoop.Tick(); // } } #if !UE_BUILD_SHIPPING // show console window on next game tick if (GShowConsoleWindowNextTick) { GShowConsoleWindowNextTick = false; AndroidThunkCpp_ShowConsoleWindow(); } #endif } TRACE_BOOKMARK(TEXT("Tick loop end")); FAppEventManager::GetInstance()->TriggerEmptyQueue(); UE_LOG(LogAndroid, Log, TEXT("Exiting")); // exit out! GEngineLoop.Exit(); UE_LOG(LogAndroid, Log, TEXT("Exiting is over")); FPlatformMisc::RequestExit(true, TEXT("AndroidMain")); return 0; } struct AChoreographer; struct FChoreographer { typedef void(*AChoreographer_frameCallback)(long frameTimeNanos, void* data); typedef AChoreographer* (*func_AChoreographer_getInstance)(); typedef void(*func_AChoreographer_postFrameCallback)( AChoreographer* choreographer, AChoreographer_frameCallback callback, void* data); typedef void(*func_AChoreographer_postFrameCallbackDelayed)( AChoreographer* choreographer, AChoreographer_frameCallback callback, void* data, long delayMillis); func_AChoreographer_getInstance AChoreographer_getInstance_ = nullptr; func_AChoreographer_postFrameCallback AChoreographer_postFrameCallback_ = nullptr; func_AChoreographer_postFrameCallbackDelayed AChoreographer_postFrameCallbackDelayed_ = nullptr; FCriticalSection ChoreographerSetupLock; TFunction Callback; void SetupChoreographer() { FScopeLock Lock(&ChoreographerSetupLock); //check(!AChoreographer_getInstance_); if (!AChoreographer_getInstance_) { void* lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); if (lib != nullptr) { // Retrieve function pointers from shared object. AChoreographer_getInstance_ = reinterpret_cast( dlsym(lib, "AChoreographer_getInstance")); AChoreographer_postFrameCallback_ = reinterpret_cast( dlsym(lib, "AChoreographer_postFrameCallback")); AChoreographer_postFrameCallbackDelayed_ = reinterpret_cast( dlsym(lib, "AChoreographer_postFrameCallbackDelayed")); } if (!AChoreographer_getInstance_ || !AChoreographer_postFrameCallback_ || !AChoreographer_postFrameCallbackDelayed_) { UE_LOG(LogAndroid, Warning, TEXT("Failed to set up Choreographer")); AChoreographer_getInstance_ = nullptr; AChoreographer_postFrameCallback_ = nullptr; AChoreographer_postFrameCallbackDelayed_ = nullptr; } else { SetCallback(0); UE_LOG(LogAndroid, Display, TEXT("Choreographer set up.")); } } } void SetupCallback(TFunction InCallback) { check(IsAvailable()); FScopeLock Lock(&ChoreographerSetupLock); Callback = InCallback; } void SetCallback(int64 Delay); void DoCallback(long frameTimeNanos) { //static long LastFrameTimeNanos = 0; //UE_LOG(LogAndroid, Warning, TEXT("Choreographer %lld delta %lld"), frameTimeNanos, frameTimeNanos - LastFrameTimeNanos); //LastFrameTimeNanos = frameTimeNanos; int64 NextDelay = -1; { FScopeLock Lock(&ChoreographerSetupLock); if (Callback) { NextDelay = Callback(frameTimeNanos); } } SetCallback((NextDelay >= 0) ? NextDelay : 0); } bool IsAvailable() { return !!AChoreographer_getInstance_; } }; FChoreographer TheChoreographer; bool ChoreographerIsAvailable() { return TheChoreographer.IsAvailable(); } void StartChoreographer(TFunction Callback) { check(ChoreographerIsAvailable()); TheChoreographer.SetupCallback(Callback); } static void choreographer_callback(long frameTimeNanos, void* data) { TheChoreographer.DoCallback(frameTimeNanos); } void FChoreographer::SetCallback(int64 Delay) { check(IsAvailable()); check(Delay >= 0); AChoreographer* choreographer = AChoreographer_getInstance_(); UE_CLOG(!choreographer, LogAndroid, Fatal, TEXT("Choreographer was null (wrong thread?).")); AChoreographer_postFrameCallbackDelayed_(choreographer, choreographer_callback, nullptr, Delay / 1000000); } bool IsInAndroidEventThread() { // Note: leave the commented out line for debug purposes. //STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("IsInAndroidEventThread(), GGameThreadId=%d, EventThreadID=%d, FPlatformTLS::GetCurrentThreadId()=%d"), GGameThreadId, EventThreadID, FPlatformTLS::GetCurrentThreadId()); check(EventThreadID != 0); return EventThreadID == FPlatformTLS::GetCurrentThreadId(); } static void* AndroidEventThreadWorker( void* param ) { FTaskTagScope::SwapTag(ETaskTag::EEventThread); pthread_setname_np(pthread_self(), "EventWorker"); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidEventThreadWorker(begin), GGameThreadId=%d, EventThreadID=%d, FPlatformTLS::GetCurrentThreadId()=%d"), GGameThreadId, EventThreadID, FPlatformTLS::GetCurrentThreadId()); EventThreadID = FPlatformTLS::GetCurrentThreadId(); FAndroidMisc::RegisterThreadName("EventWorker", EventThreadID); struct android_app* state = (struct android_app*)param; FPlatformProcess::SetThreadAffinityMask(FPlatformAffinity::GetMainGameMask()); FPlatformMisc::LowLevelOutputDebugString(TEXT("Entering event processing thread engine entry point")); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidEventThreadWorker(started), EventThreadID=%d"), EventThreadID); ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); ALooper_addFd(looper, state->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL, &state->cmdPollSource); state->looper = looper; FPlatformMisc::LowLevelOutputDebugString(TEXT("Prepared looper for event thread")); //Assign the callbacks state->onAppCmd = OnAppCommandCB; state->onInputEvent = HandleInputCB; FPlatformMisc::LowLevelOutputDebugString(TEXT("Passed callback initialization")); FPlatformMisc::LowLevelOutputDebugString(TEXT("Passed sensor initialization")); TheChoreographer.SetupChoreographer(); if(!FAndroidMisc::UseNewWindowBehavior()) { // window is initially invalid/locked. UE_LOG(LogAndroid, Log, TEXT("AndroidEventThreadWorker, Initial HW window lock.")); GAndroidWindowLock.Lock(); } DEVELOPER_LOG_COMMANDCB_CASE(AndroidEventThreadWorker_BeforeWhile); //continue to process events until the engine is shutting down while (!IsEngineExitRequested()) { AndroidProcessEvents(state); } DEVELOPER_LOG_COMMANDCB_CASE(AndroidEventThreadWorker_AfterWhile); if (!FAndroidMisc::UseNewWindowBehavior()) { GAndroidWindowLock.Unlock(); } UE_LOG(LogAndroid, Log, TEXT("AndroidEventThreadWorker->Exiting")); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Exiting AndroidEventThreadWorker")); return NULL; } //Called from the separate event processing thread static void AndroidProcessEvents(struct android_app* State) { struct android_poll_source* Source = nullptr; int32 Result = ALooper_pollOnce(-1, nullptr, nullptr, (void**)&Source); if (Result != ALOOPER_POLL_ERROR && Source != nullptr) { Source->process(State, Source); } } void android_main(struct android_app* state) { FTaskTagScope Scope(ETaskTag::EGameThread); GGameThreadId = FPlatformTLS::GetCurrentThreadId(); BootTimingPoint("android_main"); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Entering native app glue main function, GGameThreadId=%d"), GGameThreadId); GNativeAndroidApp = state; check(GNativeAndroidApp); #if USE_ANDROID_STANDALONE STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("android_main : USE_ANDROID_STANDALONE is defined and enabled")); #endif pthread_attr_t otherAttr; pthread_attr_init(&otherAttr); pthread_attr_setdetachstate(&otherAttr, PTHREAD_CREATE_DETACHED); pthread_create(&G_AndroidEventThread, &otherAttr, AndroidEventThreadWorker, state); FPlatformMisc::LowLevelOutputDebugString(TEXT("Created event thread")); // Make sure glue isn't stripped. (not needed in ndk-15) #if PLATFORM_ANDROID_NDK_VERSION < 150000 app_dummy(); #endif //@todo android: replace with native activity, main loop off of UI thread, etc. AndroidMain(state); } void* android_main(void* param) { struct android_app* state = (struct android_app*)param; android_main(state); return nullptr; } extern bool GAndroidGPUInfoReady; static bool TryIgnoreClick(AInputEvent* event, size_t actionPointer) { int pointerId = AMotionEvent_getPointerId(event, actionPointer); int32 x = AMotionEvent_getX(event, actionPointer); int32 y = AMotionEvent_getY(event, actionPointer); //ignore key down events click was within bounds if (AndroidThunkCpp_VirtualInputIgnoreClick(x, y)) { return true; } return false; } //Called from the event process thread static int32_t HandleInputCB(struct android_app* app, AInputEvent* event) { // STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("INPUT - type: %x, action: %x, source: %x, keycode: %x, buttons: %x"), AInputEvent_getType(event), // AMotionEvent_getAction(event), AInputEvent_getSource(event), AKeyEvent_getKeyCode(event), AMotionEvent_getButtonState(event)); check(IsInAndroidEventThread()); int32 EventType = AInputEvent_getType(event); int32 EventSource = AInputEvent_getSource(event); if ((EventSource & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE) { static int32 previousButtonState = 0; const int32 device = AInputEvent_getDeviceId(event); const int32 action = AMotionEvent_getAction(event); const int32 actionType = action & AMOTION_EVENT_ACTION_MASK; int32 buttonState = AMotionEvent_getButtonState(event); if (!GAndroidEnableMouse) { if (actionType == AMOTION_EVENT_ACTION_DOWN || actionType == AMOTION_EVENT_ACTION_UP) { const bool bDown = (actionType == AMOTION_EVENT_ACTION_DOWN); if (!bDown) { buttonState = previousButtonState; } if (buttonState & AMOTION_EVENT_BUTTON_PRIMARY) { const int32 ReplacementKeyEvent = FAndroidInputInterface::GetAlternateKeyEventForMouse(device, 0); if (ReplacementKeyEvent != 0) { FAndroidInputInterface::JoystickButtonEvent(device, ReplacementKeyEvent, bDown); } } if (buttonState & AMOTION_EVENT_BUTTON_SECONDARY) { const int32 ReplacementKeyEvent = FAndroidInputInterface::GetAlternateKeyEventForMouse(device, 1); if (ReplacementKeyEvent != 0) { FAndroidInputInterface::JoystickButtonEvent(device, ReplacementKeyEvent, bDown); } } if (buttonState & AMOTION_EVENT_BUTTON_TERTIARY) { const int32 ReplacementKeyEvent = FAndroidInputInterface::GetAlternateKeyEventForMouse(device, 2); if (ReplacementKeyEvent != 0) { FAndroidInputInterface::JoystickButtonEvent(device, ReplacementKeyEvent, bDown); } } previousButtonState = buttonState; } return 1; } // FPlatformMisc::LowLevelOutputDebugStringf(TEXT("-- EVENT: %d, device: %d, action: %x, actionType: %x, buttonState: %x"), EventType, device, action, actionType, buttonState); if (actionType == AMOTION_EVENT_ACTION_DOWN || actionType == AMOTION_EVENT_ACTION_UP) { const bool bDown = (actionType == AMOTION_EVENT_ACTION_DOWN); if (!bDown) { buttonState = previousButtonState; } if (buttonState & AMOTION_EVENT_BUTTON_PRIMARY) { // FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Mouse button 0: %d"), bDown ? 1 : 0); FAndroidInputInterface::MouseButtonEvent(device, 0, bDown); } if (buttonState & AMOTION_EVENT_BUTTON_SECONDARY) { // FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Mouse button 1: %d"), bDown ? 1 : 0); FAndroidInputInterface::MouseButtonEvent(device, 1, bDown); } if (buttonState & AMOTION_EVENT_BUTTON_TERTIARY) { // FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Mouse button 2: %d"), bDown ? 1 : 0); FAndroidInputInterface::MouseButtonEvent(device, 2, bDown); } previousButtonState = buttonState; return 1; } if (actionType == AMOTION_EVENT_ACTION_SCROLL) { if (GetAxes) { float WheelDelta = GetAxes(event, AMOTION_EVENT_AXIS_VSCROLL, 0); FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Mouse scroll: %f"), WheelDelta); FAndroidInputInterface::MouseWheelEvent(device, WheelDelta); } return 1; } if (GetAxes && (actionType == AMOTION_EVENT_ACTION_MOVE || actionType == AMOTION_EVENT_ACTION_HOVER_MOVE)) { float XAbsolute = GetAxes(event, AMOTION_EVENT_AXIS_X, 0); float YAbsolute = GetAxes(event, AMOTION_EVENT_AXIS_Y, 0); float XRelative = GetAxes(event, AMOTION_EVENT_AXIS_RELATIVE_X, 0); float YRelative = GetAxes(event, AMOTION_EVENT_AXIS_RELATIVE_Y, 0); if (XRelative != 0.0f || YRelative != 0.0f) { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Mouse absolute: (%f, %f), relative (%f, %f)"), XAbsolute, YAbsolute, XRelative, YRelative); FAndroidInputInterface::MouseMoveEvent(device, XAbsolute, YAbsolute, XRelative, YRelative); } } return 1; } if (EventType == AINPUT_EVENT_TYPE_MOTION) { int action = AMotionEvent_getAction(event); int actionType = action & AMOTION_EVENT_ACTION_MASK; size_t actionPointer = (size_t)((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); bool isActionTargeted = (actionType == AMOTION_EVENT_ACTION_POINTER_DOWN || actionType == AMOTION_EVENT_ACTION_POINTER_UP); int32 device = AInputEvent_getDeviceId(event); // trap Joystick events first, with fallthrough if there is no joystick support if (((EventSource & AINPUT_SOURCE_CLASS_JOYSTICK) == AINPUT_SOURCE_CLASS_JOYSTICK) && (GetAxes != NULL) && (actionType == AMOTION_EVENT_ACTION_MOVE)) { const int axisCount = sizeof(AxisList)/sizeof(int32_t); // poll all the axes and forward to update controller state for (int axis = 0; axis < axisCount; axis++) { float val = GetAxes( event, AxisList[axis], 0); FAndroidInputInterface::JoystickAxisEvent(device, AxisList[axis], val); } // handle L/R trigger and Brake/Gas special (all in 0..1 range) // LTRIGGER will either be LTRIGGER or BRAKE, whichever is larger // RTRIGGER will either be RTRIGGER or GAS, whichever is larger float ltrigger = GetAxes(event, AMOTION_EVENT_AXIS_LTRIGGER, 0); float rtrigger = GetAxes(event, AMOTION_EVENT_AXIS_RTRIGGER, 0); float brake = GetAxes(event, AMOTION_EVENT_AXIS_BRAKE, 0); float gas = GetAxes(event, AMOTION_EVENT_AXIS_GAS, 0); FAndroidInputInterface::JoystickAxisEvent(device, AMOTION_EVENT_AXIS_LTRIGGER, ltrigger > brake ? ltrigger : brake); FAndroidInputInterface::JoystickAxisEvent(device, AMOTION_EVENT_AXIS_RTRIGGER, rtrigger > gas ? rtrigger : gas); return 1; } else { TArray TouchesArray; TouchType type = TouchEnded; switch (actionType) { case AMOTION_EVENT_ACTION_DOWN: case AMOTION_EVENT_ACTION_POINTER_DOWN: type = TouchBegan; break; case AMOTION_EVENT_ACTION_MOVE: type = TouchMoved; break; case AMOTION_EVENT_ACTION_UP: case AMOTION_EVENT_ACTION_POINTER_UP: case AMOTION_EVENT_ACTION_CANCEL: case AMOTION_EVENT_ACTION_OUTSIDE: type = TouchEnded; break; case AMOTION_EVENT_ACTION_SCROLL: case AMOTION_EVENT_ACTION_HOVER_ENTER: case AMOTION_EVENT_ACTION_HOVER_MOVE: case AMOTION_EVENT_ACTION_HOVER_EXIT: return 0; default: UE_LOG(LogAndroid, Verbose, TEXT("Unknown AMOTION_EVENT %d ignored"), actionType); return 0; } size_t pointerCount = AMotionEvent_getPointerCount(event); if (pointerCount == 0) { return 1; } ANativeWindow* Window = (ANativeWindow*)FAndroidWindow::GetHardwareWindow_EventThread(); if (!Window) { return 0; } int32_t Width = 0; int32_t Height = 0; if (Window) { // we are on the event thread. true here indicates we will retrieve dimensions from the current window. FAndroidWindow::CalculateSurfaceSize(Width, Height, true); } // make sure OpenGL context created before accepting touch events.. FAndroidWindow::GetScreenRect() may try to create it early from wrong thread if this is the first call if (!GAndroidGPUInfoReady) { return 1; } FPlatformRect ScreenRect = FAndroidWindow::GetScreenRect(true); if (AndroidThunkCpp_IsVirtualKeyboardShown() && (type == TouchBegan || type == TouchMoved)) { //ignore key down events when the native input was clicked or when the keyboard animation is playing if (TryIgnoreClick(event, actionPointer)) { return 0; } } else if ((AndroidThunkCpp_IsWebViewShown() || AndroidThunkCpp_HasExtraIgnoreView()) && (type == TouchBegan || type == TouchMoved || type == TouchEnded)) { //ignore key down events when the the a web view is visible if (TryIgnoreClick(event, actionPointer) && ((EventSource & 0x80) != 0x80)) { UE_LOG(LogAndroid, Verbose, TEXT("Received touch event %d - Ignored"), type); return 0; } UE_LOG(LogAndroid, Verbose, TEXT("Received touch event %d"), type); } if (isActionTargeted) { if (actionPointer < 0 || pointerCount < (int)actionPointer) { return 1; } int pointerId = AMotionEvent_getPointerId(event, actionPointer); float x = FMath::Min(AMotionEvent_getX(event, actionPointer) / Width, 1.f); x *= (ScreenRect.Right - 1); float y = FMath::Min(AMotionEvent_getY(event, actionPointer) / Height, 1.f); y *= (ScreenRect.Bottom - 1); UE_LOG(LogAndroid, Verbose, TEXT("Received targeted motion event from pointer %u (id %d) action %d: (%.2f, %.2f)"), actionPointer, pointerId, action, x, y); TouchInput TouchMessage; TouchMessage.DeviceId = device; TouchMessage.Handle = pointerId; TouchMessage.Type = type; TouchMessage.Position = FVector2D(x, y); TouchMessage.LastPosition = FVector2D(x, y); //@todo android: AMotionEvent_getHistoricalRawX TouchesArray.Add(TouchMessage); } else { for (size_t i = 0; i < pointerCount; ++i) { int pointerId = AMotionEvent_getPointerId(event, i); float x = FMath::Min(AMotionEvent_getX(event, i) / Width, 1.f); x *= (ScreenRect.Right - 1); float y = FMath::Min(AMotionEvent_getY(event, i) / Height, 1.f); y *= (ScreenRect.Bottom - 1); UE_LOG(LogAndroid, Verbose, TEXT("Received motion event from index %u (id %d) action %d: (%.2f, %.2f)"), i, pointerId, action, x, y); TouchInput TouchMessage; TouchMessage.DeviceId = device; TouchMessage.Handle = pointerId; TouchMessage.Type = type; TouchMessage.Position = FVector2D(x, y); TouchMessage.LastPosition = FVector2D(x, y); //@todo android: AMotionEvent_getHistoricalRawX TouchesArray.Add(TouchMessage); } } FAndroidInputInterface::QueueTouchInput(TouchesArray); #if !UE_BUILD_SHIPPING if ((pointerCount >= 4) && (type == TouchBegan)) { bool bShowConsole = true; GConfig->GetBool(TEXT("/Script/Engine.InputSettings"), TEXT("bShowConsoleOnFourFingerTap"), bShowConsole, GInputIni); if (bShowConsole) { GShowConsoleWindowNextTick = true; } } #endif } return 0; } if (EventType == AINPUT_EVENT_TYPE_KEY) { int keyCode = AKeyEvent_getKeyCode(event); int keyFlags = AKeyEvent_getFlags(event); bool bSoftKey = (keyFlags & AKEY_EVENT_FLAG_SOFT_KEYBOARD) != 0; STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Received keycode: %d, softkey: %d"), keyCode, bSoftKey ? 1 : 0); //Only pass on the device id if really a gamepad, joystick or dpad (allows menu and back to be treated as gamepad events) int32 device = -1; if ((((EventSource & AINPUT_SOURCE_JOYSTICK) == AINPUT_SOURCE_JOYSTICK) && (GetAxes != NULL)) || ((EventSource & AINPUT_SOURCE_GAMEPAD) == AINPUT_SOURCE_GAMEPAD) || ((EventSource & AINPUT_SOURCE_DPAD) == AINPUT_SOURCE_DPAD)) { device = AInputEvent_getDeviceId(event); } //Trap codes handled as possible gamepad events if (device >= 0 && ValidGamepadKeyCodes.Contains(keyCode)) { bool down = AKeyEvent_getAction(event) != AKEY_EVENT_ACTION_UP; FAndroidInputInterface::JoystickButtonEvent(device, keyCode, down); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Received gamepad button: %d"), keyCode); } else { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Received key event: %d"), keyCode); // only handle mapped key codes if (!MappedKeyCodes.Contains(keyCode)) { return 0; } // forward key presses to UI if needed if (AndroidThunkCpp_IsVirtualKeyboardShown()) { return 0; } if (bSoftKey || (GAndroidEnableHardwareKeyboard && !AndroidThunkCpp_IsVirtualKeyboardShown()) || AlwaysAllowedKeyCodes.Contains(keyCode)) { FDeferredAndroidMessage Message; Message.messageType = AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_UP ? MessageType_KeyUp : MessageType_KeyDown; Message.KeyEventData.unichar = keyCode; Message.KeyEventData.keyId = keyCode; Message.KeyEventData.modifier = AKeyEvent_getMetaState(event); Message.KeyEventData.isRepeat = AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_MULTIPLE; FAndroidInputInterface::DeferMessage(Message); } // allow event to be generated for volume up and down, but conditionally allow system to handle it, too if (keyCode == AKEYCODE_VOLUME_UP || keyCode == AKEYCODE_VOLUME_DOWN) { if (FPlatformMisc::GetVolumeButtonsHandledBySystem()) { return 0; } } // optionally forward back button if (keyCode == AKEYCODE_BACK && GAllowJavaBackButtonEvent) { return 0; } } return 1; } return 0; } static bool IsStartupMoviePlaying() { return GEngine && GEngine->IsInitialized() && GetMoviePlayer() && GetMoviePlayer()->IsStartupMoviePlaying(); } static bool IsPreLoadScreenPlaying() { return IsStartupMoviePlaying() || (FPreLoadScreenManager::Get() && (FPreLoadScreenManager::Get()->HasValidActivePreLoadScreen())); } FAppEventData::FAppEventData(ANativeWindow* WindowIn) { if (WindowIn == nullptr) { UE_LOG(LogAndroid, Log, TEXT("FAppEventData was passed a NULL WindowsIn pointer.")); return; } //check(WindowIn); if (GAndroidWindowOverride != nullptr ) { GSurfaceViewWidth = WindowWidth = ANativeWindow_getWidth((ANativeWindow*)GAndroidWindowOverride); GSurfaceViewHeight = WindowHeight = ANativeWindow_getHeight((ANativeWindow*)GAndroidWindowOverride); } else { GSurfaceViewWidth = WindowWidth = ANativeWindow_getWidth(WindowIn); GSurfaceViewHeight = WindowHeight = ANativeWindow_getHeight(WindowIn); } if (WindowWidth <= 0) { UE_LOG(LogAndroid, Log, TEXT("FAppEventData was passed a window with ZERO Height.")); WindowWidth = 1; } if (WindowHeight <= 0) { UE_LOG(LogAndroid, Log, TEXT("FAppEventData was passed a window with ZERO Height.")); WindowHeight = 1; } //check(WindowWidth >= 0 && WindowHeight >= 0); } static void ActivateApp_EventThread() { DEVELOPER_LOG_COMMANDCB_CASE(ActivateApp_EventThread); if (bAppIsActive_EventThread) { return; } if (!FAndroidMisc::UseNewWindowBehavior()) { // Unlock window when we're ready. UE_LOG(LogAndroid, Log, TEXT("event thread, activate app, unlocking HW window")); GAndroidWindowLock.Unlock(); } // wake the GT up. FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_APP_ACTIVATED); bAppIsActive_EventThread = true; UE::GC::Private::SetSkipDestroy(false); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("event thread, activate app, UNLOCKED HW window lock. bReadyToProcessEvents=%d, bAppIsActive_EventThread=%d"), bReadyToProcessEvents, bAppIsActive_EventThread); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_RUN_CALLBACK, FAppEventData([]() { UE_LOG(LogAndroid, Log, TEXT("performing app foregrounding callback.")); FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Broadcast(); FCoreDelegates::ApplicationHasReactivatedDelegate.Broadcast(); FAppEventManager::GetInstance()->ResumeAudio(); })); if (EventHandlerEvent) { // Must flush the queue before enabling rendering. EventHandlerEvent->Trigger(); } if (FThreadHeartBeat* ThreadHeartBeat = FThreadHeartBeat::GetNoInit()) { ThreadHeartBeat->ResumeHeartBeat(true); } FPreLoadScreenManager::EnableRendering(true); extern void AndroidThunkCpp_ShowHiddenAlertDialog(); AndroidThunkCpp_ShowHiddenAlertDialog(); } extern void BlockRendering(); // called when the app has window with the new behavior model. static void OnNewWindow_EventThread(ANativeWindow* NewNativeWindow, ANativeWindow* OldNativeWindow) { // Unlock window when we're ready. UE_LOG(LogAndroid, Log, TEXT("event thread, new window")); TOptional WindowAccessor = FAndroidWindowManager::Get().FindFromANativeWindow(true, true, OldNativeWindow); if (WindowAccessor.IsSet()) { // Update the window handle, this will be used by the RHI callback. WindowAccessor->SetANativeWindow(NewNativeWindow); const auto& OnReinitWindowCallback = UE::FAndroidPlatformDynamicRHI::GetRHIOnReInitWindowCallback(); if (OnReinitWindowCallback) { OnReinitWindowCallback(MoveTemp(WindowAccessor)); } } else { // no window as yet, save the native handle for the next window create. FAndroidWindowManager::GetPendingWindowAccessor().SetANativeWindow(NewNativeWindow); } } // called whenever the app loses window with the new behavior model. void OnLostWindow_EventThread(ANativeWindow* OutgoingWindow) { TOptional WindowAccessor = FAndroidWindowManager::Get().FindFromANativeWindow(true, false, OutgoingWindow); if (WindowAccessor.IsSet()) { WindowAccessor->SetANativeWindow(nullptr); const auto& OnReleaseWindowCallback = UE::FAndroidPlatformDynamicRHI::GetRHIOnReleaseWindowCallback(); if(OnReleaseWindowCallback) { OnReleaseWindowCallback(MoveTemp(WindowAccessor)); } } else { FAndroidWindowManager::GetPendingWindowAccessor().SetANativeWindow(nullptr); } } // called whenever the app loses window or pause. static void SuspendApp_EventThread() { DEVELOPER_LOG_COMMANDCB_CASE(SuspendApp_EventThread); if (!bAppIsActive_EventThread) { UE_LOG(LogAndroid, Warning, TEXT("SuspendApp_EventThread -> event thread, suspending app, bAppIsActive_EventThread is false so early return!")); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("SuspendApp_EventThread -> UNEXPECTED: event thread, SuspendApp_EventThread called but aborted, bReadyToProcessEvents=%d, bAppIsActive_EventThread=%d, bHasFocus=%d"), bReadyToProcessEvents, bAppIsActive_EventThread, bHasFocus); return; } bAppIsActive_EventThread = false; UE::GC::Private::SetSkipDestroy(true); // Lock the window, this prevents event thread from removing the window whilst the RHI initializes. if (!FAndroidMisc::UseNewWindowBehavior()) { UE_LOG(LogAndroid, Log, TEXT("SuspendApp_EventThread -> event thread, suspending app, acquiring HW window lock.")); GAndroidWindowLock.Lock(); } STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("SuspendApp_EventThread -> event thread, suspending app, acquiring HW window lock. bReadyToProcessEvents=%d, bAppIsActive_EventThread=%d"), bReadyToProcessEvents, bAppIsActive_EventThread); if (bReadyToProcessEvents == false) { // App has stopped before we can process events. // AndroidLaunch will lock GAndroidWindowLock, and set bReadyToProcessEvents when we are able to block the RHI and queue up other events. // we ignore events until this point as acquiring GAndroidWindowLock means requires the window to be properly initialized. UE_LOG(LogAndroid, Warning, TEXT("SuspendApp_EventThread -> UNEXPECTED: event thread, app not yet ready.")); return; } STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("SuspendApp_EventThread -> event thread, suspending app, ACQUIRED HW window lock. bReadyToProcessEvents=%d, bAppIsActive_EventThread=%d"), bReadyToProcessEvents, bAppIsActive_EventThread); TSharedPtr EMDoneTrigger = MakeShareable(FPlatformProcess::GetSynchEventFromPool(), [](FEvent* EventToDelete) { FPlatformProcess::ReturnSynchEventToPool(EventToDelete); }); // perform the delegates before the window handle is cleared. // This ensures any tasks that require a window handle will have it before we block the RT on the invalid window. FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_RUN_CALLBACK, FAppEventData([EMDoneTrigger]() { UE_LOG(LogAndroid, Log, TEXT("SuspendApp_EventThread -> performing app backgrounding callback. %p"), EMDoneTrigger.Get()); FCoreDelegates::ApplicationWillDeactivateDelegate.Broadcast(); FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Broadcast(); FAppEventManager::GetInstance()->PauseAudio(); FAppEventManager::ReleaseMicrophone(false); EMDoneTrigger->Trigger(); })); #if USE_ANDROID_ALTERNATIVE_SUSPEND // Suspend the GT. FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_APP_SUSPENDED); #endif uint32 StartCycles = FPlatformTime::Cycles(); FEmbeddedCommunication::WakeGameThread(); FPreLoadScreenManager::EnableRendering(false); FThreadHeartBeat::Get().SuspendHeartBeat(true); if (!FAndroidMisc::UseNewWindowBehavior()) { // wait for a period of time before blocking rendering UE_LOG(LogAndroid, Log, TEXT("SuspendApp_EventThread -> , waiting for event manager to process. tid: %d"), FPlatformTLS::GetCurrentThreadId()); bool bSuccess = EMDoneTrigger->Wait(4000); float ElapsedTimeInMs_EMDoneTrigger_Wait = FPlatformTime::ToMilliseconds(FPlatformTime::Cycles() - StartCycles); UE_CLOG(!bSuccess, LogAndroid, Log, TEXT("SuspendApp_EventThread -> ERROR: backgrounding callback, not responded in timely manner. EMDoneTrigger->Wait, waited '%f' ms"), (float)ElapsedTimeInMs_EMDoneTrigger_Wait); UE_LOG(LogAndroid, Log, TEXT("SuspendApp_EventThread -> EMDoneTrigger->Wait, waited '%f' ms"), (float)ElapsedTimeInMs_EMDoneTrigger_Wait); BlockRendering(); } // Suspend the GT. #if !USE_ANDROID_ALTERNATIVE_SUSPEND FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_APP_SUSPENDED); #endif UE_LOG(LogAndroid, Log, TEXT("SuspendApp_EventThread(EOF)")); } //Called from the event process thread static void OnAppCommandCB(struct android_app* app, int32_t cmd) { check(IsInAndroidEventThread()); // Since native app can only have one window we can track of the last handle. static ANativeWindow* LastDestroyedANativeWindow = nullptr; // Set event thread's view of the window dimensions: { ANativeWindow* DimensionWindow = app->pendingWindow ? app->pendingWindow : app->window; if (DimensionWindow) { FAndroidWindow::SetWindowDimensions_EventThread(DimensionWindow); } } switch (cmd) { case APP_CMD_SAVE_STATE: /** * Command from main thread: the app should generate a new saved state * for itself, to restore from later if needed. If you have saved state, * allocate it with malloc and place it in android_app.savedState with * the size in android_app.savedStateSize. The will be freed for you * later. */ // the OS asked us to save the state of the app DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_SAVE_STATE); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_SAVE_STATE")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_SAVE_STATE); break; case APP_CMD_INIT_WINDOW: /** * Command from main thread: a new ANativeWindow is ready for use. Upon * receiving this command, android_app->window will contain the new window * surface. */ // get the window ready for showing DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_INIT_WINDOW); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_INIT_WINDOW")); if (FAndroidMisc::UseNewWindowBehavior()) { OnNewWindow_EventThread(app->window, LastDestroyedANativeWindow); } #if USE_ANDROID_STANDALONE { ANativeWindow* win = app->pendingWindow ? app->pendingWindow : app->window; STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Case APP_CMD_INIT_WINDOW(STANDALONE), app->pendingWindow=%p, app->window=%p, GAndroidWindowOverride=%p"), app->pendingWindow, app->window, GAndroidWindowOverride); if (GAndroidWindowOverride != NULL) { win = (ANativeWindow*)GAndroidWindowOverride; } checkf(win != NULL, TEXT("Engine APP_CMD_INIT_WINDOW with STANDALONE Failed since win is NULL!")); FAppEventManager::GetInstance()->HandleWindowCreated_EventThread((ANativeWindow*)win); bHasWindow = win != nullptr; } #else STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Case APP_CMD_INIT_WINDOW")); FAppEventManager::GetInstance()->HandleWindowCreated_EventThread(app->pendingWindow); bHasWindow = true; #endif UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_INIT_WINDOW: bHasWindow=%d, bHasFocus=%d, bIsResumed=%d"), bHasWindow, bHasFocus, bIsResumed); if (!FAndroidMisc::UseNewWindowBehavior()) { if (bHasWindow && bHasFocus && bIsResumed) { ActivateApp_EventThread(); } } break; case APP_CMD_TERM_WINDOW: /** * Command from main thread: the existing ANativeWindow needs to be * terminated. Upon receiving this command, android_app->window still * contains the existing window; after calling android_app_exec_cmd * it will be set to NULL. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_TERM_WINDOW); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Case APP_CMD_TERM_WINDOW, tid = %d"), gettid()); // clean up the window because it is being hidden/closed UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_TERM_WINDOW")); LastDestroyedANativeWindow = app->window; if (FAndroidMisc::UseNewWindowBehavior()) { OnLostWindow_EventThread(app->window); } else { SuspendApp_EventThread(); } FAppEventManager::GetInstance()->HandleWindowClosed_EventThread(); bHasWindow = false; break; case APP_CMD_LOST_FOCUS: /** * Command from main thread: the app's activity window has lost * input focus. */ // if the app lost focus, avoid unnecessary processing (like monitoring the accelerometer) // log it, but the actual event will be simulated later in APP_CMD_PAUSE DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_LOST_FOCUS); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_LOST_FOCUS")); break; case APP_CMD_GAINED_FOCUS: /** * Command from main thread: the app's activity window has gained * input focus. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_GAINED_FOCUS); // bring back a certain functionality, like monitoring the accelerometer // log it, but the actual event will be simulated later in APP_CMD_RESUME UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_GAINED_FOCUS")); // still check for a rare case needing activation if ((bHasWindow || FAndroidMisc::UseNewWindowBehavior()) && bHasFocus && bIsResumed) { ActivateApp_EventThread(); } break; case APP_CMD_INPUT_CHANGED: UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_INPUT_CHANGED")); break; case APP_CMD_WINDOW_RESIZED: /** * Command from main thread: the current ANativeWindow has been resized. * Please redraw with its new size. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_WINDOW_RESIZED); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_WINDOW_RESIZED")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_RESIZED, FAppEventData(app->window)); break; case APP_CMD_WINDOW_REDRAW_NEEDED: /** * Command from main thread: the system needs that the current ANativeWindow * be redrawn. You should redraw the window before handing this to * android_app_exec_cmd() in order to avoid transient drawing glitches. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_WINDOW_REDRAW_NEEDED); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_WINDOW_REDRAW_NEEDED")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_REDRAW_NEEDED); break; case APP_CMD_CONTENT_RECT_CHANGED: /** * Command from main thread: the content area of the window has changed, * such as from the soft input window being shown or hidden. You can * find the new content rect in android_app::contentRect. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_CONTENT_RECT_CHANGED); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_CONTENT_RECT_CHANGED")); break; /* receive this event from Java instead to work around NDK bug with AConfiguration_getOrientation in Oreo case APP_CMD_CONFIG_CHANGED: { // Command from main thread: the current device configuration has changed. UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_CONFIG_CHANGED")); bool bPortrait = (AConfiguration_getOrientation(app->config) == ACONFIGURATION_ORIENTATION_PORT); if (FAndroidWindow::OnWindowOrientationChanged(bPortrait)) { FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_CHANGED); } } break; */ case APP_CMD_LOW_MEMORY: /** * Command from main thread: the system is running low on memory. * Try to reduce your memory use. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_LOW_MEMORY); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_LOW_MEMORY")); break; case APP_CMD_START: /** * Command from main thread: the app's activity has been started. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_START); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_START")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_ON_START); break; case APP_CMD_RESUME: /** * Command from main thread: the app's activity has been resumed. */ bIsResumed = true; // assume focus on resume bHasFocus = true; DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_RESUME); if ((bHasWindow || FAndroidMisc::UseNewWindowBehavior()) && bHasFocus && bIsResumed) { ActivateApp_EventThread(); } STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Case APP_CMD_RESUME, bShouldRestartFromInterrupt=%d"), bShouldRestartFromInterrupt); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_RESUME")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_ON_RESUME); // trigger focus FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_GAINED_FOCUS); /* * On the initial loading the restart method must be called immediately * in order to restart the app if the startup movie was playing */ if (bShouldRestartFromInterrupt) { AndroidThunkCpp_RestartApplication(TEXT("")); } break; case APP_CMD_PAUSE: { /** * Command from main thread: the app's activity has been paused. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_PAUSE); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Case APP_CMD_PAUSE")); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_PAUSE")); // simulate lost focus FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_LOST_FOCUS); bHasFocus = false; // Ignore pause command for Oculus if the window hasn't been initialized to prevent halting initial load // if the headset is not active if (!bHasWindow && FAndroidMisc::GetDeviceMake() == FString("Oculus")) { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Oculus: Ignoring APP_CMD_PAUSE command before APP_CMD_INIT_WINDOW")); UE_LOG(LogAndroid, Log, TEXT("Oculus: Ignoring APP_CMD_PAUSE command before APP_CMD_INIT_WINDOW")); break; } bIsResumed = false; FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_ON_PAUSE); bool bAllowReboot = true; #if FAST_BOOT_HACKS if (FEmbeddedDelegates::GetNamedObject(TEXT("LoggedInObject")) == nullptr) { bAllowReboot = false; } #endif // Restart on resuming if did not complete engine initialization if (!bDidCompleteEngineInit && !bIgnorePauseOnDownloaderStart && bAllowReboot) { // // only do this if early startup enabled // FString *EarlyRestart = FAndroidMisc::GetConfigRulesVariable(TEXT("earlyrestart")); // if (EarlyRestart != NULL && EarlyRestart->Equals("true", ESearchCase::IgnoreCase)) // { // bShouldRestartFromInterrupt = true; // } } bIgnorePauseOnDownloaderStart = false; /* * On the initial loading the pause method must be called immediately * in order to stop the startup movie's sound */ if (IsPreLoadScreenPlaying() && bAllowReboot) { UE_LOG(LogAndroid, Log, TEXT("MoviePlayer force completion")); GetMoviePlayer()->ForceCompletion(); } SuspendApp_EventThread(); break; } case APP_CMD_STOP: /** * Command from main thread: the app's activity has been stopped. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_STOP); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_STOP")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_ON_STOP); break; case APP_CMD_DESTROY: /** * Command from main thread: the app's activity is being destroyed, * and waiting for the app thread to clean up and exit before proceeding. */ DEVELOPER_LOG_COMMANDCB_CASE(APP_CMD_DESTROY); UE_LOG(LogAndroid, Log, TEXT("Case APP_CMD_DESTROY")); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_RUN_CALLBACK, FAppEventData([]() { FGraphEventRef WillTerminateTask = FFunctionGraphTask::CreateAndDispatchWhenReady([]() { PRAGMA_DISABLE_DEPRECATION_WARNINGS FCoreDelegates::ApplicationWillTerminateDelegate.Broadcast(); PRAGMA_ENABLE_DEPRECATION_WARNINGS FCoreDelegates::GetApplicationWillTerminateDelegate().Broadcast(); }, TStatId(), NULL, ENamedThreads::GameThread); FTaskGraphInterface::Get().WaitUntilTaskCompletes(WillTerminateTask); FAndroidMisc::NonReentrantRequestExit(); })); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_ON_DESTROY); // Exit here, avoids having to unlock the window and letting the RHI's deal with invalid window. extern void AndroidThunkCpp_ForceQuit(); AndroidThunkCpp_ForceQuit(); break; } if (EventHandlerEvent) { EventHandlerEvent->Trigger(); } //FPlatformMisc::LowLevelOutputDebugStringf(TEXT("#### END OF OnAppCommandCB cmd: %u, tid = %d"), cmd, gettid()); } //Native-defined functions JNI_METHOD jint Java_com_epicgames_unreal_GameActivity_nativeGetCPUFamily(JNIEnv* jenv, jobject thiz) { return (jint)android_getCpuFamily(); } JNI_METHOD jboolean Java_com_epicgames_unreal_GameActivity_nativeSupportsNEON(JNIEnv* jenv, jobject thiz) { AndroidCpuFamily Family = android_getCpuFamily(); if (Family == ANDROID_CPU_FAMILY_ARM64) { return JNI_TRUE; } if (Family == ANDROID_CPU_FAMILY_ARM) { return ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) ? JNI_TRUE : JNI_FALSE; } return JNI_FALSE; } //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeOnOrientationChanged(int orientation); JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeOnOrientationChanged(JNIEnv* jenv, jobject thiz, jint orientation) { // enqueue a window changed event if orientation changed, // note that the HW window handle does not necessarily change. if (FAndroidWindow::OnWindowOrientationChanged((EDeviceScreenOrientation)orientation)) { // Enqueue an event to trigger gamethread to update the orientation: FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_WINDOW_CHANGED); FAppEventManager::GetInstance()->EnqueueAppEvent(APP_EVENT_STATE_SAFE_ZONE_UPDATED); if (EventHandlerEvent) { EventHandlerEvent->Trigger(); } } } //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeConsoleCommand(String commandString);" JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeConsoleCommand(JNIEnv* jenv, jobject thiz, jstring commandString) { FString Command = FJavaHelper::FStringFromParam(jenv, commandString); #if !USE_ANDROID_STANDALONE if (GEngine != NULL) { // Run on game thread to avoid race condition with DeferredCommands AsyncTask(ENamedThreads::GameThread, [Command]() { GEngine->DeferredCommands.Add(Command); }); } else { FPlatformMisc::LowLevelOutputDebugStringf(TEXT("UNEXPECTED -> Ignoring console command (too early): %s"), *Command); } #else IssueConsoleCommand(Command); #endif } // This is called from the Java UI thread for initializing VR HMDs JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeInitHMDs(JNIEnv* jenv, jobject thiz) { for (auto HMDModuleIt = GHMDImplementations.CreateIterator(); HMDModuleIt; ++HMDModuleIt) { (*HMDModuleIt)->PreInit(); } GHMDsInitialized = true; } JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeSetAndroidVersionInformation(JNIEnv* jenv, jobject thiz, jstring androidVersion, jint targetSDKversion, jstring phoneMake, jstring phoneModel, jstring phoneBuildNumber, jstring osLanguage, jstring productName) { auto UEAndroidVersion = FJavaHelper::FStringFromParam(jenv, androidVersion); auto UEPhoneMake = FJavaHelper::FStringFromParam(jenv, phoneMake); auto UEPhoneModel = FJavaHelper::FStringFromParam(jenv, phoneModel); auto UEPhoneBuildNumber = FJavaHelper::FStringFromParam(jenv, phoneBuildNumber); auto UEOSLanguage = FJavaHelper::FStringFromParam(jenv, osLanguage); auto UEProductName = FJavaHelper::FStringFromParam(jenv, productName); FAndroidMisc::SetVersionInfo(UEAndroidVersion, targetSDKversion, UEPhoneMake, UEPhoneModel, UEPhoneBuildNumber, UEOSLanguage, UEProductName); } //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeOnInitialDownloadStarted(); JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeOnInitialDownloadStarted(JNIEnv* jenv, jobject thiz) { bIgnorePauseOnDownloaderStart = true; } //This function is declared in the Java-defined class, GameActivity.java: "public native void nativeOnInitialDownloadCompleted(); JNI_METHOD void Java_com_epicgames_unreal_GameActivity_nativeOnInitialDownloadCompleted(JNIEnv* jenv, jobject thiz) { bIgnorePauseOnDownloaderStart = false; } // MERGE-TODO: Anticheat concerns with custom input bool GAllowCustomInput = true; JNI_METHOD void Java_com_epicgames_unreal_NativeCalls_HandleCustomTouchEvent(JNIEnv* jenv, jobject thiz, jint deviceId, jint pointerId, jint action, jint soucre, jfloat x, jfloat y) { #if ANDROID_ALLOWCUSTOMTOUCHEVENT // make sure fake input is allowed, so hacky Java can't run bots if (!GAllowCustomInput) { return; } TArray TouchesArray; TouchInput TouchMessage; TouchMessage.DeviceId = deviceId; TouchMessage.Handle = pointerId; switch (action) { case 0: // MotionEvent.ACTION_DOWN TouchMessage.Type = TouchBegan; break; case 2: // MotionEvent.ACTION_MOVE TouchMessage.Type = TouchMoved; break; default: // MotionEvent.ACTION_UP TouchMessage.Type = TouchEnded; break; } TouchMessage.Position = FVector2D(x, y); TouchMessage.LastPosition = FVector2D(x, y); TouchesArray.Add(TouchMessage); UE_LOG(LogAndroid, Verbose, TEXT("Handle custom touch event %d (%d) x=%f y=%f"), TouchMessage.Type, action, x, y); FAndroidInputInterface::QueueTouchInput(TouchesArray); #endif } JNI_METHOD void Java_com_epicgames_unreal_NativeCalls_AllowJavaBackButtonEvent(JNIEnv* jenv, jobject thiz, jboolean allow) { GAllowJavaBackButtonEvent = (allow == JNI_TRUE); } bool WaitForAndroidLoseFocusEvent(double TimeoutSeconds) { return FAppEventManager::GetInstance()->WaitForEventInQueue(EAppEventState::APP_EVENT_STATE_WINDOW_LOST_FOCUS, TimeoutSeconds); } #if USE_ANDROID_STANDALONE pthread_t G_AndroidMainThread; static struct android_app dummy_state; #define EVENTTYPE_INIT 0 #define EVENTTYPE_POST_ENGINE_INIT 1 #define EVENTTYPE_ENGINELOOP_INIT_COMPLETE 2 #define EVENTTYPE_FRAME_BEGIN 3 #define EVENTTYPE_FRAME_END 4 #define EVENTTYPE_PRE_LOAD_MAP 5 #define EVENTTYPE_POST_LOAD_MAP 6 #define EVENTTYPE_ACTION 7 #define EVENTTYPE_ACTIVITYCHANGED 8 #define EVENTTYPE_APP_SUSPENDED 9 static const TCHAR* eAppCommandNamed[] = { TEXT("APP_CMD_INPUT_CHANGED"), TEXT("APP_CMD_INIT_WINDOW"), TEXT("APP_CMD_TERM_WINDOW"), TEXT("APP_CMD_WINDOW_RESIZED"), TEXT("APP_CMD_WINDOW_REDRAW_NEEDED"), TEXT("APP_CMD_CONTENT_RECT_CHANGED"), TEXT("APP_CMD_GAINED_FOCUS"), TEXT("APP_CMD_LOST_FOCUS"), TEXT("APP_CMD_CONFIG_CHANGED"), TEXT("APP_CMD_LOW_MEMORY"), TEXT("APP_CMD_START"), TEXT("APP_CMD_RESUME"), TEXT("APP_CMD_SAVE_STATE"), TEXT("APP_CMD_PAUSE"), TEXT("APP_CMD_STOP"), TEXT("APP_CMD_DESTROY"), TEXT("APP_CMD_UNKNOWN") }; static const TCHAR* get_APP_CMD_String(uint32 cmd) { check(cmd <= 15); return (cmd <= 15) ? eAppCommandNamed[cmd] : eAppCommandNamed[16]; } void AndroidThunkCpp_Engine_SendEvent(int32 event, const FString& param1, int32 param2, int32 param3, float param4) { if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) { static jclass EngineClass = FAndroidApplication::FindJavaClassGlobalRef("com/epicgames/asis/Engine"); static jmethodID SendEventFunc = FJavaWrapper::FindStaticMethod(Env, EngineClass, "AndroidThunkJava_Engine_ReceiveEvent", "(ILjava/lang/String;IIF)V", false); if (SendEventFunc != nullptr) { auto StringParam = FJavaHelper::ToJavaString(Env, param1); Env->CallStaticVoidMethod(EngineClass, SendEventFunc, event, *StringParam, param2, param3, param4); } else { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidThunkCpp_Engine_SendEvent WARNING: SendEventFunc not found")); } } else { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidThunkCpp_Engine_SendEvent ERROR: FAndroidApplication::GetJavaEnv() == null")); } } static void InitEvent() { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("LaunchAndroid received InitEvent")); AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_INIT, TEXT(""), 0, 0, 0.0f); } static void AppSuspended() { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("LaunchAndroid received AppSuspended, GFrameCounter=%d"), (int32)GFrameCounter); AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_APP_SUSPENDED, TEXT(""), (int32)GFrameCounter, 0, 0.0f); } static void PostEngineInitEvent() { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("LaunchAndroid received PostEngineInitEvent")); AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_POST_ENGINE_INIT, TEXT(""), 0, 0, 0.0f); } static void EngineLoopInitCompleteEvent() { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("LaunchAndroid received EngineLoopInitCompleteEvent")); AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_ENGINELOOP_INIT_COMPLETE, TEXT(""), 0, 0, 0.0f); } static void BeginFrameEvent() { AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_FRAME_BEGIN, TEXT(""), (int32)GFrameCounter, 0, 0.0f); } static void EndFrameEvent() { AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_FRAME_END, TEXT(""), (int32)(GFrameCounter - 1), 0, 0.0f); } static void PreLoadMapEvent(const FString& Param) { AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_PRE_LOAD_MAP, Param, 0, 0, 0.0f); } static void PostLoadMapEvent(UWorld* World) { if (World != nullptr) { FString LevelName = World->GetMapName(); AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_POST_LOAD_MAP, LevelName, 0, 0, 0.0f); } else { AndroidThunkCpp_Engine_SendEvent(EVENTTYPE_POST_LOAD_MAP, TEXT(""), 0, 0, 0.0f); } } JNI_METHOD void Java_com_epicgames_asis_AsisGameActivity_nativeSetCommandline(JNIEnv* jenv, jobject thiz, jstring commandline) { auto UECommandLine = FJavaHelper::FStringFromParam(jenv, commandline); GAndroidCommandLine = UECommandLine; } JNI_METHOD void Java_com_epicgames_asis_AsisGameActivity_nativeMain(JNIEnv* jenv, jobject thiz, jstring projectModule) { BootTimingPoint("native_main"); GIsGameAgnosticExe = true; FString projectModuleName = FJavaHelper::FStringFromParam(jenv, projectModule); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Java_com_epicgames_asis_AsisGameActivity_nativeMain : use current!, requesting ProjectModule: %s"), *projectModuleName); if (EventThreadID == 0) { FTaskTagScope::SwapTag(ETaskTag::EEventThread); EventThreadID = FPlatformTLS::GetCurrentThreadId(); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AndroidMain set current EventThreadID=%d"), EventThreadID); } // register some delegates we want to pass back FCoreDelegates::OnInit.AddStatic(InitEvent); FCoreDelegates::OnPostEngineInit.AddStatic(PostEngineInitEvent); FCoreDelegates::OnFEngineLoopInitComplete.AddStatic(EngineLoopInitCompleteEvent); FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddStatic(AppSuspended); FCoreDelegates::OnBeginFrame.AddStatic(BeginFrameEvent); FCoreDelegates::OnEndFrame.AddStatic(EndFrameEvent); FCoreUObjectDelegates::PreLoadMap.AddStatic(PreLoadMapEvent); FCoreUObjectDelegates::PostLoadMapWithWorld.AddStatic(PostLoadMapEvent); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Entering engine main function")); dummy_state.activity = nullptr; pthread_attr_t otherAttr; pthread_attr_init(&otherAttr); pthread_attr_setdetachstate(&otherAttr, PTHREAD_CREATE_DETACHED); pthread_create(&G_AndroidMainThread, &otherAttr, AndroidMain, &dummy_state); STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("Created main thread")); } static bool MountPak(const FString& PakFilePath, int32 PakOrder, const FString& PakMountPoint) { // Mount all pak files found in this content FPakPlatformFile* PakFileMgr = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(TEXT("PakFile"))); if (PakFileMgr == nullptr) { return false; } if (!PakMountPoint.IsEmpty()) { const TCHAR* MountPoint = PakMountPoint.GetCharArray().GetData(); return PakFileMgr->Mount(*PakFilePath, PakOrder, MountPoint); } return PakFileMgr->Mount(*PakFilePath, PakOrder); } JNI_METHOD bool Java_com_epicgames_asis_Engine_nativeMountPak(JNIEnv* jenv, jobject thiz, jstring pakFile, jint order, jstring mountPoint) { FString PakFilePath = FJavaHelper::FStringFromParam(jenv, pakFile); uint32 PakOrder = (uint32)FMath::Max(0, order); FString PakMountPoint = FJavaHelper::FStringFromParam(jenv, mountPoint); if (!IsInGameThread() && FTaskGraphInterface::IsRunning()) { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("nativeMountPak: deferred '%s'"), *PakFilePath); FSimpleDelegateGraphTask::CreateAndDispatchWhenReady( FSimpleDelegateGraphTask::FDelegate::CreateLambda([=]() { MountPak(PakFilePath, PakOrder, PakMountPoint); }), TStatId(), nullptr, ENamedThreads::GameThread); return true; } else { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("nativeMountPak: immediate '%s'"), *PakFilePath); return MountPak(PakFilePath, PakOrder, PakMountPoint); } } JNI_METHOD void Java_com_epicgames_asis_AsisGameActivity_nativeAppCommand(JNIEnv* jenv, jobject thiz, jint cmd) { STANDALONE_DEBUG_LOGf(LogAndroid, TEXT("AsisGameActivity -> nativeAppCommand[%u, %s] -> EventThreadID=%d, GGameThreadId=%d, GResumeMainInit=%d, GEventHandlerInitialized=%d, FTaskGraphInterface::IsRunning()=%d, FPlatformTLS::GetCurrentThreadId()=%d"), cmd, get_APP_CMD_String(cmd), EventThreadID, GGameThreadId, GResumeMainInit, GEventHandlerInitialized, FTaskGraphInterface::IsRunning(), FPlatformTLS::GetCurrentThreadId() ); dummy_state.window = (ANativeWindow*)GAndroidWindowOverride; dummy_state.pendingWindow = (ANativeWindow*)GAndroidWindowOverride; OnAppCommandCB(&dummy_state, cmd); } #endif // USE_ANDROID_STANDALONE //Wraps __cxa_atexit and avoids registering any destructors for static objects to save startup time extern "C" { int __wrap___cxa_atexit(void (*f)(void*), void* objptr, void* dso) { return 0; } }; #endif // USE_ANDROID_LAUNCH