2341 lines
78 KiB
C++
2341 lines
78 KiB
C++
// 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 <string.h>
|
|
#include <pthread.h>
|
|
#include "Android/AndroidJNI.h"
|
|
#include "Android/AndroidEventManager.h"
|
|
#include "Android/AndroidInputInterface.h"
|
|
#include <android/asset_manager.h>
|
|
#include <android/asset_manager_jni.h>
|
|
#include <android/log.h>
|
|
#include <cpu-features.h>
|
|
#include <android_native_app_glue.h>
|
|
#include <cstdio>
|
|
#include <sys/resource.h>
|
|
#include <sys/system_properties.h>
|
|
#include <dlfcn.h>
|
|
#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 <jni.h>
|
|
#include <android/sensor.h>
|
|
|
|
#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<uint32> MappedKeyCodes;
|
|
|
|
// map of always allowed keycodes
|
|
static TSet<uint32> 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<uint32> 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<int32> 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<FString> 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<IHeadMountedDisplayModule*> 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>(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<FString> AndroidCompatCVars;
|
|
if (GConfig->GetArray(TEXT("AndroidCompatCVars"), TEXT("CVars"), AndroidCompatCVars, GEngineIni))
|
|
{
|
|
TSet<FString> AllowedCompatCVars(AndroidCompatCVars);
|
|
for (const TTuple<FString, FString>& 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<IMessagingModule>("Messaging");
|
|
TSharedPtr<ISessionService> SessionService = FModuleManager::LoadModuleChecked<ISessionServicesModule>("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<int64(int64)> 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<func_AChoreographer_getInstance>(
|
|
dlsym(lib, "AChoreographer_getInstance"));
|
|
AChoreographer_postFrameCallback_ =
|
|
reinterpret_cast<func_AChoreographer_postFrameCallback>(
|
|
dlsym(lib, "AChoreographer_postFrameCallback"));
|
|
AChoreographer_postFrameCallbackDelayed_ =
|
|
reinterpret_cast<func_AChoreographer_postFrameCallbackDelayed>(
|
|
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<int64(int64)> 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<int64(int64)> 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<TouchInput> 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<float>(AMotionEvent_getX(event, actionPointer) / Width, 1.f);
|
|
x *= (ScreenRect.Right - 1);
|
|
float y = FMath::Min<float>(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<float>(AMotionEvent_getX(event, i) / Width, 1.f);
|
|
x *= (ScreenRect.Right - 1);
|
|
float y = FMath::Min<float>(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<FAndroidWindow::FNativeAccessor> 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<FAndroidWindow::FNativeAccessor> 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<FEvent, ESPMode::ThreadSafe> 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<TouchInput> 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
|