/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include // for AZ_MAX_PATH_LEN #include #include #include #include #include #include #include #include #include #include #include #include "aztestrunner.h" #include #include #include #include #include #include #include constexpr const char* s_logTag = "LMBR"; #define MAIN_EXIT_FAILURE(_appState, ...) \ __android_log_print(ANDROID_LOG_INFO, s_logTag, "****************************************************************"); \ __android_log_print(ANDROID_LOG_INFO, s_logTag, "STARTUP FAILURE - EXITING"); \ __android_log_print(ANDROID_LOG_INFO, s_logTag, "REASON:"); \ __android_log_print(ANDROID_LOG_INFO, s_logTag, __VA_ARGS__); \ __android_log_print(ANDROID_LOG_INFO, s_logTag, "****************************************************************"); \ _appState->userData = nullptr; \ ANativeActivity_finish(_appState->activity); \ while (_appState->destroyRequested == 0) { \ g_eventDispatcher.PumpAllEvents(); \ } \ return; namespace AzTestRunner { void set_quiet_mode() { } const char* get_current_working_directory() { static char cwd_buffer[AZ_MAX_PATH_LEN] = { '\0' }; [[maybe_unused]] AZ::Utils::ExecutablePathResult result = AZ::Utils::GetExecutableDirectory(cwd_buffer, AZ_ARRAY_SIZE(cwd_buffer)); AZ_Assert(result == AZ::Utils::ExecutablePathResult::Success, "Error retrieving executable path"); return static_cast(cwd_buffer); } void pause_on_completion() { } } namespace { class NativeEventDispatcher : public AzFramework::AndroidEventDispatcher { public: NativeEventDispatcher() : m_appState(nullptr) { } ~NativeEventDispatcher() { } void PumpAllEvents() override { bool continueRunning = true; while (continueRunning) { continueRunning = PumpEvents(&ALooper_pollAll); } } void PumpEventLoopOnce() override { PumpEvents(&ALooper_pollOnce); } void SetAppState(android_app* appState) { m_appState = appState; } private: // signature of ALooper_pollOnce and ALooper_pollAll -> int timeoutMillis, int* outFd, int* outEvents, void** outData typedef int (*EventPumpFunc)(int, int*, int*, void**); bool PumpEvents(EventPumpFunc looperFunc) { if (!m_appState) { return false; } int events = 0; android_poll_source* source = nullptr; const AZ::Android::AndroidEnv* androidEnv = AZ::Android::AndroidEnv::Get(); // when timeout is negative, the function will block until an event is received const int result = looperFunc(androidEnv->IsRunning() ? 0 : -1, nullptr, &events, reinterpret_cast(&source)); // the value returned from the looper poll func is either: // 1. the identifier associated with the event source (>= 0) and has event data that needs to be processed manually // 2. an ALOOPER_POLL_* enum (< 0) indicating there is no data to be processed due to error or callback(s) registered // with the event source were called const bool validIdentifier = (result >= 0); if (validIdentifier && source) { source->process(m_appState, source); } const bool destroyRequested = (m_appState->destroyRequested != 0); return (validIdentifier && !destroyRequested); } android_app* m_appState; }; NativeEventDispatcher g_eventDispatcher; bool g_windowInitialized = false; int32_t HandleInputEvents(android_app* app, AInputEvent* event) { AzFramework::RawInputNotificationBusAndroid::Broadcast(&AzFramework::RawInputNotificationsAndroid::OnRawInputEvent, event); return 0; } void HandleApplicationLifecycleEvents(android_app* appState, int32_t command) { AZ::Android::AndroidEnv* androidEnv = static_cast(appState->userData); if (!androidEnv) { return; } switch (command) { case APP_CMD_GAINED_FOCUS: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnGainedFocus); } break; case APP_CMD_LOST_FOCUS: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnLostFocus); } break; case APP_CMD_PAUSE: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnPause); androidEnv->SetIsRunning(false); } break; case APP_CMD_RESUME: { androidEnv->SetIsRunning(true); AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnResume); } break; case APP_CMD_DESTROY: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnDestroy); } break; case APP_CMD_INIT_WINDOW: { g_windowInitialized = true; androidEnv->SetWindow(appState->window); AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowInit); } break; case APP_CMD_TERM_WINDOW: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowDestroy); androidEnv->SetWindow(nullptr); } break; case APP_CMD_LOW_MEMORY: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnLowMemory); } break; case APP_CMD_CONFIG_CHANGED: { androidEnv->UpdateConfiguration(); } break; case APP_CMD_WINDOW_REDRAW_NEEDED: { AzFramework::AndroidLifecycleEvents::Bus::Broadcast( &AzFramework::AndroidLifecycleEvents::Bus::Events::OnWindowRedrawNeeded); } break; } } void OnWindowRedrawNeeded(ANativeActivity* activity, ANativeWindow* rect) { android_app* app = static_cast(activity->instance); int8_t cmd = APP_CMD_WINDOW_REDRAW_NEEDED; if (write(app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { __android_log_print(ANDROID_LOG_ERROR, s_logTag, "Failure writing android_app cmd: %s\n", strerror(errno)); } } } // In order to read the logcat from adb, stdout and stderr needs to be redirected custom handles and we will need // to spawn a thread to read from the custom handles and pass the output of those streams through __android_log_print // to a specia 'LMBR' tag static int s_pfd[2]; static std::atomic_bool s_testRunComplete { false }; static void *thread_logger_func(void*) { ssize_t readSize; char logBuffer[256] = {'\0'}; while (!s_testRunComplete) { readSize = read(s_pfd[0], logBuffer, sizeof(logBuffer)-1); if (readSize>0) { // Trim the tailing return character if (logBuffer[readSize - 1] == '\n') { --readSize; } logBuffer[readSize] = '\0'; ((void) __android_log_print(ANDROID_LOG_INFO, s_logTag, "%s", logBuffer)); } } return 0; } constexpr const char* s_defaultAppName = "AzTestRunner"; constexpr size_t s_maxArgCount = 8; constexpr size_t s_maxArgLength = 64; void android_main(android_app* appState) { // Adding a start up banner so you can see when the test runner is starting up in amongst the logcat spam __android_log_print(ANDROID_LOG_INFO, s_logTag, "****************************************************************"); __android_log_print(ANDROID_LOG_INFO, s_logTag, " Starting %s", s_defaultAppName); __android_log_print(ANDROID_LOG_INFO, s_logTag, "****************************************************************"); // setup the system command handler which are guaranteed to be called on the same // thread the events are pumped appState->onAppCmd = HandleApplicationLifecycleEvents; appState->onInputEvent = HandleInputEvents; g_eventDispatcher.SetAppState(appState); // This callback will notify us when the orientation of the device changes. // While Android does have an onNativeWindowResized callback, it is never called in android_native_app_glue when the window size changes. // The onNativeConfigChanged callback is called too early(before the window size has changed), so we won't have the correct window size at that point. appState->activity->callbacks->onNativeWindowRedrawNeeded = OnWindowRedrawNeeded; { AZ::Android::AndroidEnv::Descriptor descriptor; descriptor.m_jvm = appState->activity->vm; descriptor.m_activityRef = appState->activity->clazz; descriptor.m_assetManager = appState->activity->assetManager; descriptor.m_configuration = appState->config; descriptor.m_appPrivateStoragePath = appState->activity->internalDataPath; descriptor.m_appPublicStoragePath = appState->activity->externalDataPath; descriptor.m_obbStoragePath = appState->activity->obbPath; if (!AZ::Android::AndroidEnv::Create(descriptor)) { AZ::Android::AndroidEnv::Destroy(); MAIN_EXIT_FAILURE(appState, "Failed to create the AndroidEnv"); } AZ::Android::AndroidEnv* androidEnv = AZ::Android::AndroidEnv::Get(); appState->userData = androidEnv; androidEnv->SetIsRunning(true); } g_eventDispatcher.PumpAllEvents(); // Prepare the command line args to pass to main char commandLineArgs[s_maxArgCount][s_maxArgLength] = { {'\0'} }; size_t currentCommandLineIndex = 0; // Always add the app as the first arg to mimic the way other platforms start with the executable name. const char* packageName = AZ::Android::Utils::GetPackageName(); const char* appName = (packageName) ? packageName : s_defaultAppName; size_t appNameLen = strlen(appName); azstrncpy(commandLineArgs[currentCommandLineIndex++], s_maxArgLength, appName, appNameLen + 1); // "activityObject" and "intent" need to be destroyed before we call Destroy() on the allocator to ensure graceful shutdown. { // Get the string extras and pass them along as cmd line params AZ::Android::JNI::Internal::Object activityObject(AZ::Android::JNI::GetEnv()->GetObjectClass(appState->activity->clazz), appState->activity->clazz); activityObject.RegisterMethod("getIntent", "()Landroid/content/Intent;"); jobject intent = activityObject.InvokeObjectMethod("getIntent"); AZ::Android::JNI::Internal::Object intentObject(AZ::Android::JNI::GetEnv()->GetObjectClass(intent), intent); intentObject.RegisterMethod("getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;"); intentObject.RegisterMethod("getExtras", "()Landroid/os/Bundle;"); jobject extras = intentObject.InvokeObjectMethod("getExtras"); int start_delay = 0; if (extras) { // Get the set of keys AZ::Android::JNI::Internal::Object extrasObject(AZ::Android::JNI::GetEnv()->GetObjectClass(extras), extras); extrasObject.RegisterMethod("keySet", "()Ljava/util/Set;"); jobject extrasKeySet = extrasObject.InvokeObjectMethod("keySet"); // get the array of string objects AZ::Android::JNI::Internal::Object extrasKeySetObject(AZ::Android::JNI::GetEnv()->GetObjectClass(extrasKeySet), extrasKeySet); extrasKeySetObject.RegisterMethod("toArray", "()[Ljava/lang/Object;"); jobjectArray extrasKeySetArray = extrasKeySetObject.InvokeObjectMethod("toArray"); int extrasKeySetArraySize = AZ::Android::JNI::GetEnv()->GetArrayLength(extrasKeySetArray); for (int x = 0; x < extrasKeySetArraySize; x++) { jstring keyObject = static_cast(AZ::Android::JNI::GetEnv()->GetObjectArrayElement(extrasKeySetArray, x)); AZ::OSString value = intentObject.InvokeStringMethod("getStringExtra", keyObject); const char* keyChars = AZ::Android::JNI::GetEnv()->GetStringUTFChars(keyObject, 0); if (azstricmp("startdelay", keyChars) == 0) { start_delay = strtol(value.c_str(), nullptr, 10); } else if (azstricmp("gtest_filter", keyChars) == 0) { azsnprintf(commandLineArgs[currentCommandLineIndex++], s_maxArgLength, "--gtest_filter=%.*s", aznumeric_cast(value.size()), value.c_str()); } else { azstrncpy(commandLineArgs[currentCommandLineIndex++], s_maxArgLength, keyChars, strlen(keyChars) + 1); azstrncpy(commandLineArgs[currentCommandLineIndex++], s_maxArgLength, value.c_str(), value.length() + 1); } AZ::Android::JNI::GetEnv()->ReleaseStringUTFChars(keyObject, keyChars); } } if (start_delay > 0) { std::this_thread::sleep_for(std::chrono::seconds(start_delay)); } } char* argv[s_maxArgCount]; for (size_t index = 0; index < s_maxArgCount; index++) { argv[index] = commandLineArgs[index]; } // Redirect stdout and stderr to custom handles and prepare the thread to read from the custom handles pthread_t log_thread; setvbuf(stdout, 0, _IOLBF, 0); setvbuf(stderr, 0, _IONBF, 0); pipe(s_pfd); dup2(s_pfd[1], 1); dup2(s_pfd[1], 2); if (pthread_create(&log_thread, 0, thread_logger_func, 0) == -1) { ((void)__android_log_print(ANDROID_LOG_INFO, s_logTag, "[FAILURE] Unable to spawn logging thread")); return; } // Execute the unit test main int result = AzTestRunner::wrapped_main(aznumeric_cast(currentCommandLineIndex), argv); g_eventDispatcher.PumpAllEvents(); s_testRunComplete = true; std::this_thread::sleep_for(std::chrono::seconds(1)); if (result == 0) { ((void)__android_log_print(ANDROID_LOG_INFO, s_logTag, "[SUCCESS]")); } else { ((void)__android_log_print(ANDROID_LOG_INFO, s_logTag, "[FAILURE]")); } AZ::Android::AndroidEnv::Destroy(); }