[Base] Android JNIEnv attachment and LaunchWebBrowser

This commit is contained in:
Triang3l 2022-01-30 23:35:01 +03:00
parent 54b057a46b
commit 3f817fb241
7 changed files with 433 additions and 31 deletions

View File

@ -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<std::string> args;
for (int n = 0; n < argc; n++) {

View File

@ -9,11 +9,15 @@
#include "xenia/base/main_android.h"
#include <android/log.h>
#include <pthread.h>
#include <cstddef>
#include <cstdlib>
#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<JNIEnv*>(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<JNIEnv*>(pthread_getspecific(android_thread_jni_env_key_));
}
jobject GetAndroidApplicationContext() { return android_application_context_; }
} // namespace xe

View File

@ -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 <jni.h>
#include <cstdint>
#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_

View File

@ -13,10 +13,17 @@
#include <filesystem>
#include <string_view>
#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);

View File

@ -7,17 +7,285 @@
******************************************************************************
*/
#include <jni.h>
#include <cstring>
#include <string>
#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<jclass>(jni_env->NewGlobalRef(
reinterpret_cast<jobject>(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<jclass>(jni_env->NewGlobalRef(
reinterpret_cast<jobject>(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<jclass>(jni_env->NewGlobalRef(
reinterpret_cast<jobject>(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_, "<init>",
"(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(); }

View File

@ -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<jobject>(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<jobject>(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<jobject>(activity_class_));
activity_class_ = nullptr;
}
if (configuration_) {
AConfiguration_delete(configuration_);
configuration_ = nullptr;

View File

@ -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.