diff --git a/src/xenia/base/console_app_main_android.cc b/src/xenia/base/console_app_main_android.cc index 320128bc1..3e2f79473 100644 --- a/src/xenia/base/console_app_main_android.cc +++ b/src/xenia/base/console_app_main_android.cc @@ -24,7 +24,7 @@ extern "C" int main(int argc, char** argv) { // Initialize Android globals, including logging. Needs parsed cvars. // TODO(Triang3l): Obtain the actual API level. - xe::InitializeAndroidAppFromMainThread(__ANDROID_API__); + xe::InitializeAndroidAppFromMainThread(__ANDROID_API__, nullptr, nullptr); std::vector args; for (int n = 0; n < argc; n++) { diff --git a/src/xenia/base/main_android.cc b/src/xenia/base/main_android.cc index 56211fee2..e1ef53c72 100644 --- a/src/xenia/base/main_android.cc +++ b/src/xenia/base/main_android.cc @@ -9,11 +9,15 @@ #include "xenia/base/main_android.h" +#include +#include #include +#include #include "xenia/base/assert.h" #include "xenia/base/logging.h" #include "xenia/base/memory.h" +#include "xenia/base/system.h" #include "xenia/base/threading.h" namespace xe { @@ -22,7 +26,25 @@ static size_t android_initializations_ = 0; static int32_t android_api_level_ = __ANDROID_API__; -void InitializeAndroidAppFromMainThread(int32_t api_level) { +static JNIEnv* android_main_thread_jni_env_ = nullptr; +static JavaVM* android_java_vm_ = nullptr; +static pthread_key_t android_thread_jni_env_key_; +static jobject android_application_context_ = nullptr; + +static void AndroidThreadJNIEnvDestructor(void* jni_env_pointer) { + // The JNIEnv pointer for the main thread is taken externally, the lifetime of + // the attachment is not managed by the key. + JNIEnv* jni_env = static_cast(jni_env_pointer); + if (jni_env && jni_env != android_main_thread_jni_env_) { + android_java_vm_->DetachCurrentThread(); + } + // Multiple iterations of destructor invocations can be done - clear. + pthread_setspecific(android_thread_jni_env_key_, nullptr); +} + +void InitializeAndroidAppFromMainThread(int32_t api_level, + JNIEnv* main_thread_jni_env, + jobject application_context) { if (android_initializations_++) { // Already initialized for another component in the process. return; @@ -32,6 +54,45 @@ void InitializeAndroidAppFromMainThread(int32_t api_level) { // subsystem initialization itself. android_api_level_ = api_level; + android_main_thread_jni_env_ = main_thread_jni_env; + if (main_thread_jni_env) { + // In a Java VM, not just in a process that runs an executable - set up + // the attachment of threads to the Java VM. + if (main_thread_jni_env->GetJavaVM(&android_java_vm_) < 0) { + // Logging has not been initialized yet. + __android_log_write( + ANDROID_LOG_ERROR, "InitializeAndroidAppFromMainThread", + "Failed to get the Java VM from the JNI environment of the main " + "thread"); + std::abort(); + } + if (pthread_key_create(&android_thread_jni_env_key_, + AndroidThreadJNIEnvDestructor)) { + __android_log_write( + ANDROID_LOG_ERROR, "InitializeAndroidAppFromMainThread", + "Failed to create the thread-specific JNI environment key"); + std::abort(); + } + if (pthread_setspecific(android_thread_jni_env_key_, main_thread_jni_env)) { + __android_log_write( + ANDROID_LOG_ERROR, "InitializeAndroidAppFromMainThread", + "Failed to set the thread-specific JNI environment pointer for the " + "main thread"); + std::abort(); + } + if (application_context) { + android_application_context_ = + main_thread_jni_env->NewGlobalRef(application_context); + if (!android_application_context_) { + __android_log_write( + ANDROID_LOG_ERROR, "InitializeAndroidAppFromMainThread", + "Failed to create a global reference to the application context " + "object"); + std::abort(); + } + } + } + // Logging uses threading. xe::threading::AndroidInitialize(); @@ -40,6 +101,15 @@ void InitializeAndroidAppFromMainThread(int32_t api_level) { xe::InitializeLogging("xenia"); xe::memory::AndroidInitialize(); + + if (android_application_context_) { + if (!xe::InitializeAndroidSystemForApplicationContext()) { + __android_log_write(ANDROID_LOG_ERROR, + "InitializeAndroidAppFromMainThread", + "Failed to initialize system UI interaction"); + std::abort(); + } + } } void ShutdownAndroidAppFromMainThread() { @@ -52,15 +122,36 @@ void ShutdownAndroidAppFromMainThread() { return; } + xe::ShutdownAndroidSystem(); + xe::memory::AndroidShutdown(); xe::ShutdownLogging(); xe::threading::AndroidShutdown(); + if (android_application_context_) { + android_main_thread_jni_env_->DeleteGlobalRef(android_application_context_); + android_application_context_ = nullptr; + } + if (android_java_vm_) { + android_java_vm_ = nullptr; + pthread_key_delete(android_thread_jni_env_key_); + } + android_main_thread_jni_env_ = nullptr; + android_api_level_ = __ANDROID_API__; } int32_t GetAndroidApiLevel() { return android_api_level_; } +JNIEnv* GetAndroidThreadJNIEnv() { + if (!android_java_vm_) { + return nullptr; + } + return static_cast(pthread_getspecific(android_thread_jni_env_key_)); +} + +jobject GetAndroidApplicationContext() { return android_application_context_; } + } // namespace xe diff --git a/src/xenia/base/main_android.h b/src/xenia/base/main_android.h index 1f2d23dd2..d871fc15a 100644 --- a/src/xenia/base/main_android.h +++ b/src/xenia/base/main_android.h @@ -2,7 +2,7 @@ ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** - * Copyright 2021 Ben Vanik. All rights reserved. * + * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ @@ -10,6 +10,7 @@ #ifndef XENIA_BASE_MAIN_ANDROID_H_ #define XENIA_BASE_MAIN_ANDROID_H_ +#include #include #include "xenia/base/platform.h" @@ -27,14 +28,22 @@ namespace xe { // counting internally. // // In standalone console apps built with $(BUILD_EXECUTABLE), these functions -// must be called in `main`. -void InitializeAndroidAppFromMainThread(int32_t api_level); +// must be called in `main`, with a null main thread JNI environment. +void InitializeAndroidAppFromMainThread(int32_t api_level, + JNIEnv* main_thread_jni_env, + jobject application_context); void ShutdownAndroidAppFromMainThread(); // May be the minimum supported level if the initialization was done without a // configuration. int32_t GetAndroidApiLevel(); +// May return null if not in a Java VM process, or in case of a failure to +// attach on a non-main thread. +JNIEnv* GetAndroidThreadJNIEnv(); +// Returns the global reference if in an application context, or null otherwise. +jobject GetAndroidApplicationContext(); + } // namespace xe #endif // XENIA_BASE_MAIN_ANDROID_H_ diff --git a/src/xenia/base/system.h b/src/xenia/base/system.h index 4bd0eac2b..b77bdbc69 100644 --- a/src/xenia/base/system.h +++ b/src/xenia/base/system.h @@ -13,10 +13,17 @@ #include #include +#include "xenia/base/platform.h" #include "xenia/base/string.h" namespace xe { +#if XE_PLATFORM_ANDROID +bool InitializeAndroidSystemForApplicationContext(); +void ShutdownAndroidSystem(); +#endif + +// The URL must include the protocol. void LaunchWebBrowser(const std::string_view url); void LaunchFileExplorer(const std::filesystem::path& path); diff --git a/src/xenia/base/system_android.cc b/src/xenia/base/system_android.cc index 8290172ca..af6ee9926 100644 --- a/src/xenia/base/system_android.cc +++ b/src/xenia/base/system_android.cc @@ -7,17 +7,285 @@ ****************************************************************************** */ +#include #include +#include #include "xenia/base/assert.h" +#include "xenia/base/logging.h" +#include "xenia/base/main_android.h" #include "xenia/base/system.h" namespace xe { +// To store jmethodIDs persistently, global references to the classes are +// required to prevent the classes from being unloaded and reloaded, potentially +// changing the method IDs. + +static jclass android_system_application_context_class_ = nullptr; +static jmethodID android_system_application_context_start_activity_ = nullptr; + +static jclass android_system_uri_class_ = nullptr; +static jmethodID android_system_uri_parse_ = nullptr; + +static jclass android_system_intent_class_ = nullptr; +static jfieldID android_system_intent_action_view_field_id_ = nullptr; +static jfieldID android_system_intent_flag_activity_new_task_field_id_ = + nullptr; +static jmethodID android_system_intent_init_action_uri_ = nullptr; +static jmethodID android_system_intent_add_flags_ = nullptr; +static jobject android_system_intent_action_view_ = nullptr; +static jint android_system_intent_flag_activity_new_task_; + +static bool android_system_initialized_ = false; + +bool InitializeAndroidSystemForApplicationContext() { + assert_false(android_system_initialized_); + + JNIEnv* jni_env = GetAndroidThreadJNIEnv(); + if (!jni_env) { + return false; + } + jobject application_context = xe::GetAndroidApplicationContext(); + if (!application_context) { + return false; + } + + // Application context. + { + { + jclass application_context_class_local_ref = + jni_env->GetObjectClass(application_context); + if (!application_context_class_local_ref) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to get the " + "class of the application context"); + ShutdownAndroidSystem(); + return false; + } + android_system_application_context_class_ = + reinterpret_cast(jni_env->NewGlobalRef( + reinterpret_cast(application_context_class_local_ref))); + jni_env->DeleteLocalRef(application_context_class_local_ref); + } + if (!android_system_application_context_class_) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to create a " + "global reference to the class of the application context"); + ShutdownAndroidSystem(); + return false; + } + bool application_context_ids_obtained = true; + application_context_ids_obtained &= + (android_system_application_context_start_activity_ = + jni_env->GetMethodID(android_system_application_context_class_, + "startActivity", + "(Landroid/content/Intent;)V")) != nullptr; + if (!application_context_ids_obtained) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to get the " + "application context class IDs"); + ShutdownAndroidSystem(); + return false; + } + } + + // URI. + { + { + jclass uri_class_local_ref = jni_env->FindClass("android/net/Uri"); + if (!uri_class_local_ref) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to find the " + "URI class"); + ShutdownAndroidSystem(); + return false; + } + android_system_uri_class_ = + reinterpret_cast(jni_env->NewGlobalRef( + reinterpret_cast(uri_class_local_ref))); + jni_env->DeleteLocalRef(uri_class_local_ref); + } + if (!android_system_uri_class_) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to create a " + "global reference to the URI class"); + ShutdownAndroidSystem(); + return false; + } + bool uri_ids_obtained = true; + uri_ids_obtained &= + (android_system_uri_parse_ = jni_env->GetStaticMethodID( + android_system_uri_class_, "parse", + "(Ljava/lang/String;)Landroid/net/Uri;")) != nullptr; + if (!uri_ids_obtained) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to get the URI " + "class IDs"); + ShutdownAndroidSystem(); + return false; + } + } + + // Intent. + { + { + jclass intent_class_local_ref = + jni_env->FindClass("android/content/Intent"); + if (!intent_class_local_ref) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to find the " + "intent class"); + ShutdownAndroidSystem(); + return false; + } + android_system_intent_class_ = + reinterpret_cast(jni_env->NewGlobalRef( + reinterpret_cast(intent_class_local_ref))); + jni_env->DeleteLocalRef(intent_class_local_ref); + } + if (!android_system_intent_class_) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to create a " + "global reference to the intent class"); + ShutdownAndroidSystem(); + return false; + } + bool intent_ids_obtained = true; + intent_ids_obtained &= (android_system_intent_action_view_field_id_ = + jni_env->GetStaticFieldID( + android_system_intent_class_, "ACTION_VIEW", + "Ljava/lang/String;")) != nullptr; + intent_ids_obtained &= + (android_system_intent_flag_activity_new_task_field_id_ = + jni_env->GetStaticFieldID(android_system_intent_class_, + "FLAG_ACTIVITY_NEW_TASK", "I")) != + nullptr; + intent_ids_obtained &= + (android_system_intent_init_action_uri_ = jni_env->GetMethodID( + android_system_intent_class_, "", + "(Ljava/lang/String;Landroid/net/Uri;)V")) != nullptr; + intent_ids_obtained &= + (android_system_intent_add_flags_ = + jni_env->GetMethodID(android_system_intent_class_, "addFlags", + "(I)Landroid/content/Intent;")) != nullptr; + if (!intent_ids_obtained) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to get the " + "intent class IDs"); + ShutdownAndroidSystem(); + return false; + } + { + jobject intent_action_view_local_ref = jni_env->GetStaticObjectField( + android_system_intent_class_, + android_system_intent_action_view_field_id_); + if (!intent_action_view_local_ref) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to get the " + "intent view action string"); + ShutdownAndroidSystem(); + return false; + } + android_system_intent_action_view_ = + jni_env->NewGlobalRef(intent_action_view_local_ref); + jni_env->DeleteLocalRef(intent_action_view_local_ref); + if (!android_system_intent_action_view_) { + XELOGE( + "InitializeAndroidSystemForApplicationContext: Failed to create a " + "global reference to the intent view action string"); + ShutdownAndroidSystem(); + return false; + } + } + android_system_intent_flag_activity_new_task_ = jni_env->GetStaticIntField( + android_system_intent_class_, + android_system_intent_flag_activity_new_task_field_id_); + } + + android_system_initialized_ = true; + return true; +} + +void ShutdownAndroidSystem() { + // May be called from InitializeAndroidSystemForApplicationContext as well. + android_system_initialized_ = false; + android_system_intent_add_flags_ = nullptr; + android_system_intent_init_action_uri_ = nullptr; + android_system_intent_flag_activity_new_task_field_id_ = nullptr; + android_system_intent_action_view_field_id_ = nullptr; + android_system_uri_parse_ = nullptr; + android_system_application_context_start_activity_ = nullptr; + JNIEnv* jni_env = GetAndroidThreadJNIEnv(); + if (jni_env) { + if (android_system_intent_action_view_) { + jni_env->DeleteGlobalRef(android_system_intent_action_view_); + } + if (android_system_intent_class_) { + jni_env->DeleteGlobalRef(android_system_intent_class_); + } + if (android_system_uri_class_) { + jni_env->DeleteGlobalRef(android_system_uri_class_); + } + if (android_system_application_context_class_) { + jni_env->DeleteGlobalRef(android_system_application_context_class_); + } + } + android_system_intent_action_view_ = nullptr; + android_system_intent_class_ = nullptr; + android_system_uri_class_ = nullptr; + android_system_application_context_class_ = nullptr; +} + void LaunchWebBrowser(const std::string_view url) { - // TODO(Triang3l): Intent.ACTION_VIEW (need a Java VM for the thread - - // possibly restrict this to the UI thread). - assert_always(); + if (!android_system_initialized_) { + return; + } + JNIEnv* jni_env = GetAndroidThreadJNIEnv(); + if (!jni_env) { + return; + } + jobject application_context = GetAndroidApplicationContext(); + if (!application_context) { + return; + } + + jstring uri_string = jni_env->NewStringUTF(std::string(url).c_str()); + if (!uri_string) { + XELOGE("LaunchWebBrowser: Failed to create the URI string"); + return; + } + jobject uri = jni_env->CallStaticObjectMethod( + android_system_uri_class_, android_system_uri_parse_, uri_string); + jni_env->DeleteLocalRef(uri_string); + if (!uri) { + XELOGE("LaunchWebBrowser: Failed to parse the URI"); + return; + } + jobject intent = jni_env->NewObject(android_system_intent_class_, + android_system_intent_init_action_uri_, + android_system_intent_action_view_, uri); + jni_env->DeleteLocalRef(uri); + if (!intent) { + XELOGE("LaunchWebBrowser: Failed to create the intent"); + return; + } + // Start a new task - the user may want to be able to switch between the + // emulator and the newly opened web browser, without having to quit the web + // browser to return to the emulator. Also, since the application context, not + // the activity, is used, the new task flag is required. + { + jobject intent_add_flags_result_local_ref = jni_env->CallObjectMethod( + intent, android_system_intent_add_flags_, + android_system_intent_flag_activity_new_task_); + if (intent_add_flags_result_local_ref) { + jni_env->DeleteLocalRef(intent_add_flags_result_local_ref); + } + } + jni_env->CallVoidMethod(application_context, + android_system_application_context_start_activity_, + intent); + jni_env->DeleteLocalRef(intent); } void LaunchFileExplorer(const std::filesystem::path& path) { assert_always(); } diff --git a/src/xenia/ui/windowed_app_context_android.cc b/src/xenia/ui/windowed_app_context_android.cc index 5af4efad6..66d1d42ba 100644 --- a/src/xenia/ui/windowed_app_context_android.cc +++ b/src/xenia/ui/windowed_app_context_android.cc @@ -141,26 +141,14 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, } AConfiguration_fromAssetManager(configuration_, asset_manager_); - // Initialize Xenia globals that may depend on the API level, as well as - // logging. - xe::InitializeAndroidAppFromMainThread( - AConfiguration_getSdkVersion(configuration_)); - android_base_initialized_ = true; - - // 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; - } + // 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) { - XELOGE("AndroidWindowedAppContext: Failed to get the activity class"); + __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the activity class"); Shutdown(); return false; } @@ -170,9 +158,46 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, 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 class"); + "activity"); Shutdown(); return false; } @@ -249,11 +274,6 @@ void AndroidWindowedAppContext::Shutdown() { } activity_method_finish_ = nullptr; - if (activity_class_) { - ui_thread_jni_env_->DeleteGlobalRef( - reinterpret_cast(activity_class_)); - activity_class_ = nullptr; - } if (activity_) { ui_thread_jni_env_->DeleteGlobalRef(activity_); activity_ = nullptr; @@ -264,6 +284,12 @@ void AndroidWindowedAppContext::Shutdown() { 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; diff --git a/src/xenia/ui/windowed_app_context_android.h b/src/xenia/ui/windowed_app_context_android.h index bc7583109..d3947938b 100644 --- a/src/xenia/ui/windowed_app_context_android.h +++ b/src/xenia/ui/windowed_app_context_android.h @@ -95,10 +95,11 @@ class AndroidWindowedAppContext final : public WindowedAppContext { AConfiguration* configuration_ = nullptr; + jclass activity_class_ = nullptr; + bool android_base_initialized_ = false; jobject activity_ = nullptr; - jclass activity_class_ = nullptr; jmethodID activity_method_finish_ = nullptr; // May be read by non-UI threads in NotifyUILoopOfPendingFunctions.