diff --git a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java index 69e0fbe59..9ea50a0ca 100644 --- a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java +++ b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java @@ -14,6 +14,13 @@ import org.jetbrains.annotations.Nullable; import jp.xenia.XeniaRuntimeException; public abstract class WindowedAppActivity extends Activity { + // The EXTRA_CVARS value literal is also used in the native code. + + /** + * Name of the Bundle intent extra containing Xenia config variable launch arguments. + */ + public static final String EXTRA_CVARS = "jp.xenia.emulator.WindowedAppActivity.EXTRA_CVARS"; + static { System.loadLibrary("xenia-app"); } diff --git a/src/xenia/base/console_app_main_android.cc b/src/xenia/base/console_app_main_android.cc index 3e2f79473..8486d4e05 100644 --- a/src/xenia/base/console_app_main_android.cc +++ b/src/xenia/base/console_app_main_android.cc @@ -24,7 +24,8 @@ 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__, nullptr, nullptr); + xe::InitializeAndroidAppFromMainThread(__ANDROID_API__, nullptr, nullptr, + nullptr); std::vector args; for (int n = 0; n < argc; n++) { diff --git a/src/xenia/base/cvar.cc b/src/xenia/base/cvar.cc index 95c7a58f3..219f033ae 100644 --- a/src/xenia/base/cvar.cc +++ b/src/xenia/base/cvar.cc @@ -7,9 +7,7 @@ ****************************************************************************** */ -#include "cvar.h" - -#include "utf8.h" +#include "xenia/base/cvar.h" #define UTF_CPP_CPLUSPLUS 201703L #include "third_party/utfcpp/source/utf8.h" @@ -17,6 +15,7 @@ #include "xenia/base/console.h" #include "xenia/base/logging.h" #include "xenia/base/system.h" +#include "xenia/base/utf8.h" namespace utfcpp = utf8; diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index 585039fd6..61b8faf11 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -20,8 +20,13 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/base/assert.h" #include "xenia/base/filesystem.h" +#include "xenia/base/platform.h" #include "xenia/base/string_util.h" +#if XE_PLATFORM_ANDROID +#include +#endif // XE_PLATFORM_ANDROID + namespace cvar { namespace toml { @@ -56,6 +61,7 @@ class CommandVar : virtual public ICommandVar { const std::string& description() const override; void AddToLaunchOptions(cxxopts::Options* options) override; void LoadFromLaunchOptions(cxxopts::ParseResult* result) override; + void SetCommandLineValue(T val); T* current_value() { return current_value_; } protected: @@ -67,7 +73,6 @@ class CommandVar : virtual public ICommandVar { T Convert(std::string val); static std::string ToString(T val); void SetValue(T val); - void SetCommandLineValue(T val); void UpdateValue() override; }; #pragma warning(push) @@ -297,6 +302,9 @@ inline void AddCommandVar(ICommandVar* cv) { void ParseLaunchArguments(int& argc, char**& argv, const std::string_view positional_help, const std::vector& positional_options); +#if XE_PLATFORM_ANDROID +void ParseLaunchArgumentsFromAndroidBundle(jobject bundle); +#endif // XE_PLATFORM_ANDROID template IConfigVar* define_configvar(const char* name, T* default_value, diff --git a/src/xenia/base/cvar_android.cc b/src/xenia/base/cvar_android.cc new file mode 100644 index 000000000..c8916d6be --- /dev/null +++ b/src/xenia/base/cvar_android.cc @@ -0,0 +1,211 @@ +/** + ****************************************************************************** + * 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 + +#include "xenia/base/assert.h" +#include "xenia/base/cvar.h" +#include "xenia/base/main_android.h" + +namespace cvar { + +void ParseLaunchArgumentsFromAndroidBundle(jobject bundle) { + if (!ConfigVars) { + return; + } + + JNIEnv* jni_env = xe::GetAndroidThreadJniEnv(); + if (!jni_env) { + return; + } + + jclass bundle_class = jni_env->GetObjectClass(bundle); + if (!bundle_class) { + return; + } + bool bundle_methods_obtained = true; + jmethodID bundle_get_boolean = + jni_env->GetMethodID(bundle_class, "getBoolean", "(Ljava/lang/String;)Z"); + bundle_methods_obtained &= (bundle_get_boolean != nullptr); + jmethodID bundle_get_double = + jni_env->GetMethodID(bundle_class, "getDouble", "(Ljava/lang/String;)D"); + bundle_methods_obtained &= (bundle_get_double != nullptr); + jmethodID bundle_get_int = + jni_env->GetMethodID(bundle_class, "getInt", "(Ljava/lang/String;)I"); + bundle_methods_obtained &= (bundle_get_int != nullptr); + jmethodID bundle_get_long = + jni_env->GetMethodID(bundle_class, "getLong", "(Ljava/lang/String;)J"); + bundle_methods_obtained &= (bundle_get_long != nullptr); + jmethodID bundle_get_string = jni_env->GetMethodID( + bundle_class, "getString", "(Ljava/lang/String;)Ljava/lang/String;"); + bundle_methods_obtained &= (bundle_get_string != nullptr); + jmethodID bundle_key_set_method_id = + jni_env->GetMethodID(bundle_class, "keySet", "()Ljava/util/Set;"); + bundle_methods_obtained &= (bundle_key_set_method_id != nullptr); + if (!bundle_methods_obtained) { + jni_env->DeleteLocalRef(bundle_class); + return; + } + + jobject key_set = jni_env->CallObjectMethod(bundle, bundle_key_set_method_id); + if (!key_set) { + jni_env->DeleteLocalRef(bundle_class); + return; + } + + jclass set_class = jni_env->GetObjectClass(key_set); + if (!set_class) { + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); + return; + } + bool set_methods_obtained = true; + jmethodID set_iterator_method_id = + jni_env->GetMethodID(set_class, "iterator", "()Ljava/util/Iterator;"); + set_methods_obtained &= (set_iterator_method_id != nullptr); + if (!set_methods_obtained) { + jni_env->DeleteLocalRef(set_class); + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); + return; + } + + jobject key_set_iterator = + jni_env->CallObjectMethod(key_set, set_iterator_method_id); + if (!key_set_iterator) { + jni_env->DeleteLocalRef(set_class); + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); + return; + } + + jclass iterator_class = jni_env->GetObjectClass(key_set_iterator); + if (!iterator_class) { + jni_env->DeleteLocalRef(key_set_iterator); + jni_env->DeleteLocalRef(set_class); + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); + return; + } + bool iterator_methods_obtained = true; + jmethodID iterator_has_next = + jni_env->GetMethodID(iterator_class, "hasNext", "()Z"); + iterator_methods_obtained &= (iterator_has_next != nullptr); + jmethodID iterator_next = + jni_env->GetMethodID(iterator_class, "next", "()Ljava/lang/Object;"); + iterator_methods_obtained &= (iterator_next != nullptr); + if (!iterator_methods_obtained) { + jni_env->DeleteLocalRef(iterator_class); + jni_env->DeleteLocalRef(key_set_iterator); + jni_env->DeleteLocalRef(set_class); + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); + return; + } + + while (jni_env->CallBooleanMethod(key_set_iterator, iterator_has_next)) { + jstring key = reinterpret_cast( + jni_env->CallObjectMethod(key_set_iterator, iterator_next)); + if (!key) { + continue; + } + const char* key_utf = jni_env->GetStringUTFChars(key, nullptr); + if (!key_utf) { + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_it = ConfigVars->find(key_utf); + jni_env->ReleaseStringUTFChars(key, key_utf); + // key_utf can't be used from now on. + if (cvar_it == ConfigVars->end()) { + jni_env->DeleteLocalRef(key); + continue; + } + IConfigVar* cvar = cvar_it->second; + auto cvar_bool = dynamic_cast*>(cvar); + if (cvar_bool) { + cvar_bool->SetCommandLineValue( + bool(jni_env->CallBooleanMethod(bundle, bundle_get_boolean, key))); + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_int32 = dynamic_cast*>(cvar); + if (cvar_int32) { + cvar_int32->SetCommandLineValue( + jni_env->CallIntMethod(bundle, bundle_get_int, key)); + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_uint32 = dynamic_cast*>(cvar); + if (cvar_uint32) { + cvar_uint32->SetCommandLineValue( + uint32_t(jni_env->CallIntMethod(bundle, bundle_get_int, key))); + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_uint64 = dynamic_cast*>(cvar); + if (cvar_uint64) { + cvar_uint64->SetCommandLineValue( + uint64_t(jni_env->CallLongMethod(bundle, bundle_get_long, key))); + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_double = dynamic_cast*>(cvar); + if (cvar_double) { + cvar_double->SetCommandLineValue( + jni_env->CallDoubleMethod(bundle, bundle_get_double, key)); + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_string = dynamic_cast*>(cvar); + if (cvar_string) { + jstring cvar_string_value = reinterpret_cast( + jni_env->CallObjectMethod(bundle, bundle_get_string, key)); + if (cvar_string_value) { + const char* cvar_string_value_utf = + jni_env->GetStringUTFChars(cvar_string_value, nullptr); + if (cvar_string_value_utf) { + cvar_string->SetCommandLineValue(cvar_string_value_utf); + jni_env->ReleaseStringUTFChars(cvar_string_value, + cvar_string_value_utf); + } + jni_env->DeleteLocalRef(cvar_string_value); + } + jni_env->DeleteLocalRef(key); + continue; + } + auto cvar_path = dynamic_cast*>(cvar); + if (cvar_path) { + jstring cvar_string_value = reinterpret_cast( + jni_env->CallObjectMethod(bundle, bundle_get_string, key)); + if (cvar_string_value) { + const char* cvar_string_value_utf = + jni_env->GetStringUTFChars(cvar_string_value, nullptr); + if (cvar_string_value_utf) { + cvar_path->SetCommandLineValue(cvar_string_value_utf); + jni_env->ReleaseStringUTFChars(cvar_string_value, + cvar_string_value_utf); + } + jni_env->DeleteLocalRef(cvar_string_value); + } + jni_env->DeleteLocalRef(key); + continue; + } + assert_always("Unsupported type of cvar {}", cvar->name().c_str()); + jni_env->DeleteLocalRef(key); + } + + jni_env->DeleteLocalRef(iterator_class); + jni_env->DeleteLocalRef(key_set_iterator); + jni_env->DeleteLocalRef(set_class); + jni_env->DeleteLocalRef(key_set); + jni_env->DeleteLocalRef(bundle_class); +} + +} // namespace cvar diff --git a/src/xenia/base/main_android.cc b/src/xenia/base/main_android.cc index a44ca7d3b..eaa4642c3 100644 --- a/src/xenia/base/main_android.cc +++ b/src/xenia/base/main_android.cc @@ -15,6 +15,7 @@ #include #include "xenia/base/assert.h" +#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/memory.h" #include "xenia/base/system.h" @@ -44,7 +45,8 @@ static void AndroidThreadJNIEnvDestructor(void* jni_env_pointer) { void InitializeAndroidAppFromMainThread(int32_t api_level, JNIEnv* main_thread_jni_env, - jobject application_context) { + jobject application_context, + jobject launch_arguments_bundle) { if (android_initializations_++) { // Already initialized for another component in the process. return; @@ -96,6 +98,11 @@ void InitializeAndroidAppFromMainThread(int32_t api_level, // Logging uses threading. xe::threading::AndroidInitialize(); + // Initialize the cvars before logging. + if (launch_arguments_bundle) { + cvar::ParseLaunchArgumentsFromAndroidBundle(launch_arguments_bundle); + } + // Multiple apps can be launched within one process - don't pass the actual // app name. xe::InitializeLogging("xenia"); diff --git a/src/xenia/base/main_android.h b/src/xenia/base/main_android.h index e74a82ffe..a715e092b 100644 --- a/src/xenia/base/main_android.h +++ b/src/xenia/base/main_android.h @@ -31,7 +31,8 @@ namespace xe { // 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); + jobject application_context, + jobject launch_arguments_bundle); void ShutdownAndroidAppFromMainThread(); // May be the minimum supported level if the initialization was done without a diff --git a/src/xenia/ui/windowed_app_context_android.cc b/src/xenia/ui/windowed_app_context_android.cc index d25ed97ee..6ff094935 100644 --- a/src/xenia/ui/windowed_app_context_android.cc +++ b/src/xenia/ui/windowed_app_context_android.cc @@ -230,7 +230,7 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, return false; } - // Get the application context. + // Get activity methods needed for initialization. jmethodID activity_get_application_context = ui_thread_jni_env_->GetMethodID( activity_class_, "getApplicationContext", "()Landroid/content/Context;"); if (!activity_get_application_context) { @@ -240,6 +240,16 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, Shutdown(); return false; } + jmethodID activity_get_intent = ui_thread_jni_env_->GetMethodID( + activity_class_, "getIntent", "()Landroid/content/Intent;"); + if (!activity_get_intent) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the getIntent method of the activity"); + Shutdown(); + return false; + } + + // Get the application context. jobject application_context_init_ref = ui_thread_jni_env_->CallObjectMethod( activity, activity_get_application_context); if (!application_context_init_ref) { @@ -250,11 +260,59 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, return false; } + // Get the launch arguments. + jobject launch_arguments_bundle_init_ref = nullptr; + { + jobject intent_init_ref = + ui_thread_jni_env_->CallObjectMethod(activity, activity_get_intent); + if (!intent_init_ref) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the intent that has started the activity"); + } else { + jclass intent_class = ui_thread_jni_env_->GetObjectClass(intent_init_ref); + if (!intent_class) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the intent class"); + } else { + jmethodID intent_get_bundle_extra = ui_thread_jni_env_->GetMethodID( + intent_class, "getBundleExtra", + "(Ljava/lang/String;)Landroid/os/Bundle;"); + if (!intent_get_bundle_extra) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the getBundleExtra method of the intent"); + } else { + jstring launch_arguments_extra_name = + ui_thread_jni_env_->NewStringUTF( + "jp.xenia.emulator.WindowedAppActivity.EXTRA_CVARS"); + if (!launch_arguments_extra_name) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to create the launch arguments intent extra data name " + "string"); + } else { + launch_arguments_bundle_init_ref = + ui_thread_jni_env_->CallObjectMethod( + intent_init_ref, intent_get_bundle_extra, + launch_arguments_extra_name); + ui_thread_jni_env_->DeleteLocalRef(launch_arguments_extra_name); + } + } + ui_thread_jni_env_->DeleteLocalRef(intent_class); + } + ui_thread_jni_env_->DeleteLocalRef(intent_init_ref); + } + } + // 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); + application_context_init_ref, launch_arguments_bundle_init_ref); android_base_initialized_ = true; + if (launch_arguments_bundle_init_ref) { + ui_thread_jni_env_->DeleteLocalRef(launch_arguments_bundle_init_ref); + } ui_thread_jni_env_->DeleteLocalRef(application_context_init_ref); // Initialize common windowed app JNI IDs.