From 03355713540bfbd19f12ff2a2605ac700e7efea5 Mon Sep 17 00:00:00 2001 From: Triang3l Date: Wed, 15 Sep 2021 22:58:11 +0300 Subject: [PATCH] [UI] Android CallInUIThread and activity onDestroy --- src/xenia/ui/windowed_app.h | 2 +- src/xenia/ui/windowed_app_context_android.cc | 222 +++++++++++++++++-- src/xenia/ui/windowed_app_context_android.h | 53 ++++- 3 files changed, 249 insertions(+), 28 deletions(-) diff --git a/src/xenia/ui/windowed_app.h b/src/xenia/ui/windowed_app.h index 14eba9cb7..b521a759e 100644 --- a/src/xenia/ui/windowed_app.h +++ b/src/xenia/ui/windowed_app.h @@ -109,7 +109,7 @@ class WindowedApp { #define XE_DEFINE_WINDOWED_APP(export_name, creator) \ __attribute__((visibility("default"))) extern "C" void export_name( \ ANativeActivity* activity, void* saved_state, size_t saved_state_size) { \ - xe::ui::AndroidWindowedAppContext::StartAppOnNativeActivityCreate( \ + xe::ui::AndroidWindowedAppContext::StartAppOnActivityCreate( \ activity, saved_state, saved_state_size, creator); \ } #else diff --git a/src/xenia/ui/windowed_app_context_android.cc b/src/xenia/ui/windowed_app_context_android.cc index c1909d02e..fcab4f5fd 100644 --- a/src/xenia/ui/windowed_app_context_android.cc +++ b/src/xenia/ui/windowed_app_context_android.cc @@ -10,50 +10,71 @@ #include "xenia/ui/windowed_app_context_android.h" #include +#include #include +#include +#include +#include #include #include "xenia/base/assert.h" +#include "xenia/base/logging.h" #include "xenia/base/main_android.h" #include "xenia/ui/windowed_app.h" namespace xe { namespace ui { -void AndroidWindowedAppContext::StartAppOnNativeActivityCreate( +void AndroidWindowedAppContext::StartAppOnActivityCreate( ANativeActivity* activity, [[maybe_unused]] void* saved_state, [[maybe_unused]] size_t saved_state_size, std::unique_ptr (*app_creator)( WindowedAppContext& app_context)) { // TODO(Triang3l): Pass the launch options from the Intent or the saved // instance state. - AndroidWindowedAppContext* app_context = - new AndroidWindowedAppContext(activity); + AndroidWindowedAppContext* app_context = new AndroidWindowedAppContext; + if (!app_context->Initialize(activity)) { + delete app_context; + ANativeActivity_finish(activity); + return; + } // The pointer is now held by the Activity as its ANativeActivity::instance, // until the destruction. if (!app_context->InitializeApp(app_creator)) { - delete app_context; + // InitializeApp might have sent commands to the UI thread looper callback + // pipe, perform deferred destruction. + app_context->RequestDestruction(); ANativeActivity_finish(activity); + return; } } -AndroidWindowedAppContext::~AndroidWindowedAppContext() { - // TODO(Triang3l): Unregister activity callbacks. - activity_->instance = nullptr; - - xe::ShutdownAndroidAppFromMainThread(); -} - void AndroidWindowedAppContext::NotifyUILoopOfPendingFunctions() { - // TODO(Triang3l): Request message processing in the UI thread. + // Don't check ui_thread_looper_callback_registered_, as it's owned + // exclusively by the UI thread, while this may be called by any, and in case + // of a pipe error, the callback will be invoked by the looper, which will + // trigger all the necessary shutdown, and the pending functions will be + // called anyway by the shutdown. + UIThreadLooperCallbackCommand command = + UIThreadLooperCallbackCommand::kExecutePendingFunctions; + if (write(ui_thread_looper_callback_pipe_[1], &command, sizeof(command)) != + sizeof(command)) { + XELOGE( + "AndroidWindowedAppContext: Failed to write a pending function " + "execution command to the UI thread looper callback pipe"); + return; + } + ALooper_wake(ui_thread_looper_); } void AndroidWindowedAppContext::PlatformQuitFromUIThread() { + // All the shutdown will be done in onDestroy of the activity. ANativeActivity_finish(activity_); } -AndroidWindowedAppContext::AndroidWindowedAppContext(ANativeActivity* activity) - : activity_(activity) { +AndroidWindowedAppContext::~AndroidWindowedAppContext() { Shutdown(); } + +bool AndroidWindowedAppContext::Initialize(ANativeActivity* activity) { int32_t api_level; { AConfiguration* configuration = AConfiguration_new(); @@ -61,11 +82,170 @@ AndroidWindowedAppContext::AndroidWindowedAppContext(ANativeActivity* activity) api_level = AConfiguration_getSdkVersion(configuration); AConfiguration_delete(configuration); } - xe::InitializeAndroidAppFromMainThread(api_level); + android_base_initialized_ = true; + // Initialize sending commands to the UI thread looper callback, for + // requesting function calls in the UI thread. + ui_thread_looper_ = ALooper_forThread(); + // The context may be created only in the UI thread, which must have an + // internal looper. + assert_not_null(ui_thread_looper_); + if (!ui_thread_looper_) { + XELOGE("AndroidWindowedAppContext: Failed to get the UI thread looper"); + Shutdown(); + return false; + } + // The looper can be woken up by other threads, so acquiring it. Shutdown + // assumes that if ui_thread_looper_ is not null, it has been acquired. + ALooper_acquire(ui_thread_looper_); + if (pipe(ui_thread_looper_callback_pipe_.data())) { + XELOGE( + "AndroidWindowedAppContext: Failed to create the UI thread looper " + "callback pipe"); + Shutdown(); + return false; + } + if (ALooper_addFd(ui_thread_looper_, ui_thread_looper_callback_pipe_[0], + ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, + UIThreadLooperCallback, this) != 1) { + XELOGE( + "AndroidWindowedAppContext: Failed to add the callback to the UI " + "thread looper"); + Shutdown(); + return false; + } + ui_thread_looper_callback_registered_ = true; + + activity_ = activity; activity_->instance = this; - // TODO(Triang3l): Register activity callbacks. + activity_->callbacks->onDestroy = OnActivityDestroy; + + return true; +} + +void AndroidWindowedAppContext::Shutdown() { + if (app_) { + app_->InvokeOnDestroy(); + app_.reset(); + } + + // The app should destroy the window, but make sure everything is cleaned up + // anyway. + assert_null(activity_window_); + activity_window_ = nullptr; + + if (activity_) { + activity_->callbacks->onDestroy = nullptr; + activity_->instance = nullptr; + activity_ = nullptr; + } + + if (ui_thread_looper_callback_registered_) { + ALooper_removeFd(ui_thread_looper_, ui_thread_looper_callback_pipe_[0]); + ui_thread_looper_callback_registered_ = false; + } + for (int& pipe_fd : ui_thread_looper_callback_pipe_) { + if (pipe_fd == -1) { + continue; + } + close(pipe_fd); + pipe_fd = -1; + } + if (ui_thread_looper_) { + ALooper_release(ui_thread_looper_); + ui_thread_looper_ = nullptr; + } + + if (android_base_initialized_) { + xe::ShutdownAndroidAppFromMainThread(); + android_base_initialized_ = false; + } +} + +void AndroidWindowedAppContext::RequestDestruction() { + // According to ALooper_removeFd documentation: + // "...it is possible for the callback to already be running or for it to run + // one last time if the file descriptor was already signalled. Calling code + // is responsible for ensuring that this case is safely handled. For example, + // if the callback takes care of removing itself during its own execution + // either by returning 0 or by calling this method..." + // If the looper callback is registered, the pipe may have pending commands, + // and thus the callback may still be called with the pointer to the context + // as the user data. + if (!ui_thread_looper_callback_registered_) { + delete this; + return; + } + UIThreadLooperCallbackCommand command = + UIThreadLooperCallbackCommand::kDestroy; + if (write(ui_thread_looper_callback_pipe_[1], &command, sizeof(command)) != + sizeof(command)) { + XELOGE( + "AndroidWindowedAppContext: Failed to write a destruction command to " + "the UI thread looper callback pipe"); + delete this; + return; + } + ALooper_wake(ui_thread_looper_); +} + +int AndroidWindowedAppContext::UIThreadLooperCallback(int fd, int events, + void* data) { + // In case of errors, destruction of the pipe (most importantly the write end) + // must not be done here immediately as other threads, which may still be + // sending commands, would not be aware of that. + auto app_context = static_cast(data); + if (events & + (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP | ALOOPER_EVENT_INVALID)) { + // Will return 0 to unregister self, this file descriptor is not usable + // anymore, so let everything potentially referencing it in QuitFromUIThread + // know. + app_context->ui_thread_looper_callback_registered_ = false; + XELOGE( + "AndroidWindowedAppContext: The UI thread looper callback pipe file " + "descriptor has encountered an error condition during polling"); + app_context->QuitFromUIThread(); + return 0; + } + if (!(events & ALOOPER_EVENT_INPUT)) { + // Spurious callback call. Need a non-empty pipe. + return 1; + } + // Process one command with a blocking `read`. The callback will be invoked + // again and again if there is still data after this read. + UIThreadLooperCallbackCommand command; + switch (read(fd, &command, sizeof(command))) { + case sizeof(command): + break; + case -1: + // Will return 0 to unregister self, this file descriptor is not usable + // anymore, so let everything potentially referencing it in + // QuitFromUIThread know. + app_context->ui_thread_looper_callback_registered_ = false; + XELOGE( + "AndroidWindowedAppContext: The UI thread looper callback pipe file " + "descriptor has encountered an error condition during reading"); + app_context->QuitFromUIThread(); + return 0; + default: + // Something like incomplete data - shouldn't be happening, but not a + // reported error. + return 1; + } + switch (command) { + case UIThreadLooperCallbackCommand::kDestroy: + // Final destruction requested. Will unregister self by returning 0, so + // set ui_thread_looper_callback_registered_ to false so Shutdown won't + // try to unregister it too. + app_context->ui_thread_looper_callback_registered_ = false; + delete app_context; + return 0; + case UIThreadLooperCallbackCommand::kExecutePendingFunctions: + app_context->ExecutePendingFunctionsFromUIThread(); + break; + } + return 1; } bool AndroidWindowedAppContext::InitializeApp(std::unique_ptr ( @@ -80,5 +260,15 @@ bool AndroidWindowedAppContext::InitializeApp(std::unique_ptr ( return true; } +void AndroidWindowedAppContext::OnActivityDestroy(ANativeActivity* activity) { + auto& app_context = + *static_cast(activity->instance); + if (app_context.app_) { + app_context.app_->InvokeOnDestroy(); + app_context.app_.reset(); + } + app_context.RequestDestruction(); +} + } // namespace ui } // namespace xe diff --git a/src/xenia/ui/windowed_app_context_android.h b/src/xenia/ui/windowed_app_context_android.h index cfdc16ed1..64e45372d 100644 --- a/src/xenia/ui/windowed_app_context_android.h +++ b/src/xenia/ui/windowed_app_context_android.h @@ -10,7 +10,9 @@ #ifndef XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ #define XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ +#include #include +#include #include #include "xenia/ui/windowed_app_context.h" @@ -24,15 +26,11 @@ class WindowedApp; class AndroidWindowedAppContext final : public WindowedAppContext { public: // For calling from android.app.func_name exports. - static void StartAppOnNativeActivityCreate( + static void StartAppOnActivityCreate( ANativeActivity* activity, void* saved_state, size_t saved_state_size, std::unique_ptr (*app_creator)( WindowedAppContext& app_context)); - // Defined in the translation unit where WindowedApp is complete because of - // std::unique_ptr. - ~AndroidWindowedAppContext(); - ANativeActivity* activity() const { return activity_; } WindowedApp* app() const { return app_.get(); } @@ -48,21 +46,54 @@ class AndroidWindowedAppContext final : public WindowedAppContext { void SetActivityWindow(AndroidWindow* window) { activity_window_ = window; } private: - explicit AndroidWindowedAppContext(ANativeActivity* activity); + enum class UIThreadLooperCallbackCommand : uint8_t { + kDestroy, + kExecutePendingFunctions, + }; + + AndroidWindowedAppContext() = default; + + // Don't delete this object directly externally if successfully initialized as + // the looper may still execute the callback for pending commands after an + // external ANativeActivity_removeFd, and the callback receives a pointer to + // the context - deletion must be deferred and done in the callback itself. + // Defined in the translation unit where WindowedApp is complete because of + // std::unique_ptr. + ~AndroidWindowedAppContext(); + + bool Initialize(ANativeActivity* activity); + void Shutdown(); + + // Call this function instead of deleting the object directly, so if needed, + // deletion will be deferred until the callback (receiving a pointer to the + // context) can no longer be executed by the looper (will be done inside the + // callback). + void RequestDestruction(); + + static int UIThreadLooperCallback(int fd, int events, void* data); + bool InitializeApp(std::unique_ptr (*app_creator)( WindowedAppContext& app_context)); + static void OnActivityDestroy(ANativeActivity* activity); + + bool android_base_initialized_ = false; + + // May be read by non-UI threads in NotifyUILoopOfPendingFunctions. + ALooper* ui_thread_looper_ = nullptr; + // [1] (the write file descriptor) may be referenced as read-only by non-UI + // threads in NotifyUILoopOfPendingFunctions. + std::array ui_thread_looper_callback_pipe_{-1, -1}; + bool ui_thread_looper_callback_registered_ = false; + // TODO(Triang3l): Switch from ANativeActivity to the context itself being the // object for communication with the Java code when NativeActivity isn't used // anymore as its functionality is heavily limited. - ANativeActivity* activity_; - std::unique_ptr app_; + ANativeActivity* activity_ = nullptr; AndroidWindow* activity_window_ = nullptr; - // TODO(Triang3l): The rest of the context, including quit handler (and the - // destructor) calling `finish` on the activity, UI looper notification - // posting, etc. + std::unique_ptr app_; }; } // namespace ui