/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/ui/windowed_app_context_android.h" #include #include #include #include #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/window_android.h" #include "xenia/ui/windowed_app.h" namespace xe { namespace ui { void AndroidWindowedAppContext::NotifyUILoopOfPendingFunctions() { // 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. if (activity_ && activity_method_finish_) { ui_thread_jni_env_->CallVoidMethod(activity_, activity_method_finish_); } } void AndroidWindowedAppContext::PostInvalidateWindowSurface() { // May be called from non-UI threads. JNIEnv* jni_env = GetAndroidThreadJniEnv(); if (!jni_env) { return; } jni_env->CallVoidMethod(activity_, activity_method_post_invalidate_window_surface_); } AndroidWindowedAppContext* AndroidWindowedAppContext::JniActivityInitializeWindowedAppOnCreate( JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier, jobject asset_manager) { WindowedApp::Creator app_creator; { const char* windowed_app_identifier_c_str = jni_env->GetStringUTFChars(windowed_app_identifier, nullptr); if (!windowed_app_identifier_c_str) { __android_log_write( ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to get the UTF-8 string for the windowed app identifier"); return nullptr; } app_creator = WindowedApp::GetCreator(windowed_app_identifier_c_str); if (!app_creator) { __android_log_print(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to get the creator for the windowed app %s", windowed_app_identifier_c_str); jni_env->ReleaseStringUTFChars(windowed_app_identifier, windowed_app_identifier_c_str); return nullptr; } jni_env->ReleaseStringUTFChars(windowed_app_identifier, windowed_app_identifier_c_str); } AndroidWindowedAppContext* app_context = new AndroidWindowedAppContext; if (!app_context->Initialize(jni_env, activity, asset_manager)) { delete app_context; return nullptr; } if (!app_context->InitializeApp(app_creator)) { // InitializeApp might have sent commands to the UI thread looper callback // pipe, perform deferred destruction. app_context->RequestDestruction(); return nullptr; } return app_context; } void AndroidWindowedAppContext::JniActivityOnDestroy() { if (app_) { app_->InvokeOnDestroy(); app_.reset(); } // Expecting that the destruction of the app will destroy the window as well, // no need to notify it explicitly. assert_null(activity_window_); RequestDestruction(); } void AndroidWindowedAppContext::JniActivityOnWindowSurfaceLayoutChange( jint left, jint top, jint right, jint bottom) { window_surface_layout_left_ = left; window_surface_layout_top_ = top; window_surface_layout_right_ = right; window_surface_layout_bottom_ = bottom; if (activity_window_) { activity_window_->OnActivitySurfaceLayoutChange(); } } void AndroidWindowedAppContext::JniActivityOnWindowSurfaceChanged( jobject window_surface_object) { // Detach from the old surface. if (window_surface_) { ANativeWindow* old_window_surface = window_surface_; window_surface_ = nullptr; if (activity_window_) { activity_window_->OnActivitySurfaceChanged(); } ANativeWindow_release(old_window_surface); } if (!window_surface_object) { return; } window_surface_ = ANativeWindow_fromSurface(ui_thread_jni_env_, window_surface_object); if (!window_surface_) { return; } if (activity_window_) { activity_window_->OnActivitySurfaceChanged(); } } void AndroidWindowedAppContext::JniActivityPaintWindow(bool force_paint) { if (!activity_window_) { return; } activity_window_->PaintActivitySurface(force_paint); } AndroidWindowedAppContext::~AndroidWindowedAppContext() { Shutdown(); } bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, jobject activity, jobject asset_manager) { // Xenia logging is not initialized yet - use __android_log_write or // __android_log_print until InitializeAndroidAppFromMainThread is done. ui_thread_jni_env_ = ui_thread_jni_env; // Initialize the asset manager for retrieving the current configuration. asset_manager_jobject_ = ui_thread_jni_env_->NewGlobalRef(asset_manager); if (!asset_manager_jobject_) { __android_log_write( ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to create a global reference to the asset manager"); Shutdown(); return false; } asset_manager_ = AAssetManager_fromJava(ui_thread_jni_env_, asset_manager_jobject_); if (!asset_manager_) { __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to create get the AAssetManager"); Shutdown(); return false; } // Get the initial configuration. configuration_ = AConfiguration_new(); if (!configuration_) { __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to create an AConfiguration"); Shutdown(); return false; } AConfiguration_fromAssetManager(configuration_, asset_manager_); // Get the activity class, needed for the application context here, and for // other activity interaction later. { jclass activity_class_local_ref = ui_thread_jni_env_->GetObjectClass(activity); if (!activity_class_local_ref) { __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to get the activity class"); Shutdown(); return false; } activity_class_ = reinterpret_cast(ui_thread_jni_env_->NewGlobalRef( reinterpret_cast(activity_class_local_ref))); ui_thread_jni_env_->DeleteLocalRef( reinterpret_cast(activity_class_local_ref)); } if (!activity_class_) { __android_log_write( ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to create a global reference to the activity class"); Shutdown(); return false; } // Get the application context. jmethodID activity_get_application_context = ui_thread_jni_env_->GetMethodID( activity_class_, "getApplicationContext", "()Landroid/content/Context;"); if (!activity_get_application_context) { __android_log_write( ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to get the getApplicationContext method of the activity"); Shutdown(); return false; } jobject application_context_init_ref = ui_thread_jni_env_->CallObjectMethod( activity, activity_get_application_context); if (!application_context_init_ref) { __android_log_write( ANDROID_LOG_ERROR, "AndroidWindowedAppContext", "Failed to get the application context from the activity"); Shutdown(); return false; } // Initialize Xenia globals that may depend on the base globals and logging. xe::InitializeAndroidAppFromMainThread( AConfiguration_getSdkVersion(configuration_), ui_thread_jni_env_, application_context_init_ref); android_base_initialized_ = true; ui_thread_jni_env_->DeleteLocalRef(application_context_init_ref); // Initialize interfacing with the WindowedAppActivity. activity_ = ui_thread_jni_env_->NewGlobalRef(activity); if (!activity_) { XELOGE( "AndroidWindowedAppContext: Failed to create a global reference to the " "activity"); Shutdown(); return false; } bool activity_ids_obtained = true; activity_ids_obtained &= (activity_method_finish_ = ui_thread_jni_env_->GetMethodID( activity_class_, "finish", "()V")) != nullptr; activity_ids_obtained &= (activity_method_post_invalidate_window_surface_ = ui_thread_jni_env_->GetMethodID( activity_class_, "postInvalidateWindowSurface", "()V")) != nullptr; if (!activity_ids_obtained) { XELOGE("AndroidWindowedAppContext: Failed to get the activity class IDs"); Shutdown(); return false; } // 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; 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 (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; } activity_method_post_invalidate_window_surface_ = nullptr; activity_method_finish_ = nullptr; if (activity_) { ui_thread_jni_env_->DeleteGlobalRef(activity_); activity_ = nullptr; } if (android_base_initialized_) { xe::ShutdownAndroidAppFromMainThread(); android_base_initialized_ = false; } if (activity_class_) { ui_thread_jni_env_->DeleteGlobalRef( reinterpret_cast(activity_class_)); activity_class_ = nullptr; } if (configuration_) { AConfiguration_delete(configuration_); configuration_ = nullptr; } asset_manager_ = nullptr; if (asset_manager_jobject_) { ui_thread_jni_env_->DeleteGlobalRef(asset_manager_jobject_); asset_manager_jobject_ = nullptr; } ui_thread_jni_env_ = nullptr; } 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 ( *app_creator)(WindowedAppContext& app_context)) { assert_null(app_); app_ = app_creator(*this); if (!app_->OnInitialize()) { app_->InvokeOnDestroy(); app_.reset(); return false; } return true; } } // namespace ui } // namespace xe extern "C" { JNIEXPORT jlong JNICALL Java_jp_xenia_emulator_WindowedAppActivity_initializeWindowedAppOnCreate( JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier, jobject asset_manager) { return reinterpret_cast( xe::ui::AndroidWindowedAppContext :: JniActivityInitializeWindowedAppOnCreate( jni_env, activity, windowed_app_identifier, asset_manager)); } JNIEXPORT void JNICALL Java_jp_xenia_emulator_WindowedAppActivity_onDestroyNative( JNIEnv* jni_env, jobject activity, jlong app_context_ptr) { reinterpret_cast(app_context_ptr) ->JniActivityOnDestroy(); } JNIEXPORT void JNICALL Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceLayoutChange( JNIEnv* jni_env, jobject activity, jlong app_context_ptr, jint left, jint top, jint right, jint bottom) { reinterpret_cast(app_context_ptr) ->JniActivityOnWindowSurfaceLayoutChange(left, top, right, bottom); } JNIEXPORT void JNICALL Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceChanged( JNIEnv* jni_env, jobject activity, jlong app_context_ptr, jobject window_surface_object) { reinterpret_cast(app_context_ptr) ->JniActivityOnWindowSurfaceChanged(window_surface_object); } JNIEXPORT void JNICALL Java_jp_xenia_emulator_WindowedAppActivity_paintWindow( JNIEnv* jni_env, jobject activity, jlong app_context_ptr, jboolean force_paint) { reinterpret_cast(app_context_ptr) ->JniActivityPaintWindow(bool(force_paint)); } } // extern "C"