514 lines
18 KiB
C++
514 lines
18 KiB
C++
/**
|
|
******************************************************************************
|
|
* 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 <android/asset_manager_jni.h>
|
|
#include <android/configuration.h>
|
|
#include <android/log.h>
|
|
#include <android/looper.h>
|
|
#include <android/native_window.h>
|
|
#include <android/native_window_jni.h>
|
|
#include <fcntl.h>
|
|
#include <jni.h>
|
|
#include <unistd.h>
|
|
#include <array>
|
|
#include <cstdint>
|
|
|
|
#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<jclass>(ui_thread_jni_env_->NewGlobalRef(
|
|
reinterpret_cast<jobject>(activity_class_local_ref)));
|
|
ui_thread_jni_env_->DeleteLocalRef(
|
|
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");
|
|
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<jobject>(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<AndroidWindowedAppContext*>(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<WindowedApp> (
|
|
*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<jlong>(
|
|
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<xe::ui::AndroidWindowedAppContext*>(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<xe::ui::AndroidWindowedAppContext*>(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<xe::ui::AndroidWindowedAppContext*>(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<xe::ui::AndroidWindowedAppContext*>(app_context_ptr)
|
|
->JniActivityPaintWindow(bool(force_paint));
|
|
}
|
|
|
|
} // extern "C"
|