diff options
Diffstat (limited to 'platform/android')
82 files changed, 3586 insertions, 2368 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index dfc921cc54..31bc7c25b0 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -1,5 +1,7 @@ #!/usr/bin/env python +import subprocess + Import("env") android_files = [ @@ -21,7 +23,7 @@ android_files = [ "android_keys_utils.cpp", "display_server_android.cpp", "plugin/godot_plugin_jni.cpp", - "vulkan_context_android.cpp", + "rendering_context_driver_vulkan_android.cpp", ] env_android = env.Clone() @@ -77,3 +79,29 @@ if lib_arch_dir != "": str(env["ANDROID_NDK_ROOT"]) + "/sources/cxx-stl/llvm-libc++/libs/" + lib_arch_dir + "/libc++_shared.so" ) env_android.Command(out_dir + "/libc++_shared.so", stl_lib_path, Copy("$TARGET", "$SOURCE")) + + def generate_apk(target, source, env): + if env["target"] != "editor" and env["dev_build"]: + subprocess.run( + [ + "./gradlew", + "generateDevTemplate", + "--quiet", + ], + cwd="platform/android/java", + ) + else: + # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task. + subprocess.run( + [ + "./gradlew", + "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates", + "--quiet", + ], + cwd="platform/android/java", + ) + + if env["generate_apk"]: + generate_apk_command = env_android.Command("generate_apk", [], generate_apk) + command = env_android.AlwaysBuild(generate_apk_command) + env_android.Depends(command, [lib]) diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index f6a0776017..373dd399e4 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -124,6 +124,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod ev->set_physical_keycode(physical_keycode); ev->set_key_label(fix_key_label(p_key_label, keycode)); ev->set_unicode(fix_unicode(unicode)); + ev->set_location(godot_location_from_android_code(p_physical_keycode)); ev->set_pressed(p_pressed); ev->set_echo(p_echo); @@ -206,6 +207,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const ev->set_index(touch[i].id); ev->set_position(p_points[idx].pos); ev->set_relative(p_points[idx].pos - touch[i].pos); + ev->set_relative_screen_position(ev->get_relative()); Input::get_singleton()->parse_input_event(ev); touch.write[i].pos = p_points[idx].pos; } @@ -292,7 +294,7 @@ void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative mouse_event_info.valid = false; } -void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative) { +void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative, float p_pressure, Vector2 p_tilt) { BitField<MouseButtonMask> event_buttons_mask = _android_button_mask_to_godot_button_mask(p_event_android_buttons_mask); switch (p_event_action) { case AMOTION_EVENT_ACTION_HOVER_MOVE: // hover move @@ -305,6 +307,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an ev->set_position(p_event_pos); ev->set_global_position(p_event_pos); ev->set_relative(p_event_pos - hover_prev_pos); + ev->set_relative_screen_position(ev->get_relative()); Input::get_singleton()->parse_input_event(ev); hover_prev_pos = p_event_pos; } break; @@ -341,14 +344,18 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an ev->set_position(hover_prev_pos); ev->set_global_position(hover_prev_pos); ev->set_relative(p_event_pos); + ev->set_relative_screen_position(p_event_pos); } else { ev->set_position(p_event_pos); ev->set_global_position(p_event_pos); ev->set_relative(p_event_pos - hover_prev_pos); + ev->set_relative_screen_position(ev->get_relative()); mouse_event_info.pos = p_event_pos; hover_prev_pos = p_event_pos; } ev->set_button_mask(event_buttons_mask); + ev->set_pressure(p_pressure); + ev->set_tilt(p_tilt); Input::get_singleton()->parse_input_event(ev); } break; diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index c74c5020e3..78a484cf05 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -96,7 +96,7 @@ private: void _cancel_all_touch(); public: - void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative); + void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative, float p_pressure, Vector2 p_tilt); void process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points, bool p_double_tap); void process_magnify(Point2 p_pos, float p_factor); void process_pan(Point2 p_pos, Vector2 p_delta); diff --git a/platform/android/android_keys_utils.cpp b/platform/android/android_keys_utils.cpp index f50437e82a..83ee98e8bc 100644 --- a/platform/android/android_keys_utils.cpp +++ b/platform/android/android_keys_utils.cpp @@ -38,3 +38,12 @@ Key godot_code_from_android_code(unsigned int p_code) { } return Key::UNKNOWN; } + +KeyLocation godot_location_from_android_code(unsigned int p_code) { + for (int i = 0; android_godot_location_pairs[i].android_code != AKEYCODE_MAX; i++) { + if (android_godot_location_pairs[i].android_code == p_code) { + return android_godot_location_pairs[i].godot_code; + } + } + return KeyLocation::UNSPECIFIED; +} diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h index 5cf5628a8b..77c0911f2b 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -177,4 +177,24 @@ static AndroidGodotCodePair android_godot_code_pairs[] = { Key godot_code_from_android_code(unsigned int p_code); +// Key location determination. +struct AndroidGodotLocationPair { + unsigned int android_code = 0; + KeyLocation godot_code = KeyLocation::UNSPECIFIED; +}; + +static AndroidGodotLocationPair android_godot_location_pairs[] = { + { AKEYCODE_ALT_LEFT, KeyLocation::LEFT }, + { AKEYCODE_ALT_RIGHT, KeyLocation::RIGHT }, + { AKEYCODE_SHIFT_LEFT, KeyLocation::LEFT }, + { AKEYCODE_SHIFT_RIGHT, KeyLocation::RIGHT }, + { AKEYCODE_CTRL_LEFT, KeyLocation::LEFT }, + { AKEYCODE_CTRL_RIGHT, KeyLocation::RIGHT }, + { AKEYCODE_META_LEFT, KeyLocation::LEFT }, + { AKEYCODE_META_RIGHT, KeyLocation::RIGHT }, + { AKEYCODE_MAX, KeyLocation::UNSPECIFIED } +}; + +KeyLocation godot_location_from_android_code(unsigned int p_code); + #endif // ANDROID_KEYS_UTILS_H diff --git a/platform/android/detect.py b/platform/android/detect.py index 2860898e5c..fea8ec3287 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -6,7 +6,7 @@ import subprocess from typing import TYPE_CHECKING if TYPE_CHECKING: - from SCons import Environment + from SCons.Script.SConscript import SConsEnvironment def get_name(): @@ -21,13 +21,14 @@ def get_opts(): from SCons.Variables import BoolVariable return [ - ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), + ("ANDROID_HOME", "Path to the Android SDK", get_env_android_sdk_root()), ( "ndk_platform", 'Target platform (android-<api>, e.g. "android-' + str(get_min_target_api()) + '")', "android-" + str(get_min_target_api()), ), BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False), + BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False), ] @@ -41,17 +42,17 @@ def get_doc_path(): return "doc_classes" -# Return the ANDROID_SDK_ROOT environment variable. +# Return the ANDROID_HOME environment variable. def get_env_android_sdk_root(): - return os.environ.get("ANDROID_SDK_ROOT", -1) + return os.environ.get("ANDROID_HOME", os.environ.get("ANDROID_SDK_ROOT", "")) def get_min_sdk_version(platform): return int(platform.split("-")[1]) -def get_android_ndk_root(env): - return env["ANDROID_SDK_ROOT"] + "/ndk/" + get_ndk_version() +def get_android_ndk_root(env: "SConsEnvironment"): + return env["ANDROID_HOME"] + "/ndk/" + get_ndk_version() # This is kept in sync with the value in 'platform/android/java/app/config.gradle'. @@ -68,14 +69,15 @@ def get_flags(): return [ ("arch", "arm64"), # Default for convenience. ("target", "template_debug"), + ("supported", ["mono"]), ] # Check if Android NDK version is installed # If not, install it. -def install_ndk_if_needed(env): +def install_ndk_if_needed(env: "SConsEnvironment"): print("Checking for Android NDK...") - sdk_root = env["ANDROID_SDK_ROOT"] + sdk_root = env["ANDROID_HOME"] if not os.path.exists(get_android_ndk_root(env)): extension = ".bat" if os.name == "nt" else "" sdkmanager = sdk_root + "/cmdline-tools/latest/bin/sdkmanager" + extension @@ -87,7 +89,7 @@ def install_ndk_if_needed(env): else: print("Cannot find " + sdkmanager) print( - "Please ensure ANDROID_SDK_ROOT is correct and cmdline-tools are installed, or install NDK version " + "Please ensure ANDROID_HOME is correct and cmdline-tools are installed, or install NDK version " + get_ndk_version() + " manually." ) @@ -95,7 +97,7 @@ def install_ndk_if_needed(env): env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env) -def configure(env: "Environment"): +def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] if env["arch"] not in supported_arches: @@ -170,10 +172,6 @@ def configure(env: "Environment"): env["RANLIB"] = compiler_path + "/llvm-ranlib" env["AS"] = compiler_path + "/clang" - # Disable exceptions on template builds - if not env.editor_build: - env.Append(CXXFLAGS=["-fno-exceptions"]) - env.Append( CCFLAGS=( "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() @@ -204,7 +202,7 @@ def configure(env: "Environment"): env.Append(LIBS=["OpenSLES", "EGL", "android", "log", "z", "dl"]) if env["vulkan"]: - env.Append(CPPDEFINES=["VULKAN_ENABLED"]) + env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) if not env["use_volk"]: env.Append(LIBS=["vulkan"]) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 6bd09fe00a..972a7dbe6a 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -67,7 +67,7 @@ String DirAccessJAndroid::get_next() { ERR_FAIL_COND_V(id == 0, ""); if (_dir_next) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ""); + ERR_FAIL_NULL_V(env, ""); jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id); if (!str) { return ""; @@ -84,7 +84,7 @@ String DirAccessJAndroid::get_next() { bool DirAccessJAndroid::current_is_dir() const { if (_dir_is_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id); } else { return false; @@ -94,7 +94,7 @@ bool DirAccessJAndroid::current_is_dir() const { bool DirAccessJAndroid::current_is_hidden() const { if (_current_is_hidden) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id); } return false; @@ -112,7 +112,7 @@ void DirAccessJAndroid::list_dir_end() { int DirAccessJAndroid::get_drive_count() { if (_get_drive_count) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type()); } else { return 0; @@ -122,7 +122,7 @@ int DirAccessJAndroid::get_drive_count() { String DirAccessJAndroid::get_drive(int p_drive) { if (_get_drive) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ""); + ERR_FAIL_NULL_V(env, ""); jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive); if (!j_drive) { return ""; @@ -191,7 +191,7 @@ String DirAccessJAndroid::get_absolute_path(String p_path) { bool DirAccessJAndroid::file_exists(String p_file) { if (_file_exists) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); String path = get_absolute_path(p_file); jstring j_path = env->NewStringUTF(path.utf8().get_data()); @@ -206,7 +206,7 @@ bool DirAccessJAndroid::file_exists(String p_file) { bool DirAccessJAndroid::dir_exists(String p_dir) { if (_dir_exists) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); String path = get_absolute_path(p_dir); jstring j_path = env->NewStringUTF(path.utf8().get_data()); @@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) { } } -Error DirAccessJAndroid::make_dir_recursive(String p_dir) { +Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) { // Check if the directory exists already if (dir_exists(p_dir)) { return ERR_ALREADY_EXISTS; @@ -226,7 +226,7 @@ Error DirAccessJAndroid::make_dir_recursive(String p_dir) { if (_make_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); String path = get_absolute_path(p_dir); jstring j_dir = env->NewStringUTF(path.utf8().get_data()); @@ -249,7 +249,7 @@ Error DirAccessJAndroid::make_dir(String p_dir) { Error DirAccessJAndroid::rename(String p_from, String p_to) { if (_rename) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); String from_path = get_absolute_path(p_from); jstring j_from = env->NewStringUTF(from_path.utf8().get_data()); @@ -273,7 +273,7 @@ Error DirAccessJAndroid::rename(String p_from, String p_to) { Error DirAccessJAndroid::remove(String p_name) { if (_remove) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); String path = get_absolute_path(p_name); jstring j_name = env->NewStringUTF(path.utf8().get_data()); @@ -292,7 +292,7 @@ Error DirAccessJAndroid::remove(String p_name) { uint64_t DirAccessJAndroid::get_space_left() { if (_get_space_left) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); return env->CallLongMethod(dir_access_handler, _get_space_left, get_access_type()); } else { return 0; @@ -331,7 +331,7 @@ DirAccessJAndroid::~DirAccessJAndroid() { int DirAccessJAndroid::dir_open(String p_path) { if (_dir_open) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); String path = get_absolute_path(p_path); jstring js = env->NewStringUTF(path.utf8().get_data()); @@ -346,7 +346,7 @@ int DirAccessJAndroid::dir_open(String p_path) { void DirAccessJAndroid::dir_close(int p_id) { if (_dir_close) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id); } } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 5ee4c85659..9aaa78f38c 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -77,7 +77,7 @@ public: virtual bool dir_exists(String p_dir) override; virtual Error make_dir(String p_dir) override; - virtual Error make_dir_recursive(String p_dir) override; + virtual Error make_dir_recursive(const String &p_dir) override; virtual Error rename(String p_from, String p_to) override; virtual Error remove(String p_name) override; diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index f02b292868..01ecbc7164 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -37,11 +37,13 @@ #include "core/config/project_settings.h" -#if defined(VULKAN_ENABLED) -#include "vulkan_context_android.h" - -#include "drivers/vulkan/rendering_device_vulkan.h" +#if defined(RD_ENABLED) #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" +#include "servers/rendering/rendering_device.h" + +#if defined(VULKAN_ENABLED) +#include "rendering_context_driver_vulkan_android.h" +#endif #endif #ifdef GLES3_ENABLED @@ -111,6 +113,30 @@ void DisplayServerAndroid::tts_stop() { TTS_Android::stop(); } +bool DisplayServerAndroid::is_dark_mode_supported() const { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + ERR_FAIL_NULL_V(godot_java, false); + + return godot_java->is_dark_mode_supported(); +} + +bool DisplayServerAndroid::is_dark_mode() const { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + ERR_FAIL_NULL_V(godot_java, false); + + return godot_java->is_dark_mode(); +} + +void DisplayServerAndroid::set_system_theme_change_callback(const Callable &p_callable) { + system_theme_changed = p_callable; +} + +void DisplayServerAndroid::emit_system_theme_changed() { + if (system_theme_changed.is_valid()) { + system_theme_changed.call_deferred(); + } +} + void DisplayServerAndroid::clipboard_set(const String &p_text) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); ERR_FAIL_NULL(godot_java); @@ -295,13 +321,10 @@ void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_call void DisplayServerAndroid::_window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred) const { if (!p_callable.is_null()) { - const Variant *argp = &p_arg; - Variant ret; - Callable::CallError ce; if (p_deferred) { - p_callable.callp((const Variant **)&argp, 1, ret, ce); + p_callable.call_deferred(p_arg); } else { - p_callable.call_deferredp((const Variant **)&argp, 1); + p_callable.call(p_arg); } } } @@ -504,36 +527,50 @@ void DisplayServerAndroid::register_android_driver() { } void DisplayServerAndroid::reset_window() { -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_NULL(native_window); +#if defined(RD_ENABLED) + if (rendering_context) { + if (rendering_device) { + rendering_device->screen_free(MAIN_WINDOW_ID); + } + + VSyncMode last_vsync_mode = rendering_context->window_get_vsync_mode(MAIN_WINDOW_ID); + rendering_context->window_destroy(MAIN_WINDOW_ID); - ERR_FAIL_NULL(context_vulkan); - VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID); - context_vulkan->window_destroy(MAIN_WINDOW_ID); + union { +#ifdef VULKAN_ENABLED + RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan; +#endif + } wpd; +#ifdef VULKAN_ENABLED + if (rendering_driver == "vulkan") { + ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); + ERR_FAIL_NULL(native_window); + wpd.vulkan.window = native_window; + } +#endif + + if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) { + ERR_PRINT(vformat("Failed to reset %s window.", rendering_driver)); + memdelete(rendering_context); + rendering_context = nullptr; + return; + } Size2i display_size = OS_Android::get_singleton()->get_display_size(); - if (context_vulkan->window_create(native_window, last_vsync_mode, display_size.width, display_size.height) != OK) { - memdelete(context_vulkan); - context_vulkan = nullptr; - ERR_FAIL_MSG("Failed to reset Vulkan window."); + rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height); + rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, last_vsync_mode); + + if (rendering_device) { + rendering_device->screen_create(MAIN_WINDOW_ID); } } #endif } void DisplayServerAndroid::notify_surface_changed(int p_width, int p_height) { - if (rect_changed_callback.is_null()) { - return; + if (rect_changed_callback.is_valid()) { + rect_changed_callback.call(Rect2i(0, 0, p_width, p_height)); } - - const Variant size = Rect2i(0, 0, p_width, p_height); - const Variant *sizep = &size; - Variant ret; - Callable::CallError ce; - - rect_changed_callback.callp(reinterpret_cast<const Variant **>(&sizep), 1, ret, ce); } DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error) { @@ -543,34 +580,55 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis #if defined(GLES3_ENABLED) if (rendering_driver == "opengl3") { - RasterizerGLES3::make_current(); + RasterizerGLES3::make_current(false); } #endif -#if defined(VULKAN_ENABLED) - context_vulkan = nullptr; - rendering_device_vulkan = nullptr; +#if defined(RD_ENABLED) + rendering_context = nullptr; + rendering_device = nullptr; +#if defined(VULKAN_ENABLED) if (rendering_driver == "vulkan") { - ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_NULL(native_window); - - context_vulkan = memnew(VulkanContextAndroid); - if (context_vulkan->initialize() != OK) { - memdelete(context_vulkan); - context_vulkan = nullptr; - ERR_FAIL_MSG("Failed to initialize Vulkan context"); + rendering_context = memnew(RenderingContextDriverVulkanAndroid); + } +#endif + + if (rendering_context) { + if (rendering_context->initialize() != OK) { + ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); + memdelete(rendering_context); + rendering_context = nullptr; + return; } - Size2i display_size = OS_Android::get_singleton()->get_display_size(); - if (context_vulkan->window_create(native_window, p_vsync_mode, display_size.width, display_size.height) != OK) { - memdelete(context_vulkan); - context_vulkan = nullptr; - ERR_FAIL_MSG("Failed to create Vulkan window."); + union { +#ifdef VULKAN_ENABLED + RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan; +#endif + } wpd; +#ifdef VULKAN_ENABLED + if (rendering_driver == "vulkan") { + ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); + ERR_FAIL_NULL(native_window); + wpd.vulkan.window = native_window; } +#endif - rendering_device_vulkan = memnew(RenderingDeviceVulkan); - rendering_device_vulkan->initialize(context_vulkan); + if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) { + ERR_PRINT(vformat("Failed to create %s window.", rendering_driver)); + memdelete(rendering_context); + rendering_context = nullptr; + return; + } + + Size2i display_size = OS_Android::get_singleton()->get_display_size(); + rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height); + rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, p_vsync_mode); + + rendering_device = memnew(RenderingDevice); + rendering_device->initialize(rendering_context, MAIN_WINDOW_ID); + rendering_device->screen_create(MAIN_WINDOW_ID); RendererCompositorRD::make_current(); } @@ -583,16 +641,12 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis } DisplayServerAndroid::~DisplayServerAndroid() { -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - if (rendering_device_vulkan) { - rendering_device_vulkan->finalize(); - memdelete(rendering_device_vulkan); - } - - if (context_vulkan) { - memdelete(context_vulkan); - } +#if defined(RD_ENABLED) + if (rendering_device) { + memdelete(rendering_device); + } + if (rendering_context) { + memdelete(rendering_context); } #endif } @@ -683,17 +737,17 @@ void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor } void DisplayServerAndroid::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { -#if defined(VULKAN_ENABLED) - if (context_vulkan) { - context_vulkan->set_vsync_mode(p_window, p_vsync_mode); +#if defined(RD_ENABLED) + if (rendering_context) { + rendering_context->window_set_vsync_mode(p_window, p_vsync_mode); } #endif } DisplayServer::VSyncMode DisplayServerAndroid::window_get_vsync_mode(WindowID p_window) const { -#if defined(VULKAN_ENABLED) - if (context_vulkan) { - return context_vulkan->get_vsync_mode(p_window); +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_vsync_mode(p_window); } #endif return DisplayServer::VSYNC_ENABLED; diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index e0ad2cb916..c95eaddf93 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -33,9 +33,9 @@ #include "servers/display_server.h" -#if defined(VULKAN_ENABLED) -class VulkanContextAndroid; -class RenderingDeviceVulkan; +#if defined(RD_ENABLED) +class RenderingContextDriver; +class RenderingDevice; #endif class DisplayServerAndroid : public DisplayServer { @@ -72,9 +72,9 @@ class DisplayServerAndroid : public DisplayServer { CursorShape cursor_shape = CursorShape::CURSOR_ARROW; -#if defined(VULKAN_ENABLED) - VulkanContextAndroid *context_vulkan = nullptr; - RenderingDeviceVulkan *rendering_device_vulkan = nullptr; +#if defined(RD_ENABLED) + RenderingContextDriver *rendering_context = nullptr; + RenderingDevice *rendering_device = nullptr; #endif ObjectID window_attached_instance_id; @@ -84,6 +84,8 @@ class DisplayServerAndroid : public DisplayServer { Callable input_text_callback; Callable rect_changed_callback; + Callable system_theme_changed; + void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const; static void _dispatch_input_events(const Ref<InputEvent> &p_event); @@ -103,6 +105,12 @@ public: virtual void tts_resume() override; virtual void tts_stop() override; + void emit_system_theme_changed(); + + virtual bool is_dark_mode_supported() const override; + virtual bool is_dark_mode() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; + virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; virtual bool clipboard_has() const override; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index 6d3affed15..64485afeb0 100644 --- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -7,7 +7,8 @@ </description> <tutorials> <link title="Exporting for Android">$DOCS_URL/tutorials/export/exporting_for_android.html</link> - <link title="Custom builds for Android">$DOCS_URL/tutorials/export/android_custom_build.html</link> + <link title="Gradle builds for Android">$DOCS_URL/tutorials/export/android_gradle_build.html</link> + <link title="Android plugins documentation index">$DOCS_URL/tutorials/platform/index.html</link> </tutorials> <members> <member name="apk_expansion/SALT" type="String" setter="" getter=""> @@ -36,14 +37,26 @@ A list of additional command line arguments, exported project will receive when started. </member> <member name="custom_template/debug" type="String" setter="" getter=""> - Path to the custom export template. If left empty, default template is used. + Path to an APK file to use as a custom export template for debug exports. If left empty, default template is used. + [b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled. </member> <member name="custom_template/release" type="String" setter="" getter=""> - Path to the custom export template. If left empty, default template is used. + Path to an APK file to use as a custom export template for release exports. If left empty, default template is used. + [b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled. + </member> + <member name="gradle_build/android_source_template" type="String" setter="" getter=""> + Path to a ZIP file holding the source for the export template used in a Gradle build. If left empty, the default template is used. + </member> + <member name="gradle_build/compress_native_libraries" type="bool" setter="" getter=""> + If [code]true[/code], native libraries are compressed when performing a Gradle build. + [b]Note:[/b] Although your binary may be smaller, your application may load slower because the native libraries are not loaded directly from the binary at runtime. </member> <member name="gradle_build/export_format" type="int" setter="" getter=""> Export format for Gradle build. </member> + <member name="gradle_build/gradle_build_directory" type="String" setter="" getter=""> + Path to the Gradle build directory. If left empty, then [code]res://android[/code] will be used. + </member> <member name="gradle_build/min_sdk" type="String" setter="" getter=""> Minimal Android SDK version for Gradle build. </member> @@ -104,11 +117,24 @@ <member name="package/retain_data_on_uninstall" type="bool" setter="" getter=""> If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. </member> + <member name="package/show_as_launcher_app" type="bool" setter="" getter=""> + If [code]true[/code], the user will be able to set this app as the system launcher in Android preferences. + </member> + <member name="package/show_in_android_tv" type="bool" setter="" getter=""> + If [code]true[/code], this app will show in Android TV launcher UI. + </member> + <member name="package/show_in_app_library" type="bool" setter="" getter=""> + If [code]true[/code], this app will show in the device's app library. + [b]Note:[/b] This is [code]true[/code] by default. + </member> <member name="package/signed" type="bool" setter="" getter=""> If [code]true[/code], package signing is enabled. </member> <member name="package/unique_name" type="String" setter="" getter=""> - Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]). + Unique application identifier in a reverse-DNS format. The reverse DNS format should preferably match a domain name you control, but this is not strictly required. For instance, if you own [code]example.com[/code], your package unique name should preferably be of the form [code]com.example.mygame[/code]. This identifier can only contain lowercase alphanumeric characters ([code]a-z[/code], and [code]0-9[/code]), underscores ([code]_[/code]), and periods ([code].[/code]). Each component of the reverse DNS format must start with a letter: for instance, [code]com.example.8game[/code] is not valid. + If [code]$genname[/code] is present in the value, it will be replaced by the project name converted to lowercase. If there are invalid characters in the project name, they will be stripped. If all characters in the project name are stripped, [code]$genname[/code] is replaced by [code]noname[/code]. + [b]Note:[/b] Changing the package name will cause the package to be considered as a new package, with its own installation and data paths. The new package won't be usable to update existing installations. + [b]Note:[/b] When publishing to Google Play, the package name must be [i]globally[/i] unique. This means no other apps published on Google Play must be using the same package name as yours. Otherwise, you'll be prevented from publishing your app on Google Play. </member> <member name="permissions/access_checkin_properties" type="bool" setter="" getter=""> Allows read/write access to the "properties" table in the checkin database. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_CHECKIN_PROPERTIES]ACCESS_CHECKIN_PROPERTIES[/url]. @@ -144,7 +170,7 @@ Allows an application to act as an AccountAuthenticator for the AccountManager. </member> <member name="permissions/battery_stats" type="bool" setter="" getter=""> - Allows an application to collect battery statistics. Sett [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. + Allows an application to collect battery statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. </member> <member name="permissions/bind_accessibility_service" type="bool" setter="" getter=""> Must be required by an AccessibilityService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_ACCESSIBILITY_SERVICE]BIND_ACCESSIBILITY_SERVICE[/url]. @@ -573,16 +599,10 @@ If [code]true[/code], allows the application to participate in the backup and restore infrastructure. </member> <member name="version/code" type="int" setter="" getter=""> - Machine-readable application version. + Machine-readable application version. This must be incremented for every new release pushed to the Play Store. </member> <member name="version/name" type="String" setter="" getter=""> - Application version visible to the user. - </member> - <member name="xr_features/hand_tracking" type="int" setter="" getter=""> - </member> - <member name="xr_features/hand_tracking_frequency" type="int" setter="" getter=""> - </member> - <member name="xr_features/passthrough" type="int" setter="" getter=""> + Application version visible to the user. Falls back to [member ProjectSettings.application/config/version] if left empty. </member> <member name="xr_features/xr_mode" type="int" setter="" getter=""> </member> diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 32b46271fd..138714634f 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -42,17 +42,23 @@ void register_android_exporter_types() { void register_android_exporter() { #ifndef ANDROID_ENABLED - EDITOR_DEF("export/android/android_sdk_path", ""); + EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME")); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); + EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/debug_keystore", ""); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks")); EDITOR_DEF("export/android/debug_keystore_user", "androiddebugkey"); EDITOR_DEF("export/android/debug_keystore_pass", "android"); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD)); EDITOR_DEF("export/android/force_system_user", false); EDITOR_DEF("export/android/shutdown_adb_on_exit", true); EDITOR_DEF("export/android/one_click_deploy_clear_previous_install", false); + + EDITOR_DEF("export/android/use_wifi_for_remote_debug", false); + EDITOR_DEF("export/android/wifi_remote_debug_host", "localhost"); #endif Ref<EditorExportPlatformAndroid> exporter = Ref<EditorExportPlatformAndroid>(memnew(EditorExportPlatformAndroid)); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 830548d801..28ab8e3335 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -45,9 +45,12 @@ #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" +#include "editor/export/export_template_manager.h" +#include "editor/import/resource_importer_texture_settings.h" +#include "editor/themes/editor_scale.h" #include "main/splash.gen.h" +#include "scene/resources/image_texture.h" #include "modules/modules_enabled.gen.h" // For mono and svg. #ifdef MODULE_SVG_ENABLED @@ -206,12 +209,14 @@ static const char *android_perms[] = { nullptr }; +static const char *MISMATCHED_VERSIONS_MESSAGE = "Android build version mismatch:\n| Template installed: %s\n| Requested version: %s\nPlease reinstall Android build template from 'Project' menu."; + static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi/splash.png"; static const char *LEGACY_BUILD_SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi-v4/splash.png"; static const char *SPLASH_BG_COLOR_PATH = "res/drawable-nodpi/splash_bg_color.png"; static const char *LEGACY_BUILD_SPLASH_BG_COLOR_PATH = "res/drawable-nodpi-v4/splash_bg_color.png"; -static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash_drawable.xml"; -static const char *GDEXTENSION_LIBS_PATH = "res://android/build/libs/gdextensionlibs.json"; +static const char *SPLASH_CONFIG_PATH = "res/drawable/splash_drawable.xml"; +static const char *GDEXTENSION_LIBS_PATH = "libs/gdextensionlibs.json"; static const int icon_densities_count = 6; static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192"); @@ -248,46 +253,48 @@ static const LauncherIcon launcher_adaptive_icon_backgrounds[icon_densities_coun static const int EXPORT_FORMAT_APK = 0; static const int EXPORT_FORMAT_AAB = 1; -static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets"; -static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets"; +static const char *APK_ASSETS_DIRECTORY = "assets"; +static const char *AAB_ASSETS_DIRECTORY = "assetPacks/installTime/src/main/assets"; static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' static const int VULKAN_MIN_SDK_VERSION = 24; -static const int DEFAULT_TARGET_SDK_VERSION = 33; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' +static const int DEFAULT_TARGET_SDK_VERSION = 34; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' #ifndef ANDROID_ENABLED void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); while (!ea->quit_request.is_set()) { - // Check for plugins updates +#ifndef DISABLE_DEPRECATED + // Check for android plugins updates { // Nothing to do if we already know the plugins have changed. - if (!ea->plugins_changed.is_set()) { + if (!ea->android_plugins_changed.is_set()) { Vector<PluginConfigAndroid> loaded_plugins = get_plugins(); - MutexLock lock(ea->plugins_lock); + MutexLock lock(ea->android_plugins_lock); - if (ea->plugins.size() != loaded_plugins.size()) { - ea->plugins_changed.set(); + if (ea->android_plugins.size() != loaded_plugins.size()) { + ea->android_plugins_changed.set(); } else { - for (int i = 0; i < ea->plugins.size(); i++) { - if (ea->plugins[i].name != loaded_plugins[i].name) { - ea->plugins_changed.set(); + for (int i = 0; i < ea->android_plugins.size(); i++) { + if (ea->android_plugins[i].name != loaded_plugins[i].name) { + ea->android_plugins_changed.set(); break; } } } - if (ea->plugins_changed.is_set()) { - ea->plugins = loaded_plugins; + if (ea->android_plugins_changed.is_set()) { + ea->android_plugins = loaded_plugins; } } } +#endif // DISABLE_DEPRECATED // Check for devices updates String adb = get_adb_path(); - if (FileAccess::exists(adb)) { + if (ea->has_runnable_preset.is_set() && FileAccess::exists(adb)) { String devices; List<String> args; args.push_back("devices"); @@ -419,6 +426,25 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { OS::get_singleton()->execute(adb, args); } } + +void EditorExportPlatformAndroid::_update_preset_status() { + const int preset_count = EditorExport::get_singleton()->get_export_preset_count(); + bool has_runnable = false; + + for (int i = 0; i < preset_count; i++) { + const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i); + if (preset->get_platform() == this && preset->is_runnable()) { + has_runnable = true; + break; + } + } + + if (has_runnable) { + has_runnable_preset.set(); + } else { + has_runnable_preset.clear(); + } +} #endif String EditorExportPlatformAndroid::get_project_name(const String &p_name) const { @@ -456,7 +482,7 @@ String EditorExportPlatformAndroid::get_valid_basename() const { if (is_digit(c) && first) { continue; } - if (is_ascii_alphanumeric_char(c)) { + if (is_ascii_identifier_char(c)) { name += String::chr(c); first = false; } @@ -470,7 +496,8 @@ String EditorExportPlatformAndroid::get_valid_basename() const { } String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const { - return p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY; + String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset); + return gradle_build_directory.path_join(p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY); } bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, String *r_error) const { @@ -533,13 +560,6 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, return false; } - if (p_package.find("$genname") >= 0 && !is_project_name_valid()) { - if (r_error) { - *r_error = TTR("The project name does not meet the requirement for the package name format. Please explicitly specify the package name."); - } - return false; - } - return true; } @@ -627,6 +647,7 @@ Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_abis() return abis; } +#ifndef DISABLE_DEPRECATED /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; @@ -693,6 +714,7 @@ Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_enabled_plugins(con return enabled_plugins; } +#endif // DISABLE_DEPRECATED Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method) { zip_fileinfo zipfi = get_zip_fileinfo(); @@ -775,11 +797,10 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared } if (abi_index != -1) { exported = true; - String base = "res://android/build/libs"; String type = export_data->debug ? "debug" : "release"; String abi = abis[abi_index].abi; String filename = p_so.path.get_file(); - String dst_path = base.path_join(type).path_join(abi).path_join(filename); + String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename); Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path); print_verbose("Copying .so file from " + p_so.path + " to " + dst_path); Error err = store_file_at_path(dst_path, data); @@ -806,6 +827,16 @@ bool EditorExportPlatformAndroid::_uses_vulkan() { return uses_vulkan; } +void EditorExportPlatformAndroid::_notification(int p_what) { +#ifndef ANDROID_ENABLED + if (p_what == NOTIFICATION_POSTINITIALIZE) { + if (EditorExport::get_singleton()) { + EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status)); + } + } +#endif +} + void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) { const char **aperms = android_perms; while (*aperms) { @@ -827,16 +858,6 @@ void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> r_permissions.push_back("android.permission.INTERNET"); } } - - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - if (xr_mode_index == XR_MODE_OPENXR) { - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required - if (hand_tracking_index > XR_HAND_TRACKING_NONE) { - if (r_permissions.find("com.oculus.permission.HAND_TRACKING") == -1) { - r_permissions.push_back("com.oculus.permission.HAND_TRACKING"); - } - } - } } void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) { @@ -860,10 +881,25 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres } } - manifest_text += _get_xr_features_tag(p_preset, _uses_vulkan()); - manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms)); + if (_uses_vulkan()) { + manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; + manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; + } + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + const String contents = export_plugins[i]->get_android_manifest_element_contents(Ref<EditorExportPlatform>(this), p_debug); + if (!contents.is_empty()) { + manifest_text += contents; + manifest_text += "\n"; + } + } + } + + manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug); manifest_text += "</manifest>\n"; - String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); + String manifest_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join(vformat("src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"))); print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text); store_string_at_path(manifest_path, manifest_text); @@ -894,7 +930,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p uint32_t string_table_ends = 0; Vector<uint8_t> stable_extra; - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); int version_code = p_preset->get("version/code"); String package_name = p_preset->get("package/unique_name"); @@ -1107,7 +1143,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } for (int i = 0; i < feature_names.size(); i++) { - String feature_name = feature_names[i]; + const String &feature_name = feature_names[i]; bool feature_required = feature_required_list[i]; int feature_version = feature_versions[i]; bool has_version_attribute = feature_version != -1; @@ -1531,18 +1567,32 @@ void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_n String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) { bool scale_splash = GLOBAL_GET("application/boot_splash/fullsize"); bool apply_filter = GLOBAL_GET("application/boot_splash/use_filter"); + bool show_splash_image = GLOBAL_GET("application/boot_splash/show_image"); String project_splash_path = GLOBAL_GET("application/boot_splash/image"); - if (!project_splash_path.is_empty()) { - splash_image.instantiate(); - print_verbose("Loading splash image: " + project_splash_path); - const Error err = ImageLoader::load_image(project_splash_path, splash_image); - if (err) { - if (OS::get_singleton()->is_stdout_verbose()) { - print_error("- unable to load splash image from " + project_splash_path + " (" + itos(err) + ")"); + // Setup the splash bg color. + bool bg_color_valid = false; + Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid); + if (!bg_color_valid) { + bg_color = boot_splash_bg_color; + } + + if (show_splash_image) { + if (!project_splash_path.is_empty()) { + splash_image.instantiate(); + print_verbose("Loading splash image: " + project_splash_path); + const Error err = ImageLoader::load_image(project_splash_path, splash_image); + if (err) { + if (OS::get_singleton()->is_stdout_verbose()) { + print_error("- unable to load splash image from " + project_splash_path + " (" + itos(err) + ")"); + } + splash_image.unref(); } - splash_image.unref(); } + } else { + splash_image.instantiate(); + splash_image->initialize_data(1, 1, false, Image::FORMAT_RGBA8); + splash_image->set_pixel(0, 0, bg_color); } if (splash_image.is_null()) { @@ -1566,13 +1616,6 @@ String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, R splash_image->resize(width, height); } - // Setup the splash bg color - bool bg_color_valid; - Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid); - if (!bg_color_valid) { - bg_color = boot_splash_bg_color; - } - print_verbose("Creating splash background color image."); splash_bg_color_image.instantiate(); splash_bg_color_image->initialize_data(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); @@ -1594,7 +1637,11 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> & print_verbose("Loading regular icon from " + path); if (path.is_empty() || ImageLoader::load_image(path, icon) != OK) { print_verbose("- falling back to project icon: " + project_icon_path); - ImageLoader::load_image(project_icon_path, icon); + if (!project_icon_path.is_empty()) { + ImageLoader::load_image(project_icon_path, icon); + } else { + ERR_PRINT("No project icon specified. Please specify one in the Project Settings under Application -> Config -> Icon"); + } } // Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default). @@ -1613,15 +1660,6 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> & } } -void EditorExportPlatformAndroid::store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) { - store_image(launcher_icon.export_path, data); -} - -void EditorExportPlatformAndroid::store_image(const String &export_path, const Vector<uint8_t> &data) { - String img_path = export_path.insert(0, "res://android/build/"); - store_file_at_path(img_path, data); -} - void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, const String &processed_splash_config_xml, const Ref<Image> &splash_image, @@ -1629,26 +1667,30 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor const Ref<Image> &main_image, const Ref<Image> &foreground, const Ref<Image> &background) { + String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset); + // Store the splash configuration if (!processed_splash_config_xml.is_empty()) { print_verbose("Storing processed splash configuration: " + String("\n") + processed_splash_config_xml); - store_string_at_path(SPLASH_CONFIG_PATH, processed_splash_config_xml); + store_string_at_path(gradle_build_dir.path_join(SPLASH_CONFIG_PATH), processed_splash_config_xml); } // Store the splash image if (splash_image.is_valid() && !splash_image->is_empty()) { - print_verbose("Storing splash image in " + String(SPLASH_IMAGE_EXPORT_PATH)); + String splash_export_path = gradle_build_dir.path_join(SPLASH_IMAGE_EXPORT_PATH); + print_verbose("Storing splash image in " + splash_export_path); Vector<uint8_t> data; _load_image_data(splash_image, data); - store_image(SPLASH_IMAGE_EXPORT_PATH, data); + store_file_at_path(splash_export_path, data); } // Store the splash bg color image if (splash_bg_color_image.is_valid() && !splash_bg_color_image->is_empty()) { - print_verbose("Storing splash background image in " + String(SPLASH_BG_COLOR_PATH)); + String splash_bg_color_path = gradle_build_dir.path_join(SPLASH_BG_COLOR_PATH); + print_verbose("Storing splash background image in " + splash_bg_color_path); Vector<uint8_t> data; _load_image_data(splash_bg_color_image, data); - store_image(SPLASH_BG_COLOR_PATH, data); + store_file_at_path(splash_bg_color_path, data); } // Prepare images to be resized for the icons. If some image ends up being uninitialized, @@ -1659,7 +1701,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor print_verbose("Processing launcher icon for dimension " + itos(launcher_icons[i].dimensions) + " into " + launcher_icons[i].export_path); Vector<uint8_t> data; _process_launcher_icons(launcher_icons[i].export_path, main_image, launcher_icons[i].dimensions, data); - store_image(launcher_icons[i], data); + store_file_at_path(gradle_build_dir.path_join(launcher_icons[i].export_path), data); } if (foreground.is_valid() && !foreground->is_empty()) { @@ -1667,7 +1709,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor Vector<uint8_t> data; _process_launcher_icons(launcher_adaptive_icon_foregrounds[i].export_path, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); - store_image(launcher_adaptive_icon_foregrounds[i], data); + store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_foregrounds[i].export_path), data); } if (background.is_valid() && !background->is_empty()) { @@ -1675,7 +1717,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor Vector<uint8_t> data; _process_launcher_icons(launcher_adaptive_icon_backgrounds[i].export_path, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); - store_image(launcher_adaptive_icon_backgrounds[i], data); + store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_backgrounds[i].export_path), data); } } } @@ -1699,7 +1741,6 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres Vector<ABI> abis = get_enabled_abis(p_preset); for (int i = 0; i < abis.size(); ++i) { r_features->push_back(abis[i].arch); - r_features->push_back(abis[i].abi); } } @@ -1720,7 +1761,7 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport } } else if (p_name == "gradle_build/use_gradle_build") { bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); - String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(Ref<EditorExportPreset>(p_preset))); + String enabled_plugins_names = _get_plugins_names(Ref<EditorExportPreset>(p_preset)); if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { return TTR("\"Use Gradle Build\" must be enabled to use the plugins."); } @@ -1730,21 +1771,10 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { return TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); } - } else if (p_name == "xr_features/hand_tracking") { - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int hand_tracking = p_preset->get("xr_features/hand_tracking"); - if (xr_mode_index != XR_MODE_OPENXR) { - if (hand_tracking > XR_HAND_TRACKING_NONE) { - return TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); - } - } - } else if (p_name == "xr_features/passthrough") { - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (xr_mode_index != XR_MODE_OPENXR) { - if (passthrough_mode > XR_PASSTHROUGH_NONE) { - return TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); - } + } else if (p_name == "gradle_build/compress_native_libraries") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) { + return TTR("\"Compress Native Libraries\" is only valid when \"Use Gradle Build\" is enabled."); } } else if (p_name == "gradle_build/export_format") { bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); @@ -1800,19 +1830,24 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/compress_native_libraries"), false, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true)); // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465). // This implies doing validation that the string is a proper int. r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true)); +#ifndef DISABLE_DEPRECATED Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { print_verbose("Found Android plugin " + plugins_configs[i].name); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false)); } - plugins_changed.clear(); + android_plugins_changed.clear(); +#endif // DISABLE_DEPRECATED // Android supports multiple architectures in an app bundle, so // we expose each option as a checkbox in the export dialog. @@ -1827,20 +1862,23 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "com.example.$genname", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/retain_data_on_uninstall"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/exclude_from_recents"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_android_tv"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_app_library"), true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_as_launcher_app"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_icon_option, PROPERTY_HINT_FILE, "*.png"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_foreground_option, PROPERTY_HINT_FILE, "*.png"), "")); @@ -1849,9 +1887,6 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE, false, true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking_frequency", PROPERTY_HINT_ENUM, "Low,High"), XR_HAND_TRACKING_FREQUENCY_LOW)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true)); @@ -1876,6 +1911,27 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio } } +bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + if (p_preset == nullptr) { + return true; + } + + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); + if (p_option == "graphics/opengl_debug" || + p_option == "command_line/extra_args" || + p_option == "permissions/custom_permissions") { + return advanced_options_enabled; + } + if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") { + return advanced_options_enabled && bool(p_preset->get("gradle_build/use_gradle_build")); + } + if (p_option == "custom_template/debug" || p_option == "custom_template/release") { + // The APK templates are ignored if Gradle build is enabled. + return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build")); + } + return true; +} + String EditorExportPlatformAndroid::get_name() const { return "Android"; } @@ -1889,12 +1945,14 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_logo() const { } bool EditorExportPlatformAndroid::should_update_export_options() { - bool export_options_changed = plugins_changed.is_set(); - if (export_options_changed) { +#ifndef DISABLE_DEPRECATED + if (android_plugins_changed.is_set()) { // don't clear unless we're reporting true, to avoid race - plugins_changed.clear(); + android_plugins_changed.clear(); + return true; } - return export_options_changed; +#endif // DISABLE_DEPRECATED + return false; } bool EditorExportPlatformAndroid::poll_export() { @@ -1955,8 +2013,9 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, return ERR_SKIP; } + const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug"); const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT); - const bool use_reverse = devices[p_device].api_level >= 21; + const bool use_reverse = devices[p_device].api_level >= 21 && !use_wifi_for_remote_debug; if (use_reverse) { p_debug_flags |= DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST; @@ -1983,7 +2042,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, String output; bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install"); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String package_name = p_preset->get("package/unique_name"); if (remove_prev) { @@ -2069,7 +2128,10 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, print_line("Reverse result2: " + itos(rv)); } } else { - static const char *const msg = "--- Device API < 21; debugging over Wi-Fi ---"; + static const char *const api_version_msg = "--- Device API < 21; debugging over Wi-Fi ---"; + static const char *const manual_override_msg = "--- Wi-Fi remote debug enabled in project settings; debugging over Wi-Fi ---"; + + const char *const msg = use_wifi_for_remote_debug ? manual_override_msg : api_version_msg; EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR); print_line(String(msg).to_upper()); } @@ -2109,8 +2171,17 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const { return run_icon; } +String EditorExportPlatformAndroid::get_java_path() { + String exe_ext; + if (OS::get_singleton()->get_name() == "Windows") { + exe_ext = ".exe"; + } + String java_sdk_path = EDITOR_GET("export/android/java_sdk_path"); + return java_sdk_path.path_join("bin/java" + exe_ext); +} + String EditorExportPlatformAndroid::get_adb_path() { - String exe_ext = ""; + String exe_ext; if (OS::get_singleton()->get_name() == "Windows") { exe_ext = ".exe"; } @@ -2122,13 +2193,13 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_ if (p_target_sdk == -1) { p_target_sdk = DEFAULT_TARGET_SDK_VERSION; } - String exe_ext = ""; + String exe_ext; if (OS::get_singleton()->get_name() == "Windows") { exe_ext = ".bat"; } String apksigner_command_name = "apksigner" + exe_ext; String sdk_path = EDITOR_GET("export/android/android_sdk_path"); - String apksigner_path = ""; + String apksigner_path; Error errn; String build_tools_dir = sdk_path.path_join("build-tools"); @@ -2227,17 +2298,75 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_ return apksigner_path; } +static bool has_valid_keystore_credentials(String &r_error_str, const String &p_keystore, const String &p_username, const String &p_password, const String &p_type) { + String output; + List<String> args; + args.push_back("-list"); + args.push_back("-keystore"); + args.push_back(p_keystore); + args.push_back("-storepass"); + args.push_back(p_password); + args.push_back("-alias"); + args.push_back(p_username); + Error error = OS::get_singleton()->execute("keytool", args, &output, nullptr, true); + String keytool_error = "keytool error:"; + bool valid = output.substr(0, keytool_error.length()) != keytool_error; + + if (error != OK) { + r_error_str = TTR("Error: There was a problem validating the keystore username and password"); + return false; + } + if (!valid) { + r_error_str = TTR(p_type + " Username and/or Password is invalid for the given " + p_type + " Keystore"); + return false; + } + r_error_str = ""; + return true; +} + +bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) { + String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); + String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); + String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); + String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); + + bool valid = true; + if (!dk.is_empty() && !dk_user.is_empty() && !dk_password.is_empty()) { + String err = ""; + valid = has_valid_keystore_credentials(err, dk, dk_user, dk_password, "Debug"); + r_error += err; + } + if (!rk.is_empty() && !rk_user.is_empty() && !rk_password.is_empty()) { + String err = ""; + valid = has_valid_keystore_credentials(err, rk, rk_user, rk_password, "Release"); + r_error += err; + } + return valid; +} + bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { String err; bool valid = false; const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); #ifdef MODULE_MONO_ENABLED - err += TTR("Exporting to Android is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Android with C#/Mono instead.") + "\n"; - err += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n"; - // Don't check for additional errors, as this particular error cannot be resolved. - r_error = err; - return false; + // Android export is still a work in progress, keep a message as a warning. + err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n"; + + bool unsupported_arch = false; + Vector<ABI> enabled_abis = get_enabled_abis(p_preset); + for (ABI abi : enabled_abis) { + if (abi.arch != "arm64" && abi.arch != "x86_64") { + err += vformat(TTR("Android architecture %s not supported in C# projects."), abi.arch) + "\n"; + unsupported_arch = true; + } + } + if (unsupported_arch) { + r_error = err; + return false; + } #endif // Look for export templates (first official, and if defined custom templates). @@ -2272,7 +2401,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += template_err; } } else { - bool installed_android_build_template = FileAccess::exists("res://android/build/build.gradle"); + bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle")); if (!installed_android_build_template) { r_missing_templates = !exists_export_template("android_source.zip", &err); err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n"; @@ -2317,6 +2446,32 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += TTR("Release keystore incorrectly configured in the export preset.") + "\n"; } + String java_sdk_path = EDITOR_GET("export/android/java_sdk_path"); + if (java_sdk_path.is_empty()) { + err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n"; + valid = false; + } else { + // Validate the given path by checking that `java` is present under the `bin` directory. + Error errn; + // Check for the bin directory. + Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn); + if (errn != OK) { + err += TTR("Invalid Java SDK path in Editor Settings."); + err += TTR("Missing 'bin' directory!"); + err += "\n"; + valid = false; + } else { + // Check for the `java` command. + String java_path = get_java_path(); + if (!FileAccess::exists(java_path)) { + err += TTR("Unable to find 'java' command using the Java SDK path."); + err += TTR("Please check the Java SDK directory specified in Editor Settings."); + err += "\n"; + valid = false; + } + } + } + String sdk_path = EDITOR_GET("export/android/android_sdk_path"); if (sdk_path.is_empty()) { err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n"; @@ -2389,10 +2544,21 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit } } - String etc_error = test_etc2(); - if (!etc_error.is_empty()) { + if (!ResourceImporterTextureSettings::should_import_etc2_astc()) { valid = false; - err += etc_error; + } + + if (p_preset->get("gradle_build/use_gradle_build")) { + String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version"); + Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ); + if (f.is_valid()) { + String current_version = ExportTemplateManager::get_android_template_identifier(p_preset); + String installed_version = f->get_line().strip_edges(); + if (current_version != installed_version) { + err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version); + err += "\n"; + } + } } String min_sdk_str = p_preset->get("gradle_build/min_sdk"); @@ -2429,6 +2595,13 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit err += "\n"; } + String package_name = p_preset->get("package/unique_name"); + if (package_name.find("$genname") >= 0 && !is_project_name_valid()) { + // Warning only, so don't override `valid`. + err += vformat(TTR("The project name does not meet the requirement for the package name format and will be updated to \"%s\". Please explicitly specify the package name if needed."), get_valid_basename()); + err += "\n"; + } + r_error = err; return valid; } @@ -2630,30 +2803,37 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre return OK; } -void EditorExportPlatformAndroid::_clear_assets_directory() { +void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) { Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset); // Clear the APK assets directory - if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { + String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY); + if (da_res->dir_exists(apk_assets_directory)) { print_verbose("Clearing APK assets directory..."); - Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory); + ERR_FAIL_COND(da_assets.is_null()); + da_assets->erase_contents_recursive(); - da_res->remove(APK_ASSETS_DIRECTORY); + da_res->remove(apk_assets_directory); } // Clear the AAB assets directory - if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { + String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY); + if (da_res->dir_exists(aab_assets_directory)) { print_verbose("Clearing AAB assets directory..."); - Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory); + ERR_FAIL_COND(da_assets.is_null()); + da_assets->erase_contents_recursive(); - da_res->remove(AAB_ASSETS_DIRECTORY); + da_res->remove(aab_assets_directory); } } -void EditorExportPlatformAndroid::_remove_copied_libs() { +void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) { print_verbose("Removing previously installed libraries..."); Error error; - String libs_json = FileAccess::get_file_as_string(GDEXTENSION_LIBS_PATH, &error); + String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error); if (error || libs_json.is_empty()) { print_verbose("No previously installed libraries found"); return; @@ -2669,7 +2849,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() { print_verbose("Removing previously installed library " + libs[i]); da->remove(libs[i]); } - da->remove(GDEXTENSION_LIBS_PATH); + da->remove(p_gdextension_libs_path); } String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) { @@ -2694,6 +2874,67 @@ String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformA return ret; } +String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const { + Vector<String> names; + +#ifndef DISABLE_DEPRECATED + PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names); +#endif // DISABLE_DEPRECATED + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + names.push_back(export_plugins[i]->get_name()); + } + } + + String plugins_names = String("|").join(names); + return plugins_names; +} + +String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const { + String absolute_path; + if (!p_android_library_path.is_empty()) { + if (p_android_library_path.is_absolute_path()) { + absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path); + } else { + const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path); + absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path); + } + } + return absolute_path; +} + +bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) { + bool first_build = last_gradle_build_time == 0; + bool have_plugins_changed = false; + String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset); + bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir; + + String plugin_names = _get_plugins_names(p_preset); + + if (!first_build) { + have_plugins_changed = plugin_names != last_plugin_names; +#ifndef DISABLE_DEPRECATED + if (!have_plugins_changed) { + Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset); + for (int i = 0; i < enabled_plugins.size(); i++) { + if (enabled_plugins.get(i).last_updated > last_gradle_build_time) { + have_plugins_changed = true; + break; + } + } + } +#endif // DISABLE_DEPRECATED + } + + last_gradle_build_time = OS::get_singleton()->get_unix_time(); + last_gradle_build_dir = gradle_build_dir; + last_plugin_names = plugin_names; + + return have_plugins_changed || has_build_dir_changed || first_build; +} + Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { int export_format = int(p_preset->get("gradle_build/export_format")); bool should_sign = p_preset->get("package/signed"); @@ -2703,12 +2944,19 @@ Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + const String base_dir = p_path.get_base_dir(); + if (!DirAccess::exists(base_dir)) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir)); + return ERR_FILE_BAD_PATH; + } + String src_apk; Error err; EditorProgress ep("export", TTR("Exporting for Android"), 105, true); bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build")); + String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : ""; bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); bool apk_expansion = p_preset->get("apk_expansion/enable"); Vector<ABI> enabled_abis = get_enabled_abis(p_preset); @@ -2757,34 +3005,51 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!")); return ERR_UNCONFIGURED; } + String err_string; + if (!has_valid_username_and_password(p_preset, err_string)) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string)); + return ERR_UNCONFIGURED; + } if (use_gradle_build) { print_verbose("Starting gradle build..."); //test that installed build version is alright { print_verbose("Checking build version..."); - Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + String gradle_base_directory = gradle_build_directory.get_base_dir(); + Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ); if (f.is_null()) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } - String version = f->get_line().strip_edges(); - print_verbose("- build version: " + version); - if (version != VERSION_FULL_CONFIG) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); + String current_version = ExportTemplateManager::get_android_template_identifier(p_preset); + String installed_version = f->get_line().strip_edges(); + print_verbose("- build version: " + installed_version); + if (installed_version != current_version) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version)); return ERR_UNCONFIGURED; } } const String assets_directory = get_assets_directory(p_preset, export_format); + String java_sdk_path = EDITOR_GET("export/android/java_sdk_path"); + if (java_sdk_path.is_empty()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'.")); + return ERR_UNCONFIGURED; + } + print_verbose("Java sdk path: " + java_sdk_path); + String sdk_path = EDITOR_GET("export/android/android_sdk_path"); - ERR_FAIL_COND_V_MSG(sdk_path.is_empty(), ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."); + if (sdk_path.is_empty()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'.")); + return ERR_UNCONFIGURED; + } print_verbose("Android sdk path: " + sdk_path); // TODO: should we use "package/name" or "application/config/name"? String project_name = get_project_name(p_preset->get("package/name")); - err = _create_project_name_strings_files(p_preset, project_name); //project name localization. + err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory); //project name localization. if (err != OK) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name.")); } // Copies the project icon files into the appropriate Gradle project directory. _copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background); @@ -2792,12 +3057,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP _write_tmp_manifest(p_preset, p_give_internet, p_debug); //stores all the project files inside the Gradle project directory. Also includes all ABIs - _clear_assets_directory(); - _remove_copied_libs(); + _clear_assets_directory(p_preset); + String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH); + _remove_copied_libs(gdextension_libs_path); if (!apk_expansion) { print_verbose("Exporting project files..."); CustomExportData user_data; user_data.assets_directory = assets_directory; + user_data.libs_directory = gradle_build_directory.path_join("libs"); user_data.debug = p_debug; if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); @@ -2823,8 +3090,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("Storing command line flags..."); store_file_at_path(assets_directory + "/_cl_", command_line_flags); + print_verbose("Updating JAVA_HOME environment to " + java_sdk_path); + OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path); + print_verbose("Updating ANDROID_HOME environment to " + sdk_path); - OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required + OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); String build_command; #ifdef WINDOWS_ENABLED @@ -2833,12 +3103,12 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP build_command = "gradlew"; #endif - String build_path = ProjectSettings::get_singleton()->get_resource_path().path_join("android/build"); + String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory); build_command = build_path.path_join(build_command); String package_name = get_package_name(p_preset->get("package/unique_name")); String version_code = itos(p_preset->get("version/code")); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String min_sdk_version = p_preset->get("gradle_build/min_sdk"); if (!min_sdk_version.is_valid_int()) { min_sdk_version = itos(VULKAN_MIN_SDK_VERSION); @@ -2850,14 +3120,45 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String enabled_abi_string = join_abis(enabled_abis, "|", false); String sign_flag = should_sign ? "true" : "false"; String zipalign_flag = "true"; + String compress_native_libraries_flag = bool(p_preset->get("gradle_build/compress_native_libraries")) ? "true" : "false"; + + Vector<String> android_libraries; + Vector<String> android_dependencies; + Vector<String> android_dependencies_maven_repos; +#ifndef DISABLE_DEPRECATED Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset); - String local_plugins_binaries = PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins); - String remote_plugins_binaries = PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins); - String custom_maven_repos = PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins); - bool clean_build_required = is_clean_build_required(enabled_plugins); + PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries); + PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies); + PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos); +#endif // DISABLE_DEPRECATED + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug); + for (int k = 0; k < export_plugin_android_libraries.size(); k++) { + const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]); + if (!resolved_android_library_path.is_empty()) { + android_libraries.push_back(resolved_android_library_path); + } + } + + PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug); + android_dependencies.append_array(export_plugin_android_dependencies); + + PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug); + android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos); + } + } + + bool clean_build_required = _is_clean_build_required(p_preset); + String combined_android_libraries = String("|").join(android_libraries); + String combined_android_dependencies = String("|").join(android_dependencies); + String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos); List<String> cmdline; + cmdline.push_back("validateJavaVersion"); if (clean_build_required) { cmdline.push_back("clean"); } @@ -2879,11 +3180,12 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk. cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk. cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs. - cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies. - cmdline.push_back("-Pplugins_remote_binaries=" + remote_plugins_binaries); // argument to specify the list of plugins remote dependencies. - cmdline.push_back("-Pplugins_maven_repos=" + custom_maven_repos); // argument to specify the list of custom maven repos for the plugins dependencies. + cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins. + cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins. + cmdline.push_back("-Pplugins_maven_repos=" + combined_android_dependencies_maven_repos); // argument to specify the list of maven repos for android dependencies provided by plugins. cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned. cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed. + cmdline.push_back("-Pcompress_native_libraries=" + compress_native_libraries_flag); // argument to specify whether the build should compress native libraries. cmdline.push_back("-Pgodot_editor_version=" + String(VERSION_FULL_CONFIG)); // NOTE: The release keystore is not included in the verbose logging @@ -2933,10 +3235,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } } - int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline); + String build_project_output; + int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output); if (result != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output); return ERR_CANT_CREATE; + } else { + print_verbose(build_project_output); } List<String> copy_args; @@ -2963,10 +3268,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP copy_args.push_back("-Pexport_filename=" + export_filename); print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" "))); - int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args); + String copy_binary_output; + int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, ©_binary_output); if (copy_result != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output); return ERR_CANT_CREATE; + } else { + print_verbose(copy_binary_output); } print_verbose("Successfully completed Android gradle build."); @@ -2992,10 +3300,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } } - if (!DirAccess::exists(p_path.get_base_dir())) { - return ERR_FILE_BAD_PATH; - } - Ref<FileAccess> io_fa; zlib_filefunc_def io = zipio_create_io(&io_fa); @@ -3027,7 +3331,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String cmdline = p_preset->get("command_line/extra_args"); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String package_name = p_preset->get("package/unique_name"); String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); @@ -3188,10 +3492,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP zipClose(unaligned_apk, nullptr); unzClose(pkg); - if (err != OK) { - CLEANUP_AND_RETURN(err); - } - // Let's zip-align (must be done before signing) static const int ZIP_ALIGNMENT = 4; @@ -3278,6 +3578,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP // file will invalidate the signature. err = sign_apk(p_preset, p_debug, p_path, ep); if (err != OK) { + // Message is supplied by the subroutine method. CLEANUP_AND_RETURN(err); } } @@ -3307,8 +3608,11 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() { #endif devices_changed.set(); - plugins_changed.set(); +#ifndef DISABLE_DEPRECATED + android_plugins_changed.set(); +#endif // DISABLE_DEPRECATED #ifndef ANDROID_ENABLED + _update_preset_status(); check_for_changes_thread.start(_check_for_changes_poll_thread, this); #endif } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 0ac0fbb10b..e25655c6cc 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -31,7 +31,9 @@ #ifndef ANDROID_EXPORT_PLUGIN_H #define ANDROID_EXPORT_PLUGIN_H +#ifndef DISABLE_DEPRECATED #include "godot_plugin_config.h" +#endif // DISABLE_DEPRECATED #include "core/io/zip_io.h" #include "core/os/os.h" @@ -81,19 +83,25 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { EditorProgress *ep = nullptr; }; - mutable Vector<PluginConfigAndroid> plugins; +#ifndef DISABLE_DEPRECATED + mutable Vector<PluginConfigAndroid> android_plugins; + mutable SafeFlag android_plugins_changed; + Mutex android_plugins_lock; +#endif // DISABLE_DEPRECATED String last_plugin_names; uint64_t last_gradle_build_time = 0; - mutable SafeFlag plugins_changed; - Mutex plugins_lock; + String last_gradle_build_dir; + Vector<Device> devices; SafeFlag devices_changed; Mutex device_lock; #ifndef ANDROID_ENABLED Thread check_for_changes_thread; SafeFlag quit_request; + SafeFlag has_runnable_preset; static void _check_for_changes_poll_thread(void *ud); + void _update_preset_status(); #endif String get_project_name(const String &p_name) const; @@ -128,12 +136,14 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Vector<ABI> get_abis(); +#ifndef DISABLE_DEPRECATED /// List the gdap files in the directory specified by the p_path parameter. static Vector<String> list_gdap_files(const String &p_path); static Vector<PluginConfigAndroid> get_plugins(); static Vector<PluginConfigAndroid> get_enabled_plugins(const Ref<EditorExportPreset> &p_presets); +#endif // DISABLE_DEPRECATED static Error store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method = Z_DEFLATED); @@ -167,10 +177,6 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background); - void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data); - - void store_image(const String &export_path, const Vector<uint8_t> &data); - void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, const String &processed_splash_config_xml, const Ref<Image> &splash_image, @@ -183,14 +189,18 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static bool _uses_vulkan(); +protected: + void _notification(int p_what); + public: typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); -public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; virtual void get_export_options(List<ExportOption> *r_options) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; virtual String get_name() const override; @@ -219,33 +229,19 @@ public: static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false); + static String get_java_path(); + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; + static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error); virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; - inline bool is_clean_build_required(Vector<PluginConfigAndroid> enabled_plugins) { - String plugin_names = PluginConfigAndroid::get_plugins_names(enabled_plugins); - bool first_build = last_gradle_build_time == 0; - bool have_plugins_changed = false; - - if (!first_build) { - have_plugins_changed = plugin_names != last_plugin_names; - if (!have_plugins_changed) { - for (int i = 0; i < enabled_plugins.size(); i++) { - if (enabled_plugins.get(i).last_updated > last_gradle_build_time) { - have_plugins_changed = true; - break; - } - } - } - } + String _get_plugins_names(const Ref<EditorExportPreset> &p_preset) const; - last_gradle_build_time = OS::get_singleton()->get_unix_time(); - last_plugin_names = plugin_names; + String _resolve_export_plugin_android_library_path(const String &p_android_library_path) const; - return have_plugins_changed || first_build; - } + bool _is_clean_build_required(const Ref<EditorExportPreset> &p_preset); String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path); @@ -255,9 +251,9 @@ public: Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep); - void _clear_assets_directory(); + void _clear_assets_directory(const Ref<EditorExportPreset> &p_preset); - void _remove_copied_libs(); + void _remove_copied_libs(String p_gdextension_libs_path); static String join_list(const List<String> &p_parts, const String &p_separator); static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch); diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp index b64cca3254..cdec5f55b7 100644 --- a/platform/android/export/godot_plugin_config.cpp +++ b/platform/android/export/godot_plugin_config.cpp @@ -30,6 +30,8 @@ #include "godot_plugin_config.h" +#ifndef DISABLE_DEPRECATED + /* * Set of prebuilt plugins. * Currently unused, this is just for future reference: @@ -145,10 +147,8 @@ PluginConfigAndroid PluginConfigAndroid::load_plugin_config(Ref<ConfigFile> conf return plugin_config; } -String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs) { - String plugins_binaries; +void PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> binaries; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { @@ -156,56 +156,44 @@ String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<Plug } if (config.binary_type == binary_type) { - binaries.push_back(config.binary); + r_result.push_back(config.binary); } if (binary_type == PluginConfigAndroid::BINARY_TYPE_LOCAL) { - binaries.append_array(config.local_dependencies); + r_result.append_array(config.local_dependencies); } if (binary_type == PluginConfigAndroid::BINARY_TYPE_REMOTE) { - binaries.append_array(config.remote_dependencies); + r_result.append_array(config.remote_dependencies); } } - - plugins_binaries = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(binaries); } - - return plugins_binaries; } -String PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs) { - String custom_maven_repos; +void PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> repos_urls; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { continue; } - repos_urls.append_array(config.custom_maven_repos); + r_result.append_array(config.custom_maven_repos); } - - custom_maven_repos = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(repos_urls); } - return custom_maven_repos; } -String PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs) { - String plugins_names; +void PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> names; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { continue; } - names.push_back(config.name); + r_result.push_back(config.name); } - plugins_names = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(names); } - - return plugins_names; } + +#endif // DISABLE_DEPRECATED diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h index bef00979a9..8c56d00187 100644 --- a/platform/android/export/godot_plugin_config.h +++ b/platform/android/export/godot_plugin_config.h @@ -31,6 +31,8 @@ #ifndef ANDROID_GODOT_PLUGIN_CONFIG_H #define ANDROID_GODOT_PLUGIN_CONFIG_H +#ifndef DISABLE_DEPRECATED + #include "core/config/project_settings.h" #include "core/error/error_list.h" #include "core/io/config_file.h" @@ -67,8 +69,6 @@ struct PluginConfigAndroid { inline static const char *BINARY_TYPE_LOCAL = "local"; inline static const char *BINARY_TYPE_REMOTE = "remote"; - inline static const char *PLUGIN_VALUE_SEPARATOR = "|"; - // Set to true when the config file is properly loaded. bool valid_config = false; // Unix timestamp of last change to this plugin. @@ -96,11 +96,13 @@ struct PluginConfigAndroid { static PluginConfigAndroid load_plugin_config(Ref<ConfigFile> config_file, const String &path); - static String get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); - static String get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); - static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); }; +#endif // DISABLE_DEPRECATED + #endif // ANDROID_GODOT_PLUGIN_CONFIG_H diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index ba4487cc4d..9eddef6a4c 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -191,14 +191,14 @@ String _android_xml_escape(const String &p_string) { } // Creates strings.xml files inside the gradle project for different locales. -Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name) { +Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir) { print_verbose("Creating strings resources for supported locales for project " + project_name); // Stores the string into the default values directory. String processed_default_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(project_name)); - store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string); + store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales - Ref<DirAccess> da = DirAccess::open("res://android/build/res"); + Ref<DirAccess> da = DirAccess::open(p_gradle_build_dir.path_join("res")); if (da.is_null()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error("Unable to open Android resources directory."); @@ -217,7 +217,7 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset continue; } String locale = file.replace("values-", "").replace("-r", "_"); - String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml"; + String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml"); if (appnames.has(locale)) { String locale_project_name = appnames[locale]; String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name)); @@ -254,34 +254,7 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_screen_sizes; } -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan) { - String manifest_xr_features; - int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); - bool uses_xr = xr_mode_index == XR_MODE_OPENXR; - if (uses_xr) { - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required - if (hand_tracking_index == XR_HAND_TRACKING_OPTIONAL) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; - } else if (hand_tracking_index == XR_HAND_TRACKING_REQUIRED) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"true\" />\n"; - } - - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (passthrough_mode == XR_PASSTHROUGH_OPTIONAL) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"com.oculus.feature.PASSTHROUGH\" android:required=\"false\" />\n"; - } else if (passthrough_mode == XR_PASSTHROUGH_REQUIRED) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"com.oculus.feature.PASSTHROUGH\" android:required=\"true\" />\n"; - } - } - - if (p_uses_vulkan) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; - } - return manifest_xr_features; -} - -String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr) { +String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) { String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " @@ -294,40 +267,46 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr orientation, bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); - if (p_uses_xr) { - manifest_activity_text += " <intent-filter>\n" - " <action android:name=\"android.intent.action.MAIN\" />\n" - " <category android:name=\"android.intent.category.LAUNCHER\" />\n" - "\n" - " <!-- Enable access to OpenXR on Oculus mobile devices, no-op on other Android\n" - " platforms. -->\n" - " <category android:name=\"com.oculus.intent.category.VR\" />\n" - "\n" - " <!-- OpenXR category tag to indicate the activity starts in an immersive OpenXR mode. \n" - " See https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#android-runtime-category. -->\n" - " <category android:name=\"org.khronos.openxr.intent.category.IMMERSIVE_HMD\" />\n" - "\n" - " <!-- Enable VR access on HTC Vive Focus devices. -->\n" - " <category android:name=\"com.htc.intent.category.VRAPP\" />\n" - " </intent-filter>\n"; - } else { - manifest_activity_text += " <intent-filter>\n" - " <action android:name=\"android.intent.action.MAIN\" />\n" - " <category android:name=\"android.intent.category.LAUNCHER\" />\n" - " </intent-filter>\n"; + manifest_activity_text += " <intent-filter>\n" + " <action android:name=\"android.intent.action.MAIN\" />\n" + " <category android:name=\"android.intent.category.DEFAULT\" />\n"; + + bool show_in_app_library = p_preset->get("package/show_in_app_library"); + if (show_in_app_library) { + manifest_activity_text += " <category android:name=\"android.intent.category.LAUNCHER\" />\n"; + } + + bool uses_leanback_category = p_preset->get("package/show_in_android_tv"); + if (uses_leanback_category) { + manifest_activity_text += " <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n"; + } + + bool uses_home_category = p_preset->get("package/show_as_launcher_app"); + if (uses_home_category) { + manifest_activity_text += " <category android:name=\"android.intent.category.HOME\" />\n"; + } + + manifest_activity_text += " </intent-filter>\n"; + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(p_export_platform)) { + const String contents = export_plugins[i]->get_android_manifest_activity_element_contents(p_export_platform, p_debug); + if (!contents.is_empty()) { + manifest_activity_text += contents; + manifest_activity_text += "\n"; + } + } } manifest_activity_text += " </activity>\n"; return manifest_activity_text; } -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission) { +String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug) { int app_category_index = (int)(p_preset->get("package/app_category")); bool is_game = app_category_index == APP_CATEGORY_GAME; - int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); - bool uses_xr = xr_mode_index == XR_MODE_OPENXR; - String manifest_application_text = vformat( " <application android:label=\"@string/godot_project_name_string\"\n" " android:allowBackup=\"%s\"\n" @@ -344,18 +323,18 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ bool_to_string(p_preset->get("package/retain_data_on_uninstall")), bool_to_string(p_has_read_write_storage_permission)); - if (uses_xr) { - bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE; - if (hand_tracking_enabled) { - int hand_tracking_frequency_index = p_preset->get("xr_features/hand_tracking_frequency"); - String hand_tracking_frequency = hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"; - manifest_application_text += vformat( - " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", - hand_tracking_frequency); - manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(p_export_platform)) { + const String contents = export_plugins[i]->get_android_manifest_application_element_contents(p_export_platform, p_debug); + if (!contents.is_empty()) { + manifest_application_text += contents; + manifest_application_text += "\n"; + } } } - manifest_application_text += _get_activity_tag(p_preset, uses_xr); + + manifest_application_text += _get_activity_tag(p_export_platform, p_preset, p_debug); manifest_application_text += " </application>\n"; return manifest_application_text; } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 8a885a0d12..9f8e476f73 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -61,22 +61,9 @@ static const int APP_CATEGORY_VIDEO = 8; static const int XR_MODE_REGULAR = 0; static const int XR_MODE_OPENXR = 1; -// Supported XR hand tracking modes. -static const int XR_HAND_TRACKING_NONE = 0; -static const int XR_HAND_TRACKING_OPTIONAL = 1; -static const int XR_HAND_TRACKING_REQUIRED = 2; - -// Supported XR hand tracking frequencies. -static const int XR_HAND_TRACKING_FREQUENCY_LOW = 0; -static const int XR_HAND_TRACKING_FREQUENCY_HIGH = 1; - -// Supported XR passthrough modes. -static const int XR_PASSTHROUGH_NONE = 0; -static const int XR_PASSTHROUGH_OPTIONAL = 1; -static const int XR_PASSTHROUGH_REQUIRED = 2; - struct CustomExportData { String assets_directory; + String libs_directory; bool debug; Vector<String> libs; }; @@ -108,7 +95,7 @@ Error store_string_at_path(const String &p_path, const String &p_data); Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); // Creates strings.xml files inside the gradle project for different locales. -Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name); +Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir); String bool_to_string(bool v); @@ -116,10 +103,8 @@ String _get_gles_tag(); String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset); -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan); - -String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr); +String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug); -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission); +String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug); #endif // ANDROID_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index 1249f2219f..f56eda4694 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -121,6 +121,75 @@ uint8_t FileAccessAndroid::get_8() const { return byte; } +uint16_t FileAccessAndroid::get_16() const { + if (pos >= len) { + eof = true; + return 0; + } + + uint16_t bytes = 0; + int r = AAsset_read(asset, &bytes, 2); + + if (r >= 0) { + pos += r; + if (pos >= len) { + eof = true; + } + } + + if (big_endian) { + bytes = BSWAP16(bytes); + } + + return bytes; +} + +uint32_t FileAccessAndroid::get_32() const { + if (pos >= len) { + eof = true; + return 0; + } + + uint32_t bytes = 0; + int r = AAsset_read(asset, &bytes, 4); + + if (r >= 0) { + pos += r; + if (pos >= len) { + eof = true; + } + } + + if (big_endian) { + bytes = BSWAP32(bytes); + } + + return bytes; +} + +uint64_t FileAccessAndroid::get_64() const { + if (pos >= len) { + eof = true; + return 0; + } + + uint64_t bytes = 0; + int r = AAsset_read(asset, &bytes, 8); + + if (r >= 0) { + pos += r; + if (pos >= len) { + eof = true; + } + } + + if (big_endian) { + bytes = BSWAP64(bytes); + } + + return bytes; +} + uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); @@ -151,6 +220,18 @@ void FileAccessAndroid::store_8(uint8_t p_dest) { ERR_FAIL(); } +void FileAccessAndroid::store_16(uint16_t p_dest) { + ERR_FAIL(); +} + +void FileAccessAndroid::store_32(uint32_t p_dest) { + ERR_FAIL(); +} + +void FileAccessAndroid::store_64(uint64_t p_dest) { + ERR_FAIL(); +} + bool FileAccessAndroid::file_exists(const String &p_path) { String path = fix_path(p_path).simplify_path(); if (path.begins_with("/")) { diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index 7ff5c4d117..ec613b6687 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -66,18 +66,29 @@ public: virtual bool eof_reached() const override; // reading passed EOF virtual uint8_t get_8() const override; // get a byte + virtual uint16_t get_16() const override; + virtual uint32_t get_32() const override; + virtual uint64_t get_64() const override; virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; // get last error virtual void flush() override; virtual void store_8(uint8_t p_dest) override; // store a byte + virtual void store_16(uint16_t p_dest) override; + virtual void store_32(uint32_t p_dest) override; + virtual void store_64(uint64_t p_dest) override; virtual bool file_exists(const String &p_path) override; // return true if a file exists virtual uint64_t _get_modified_time(const String &p_file) override { return 0; } - virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + + virtual bool _get_hidden_attribute(const String &p_file) override { return false; } + virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } + virtual bool _get_read_only_attribute(const String &p_file) override { return false; } + virtual Error _set_read_only_attribute(const String &p_file, bool p_ro) override { return ERR_UNAVAILABLE; } virtual void close() override; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index a1865fb1d4..46d9728632 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -69,7 +69,7 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo if (_file_open) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); String path = fix_path(p_path).simplify_path(); jstring js = env->NewStringUTF(path.utf8().get_data()); @@ -103,7 +103,7 @@ void FileAccessFilesystemJAndroid::_close() { if (_file_close) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(file_access_handler, _file_close, id); } id = 0; @@ -116,7 +116,7 @@ bool FileAccessFilesystemJAndroid::is_open() const { void FileAccessFilesystemJAndroid::seek(uint64_t p_position) { if (_file_seek) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); env->CallVoidMethod(file_access_handler, _file_seek, id, p_position); } @@ -125,7 +125,7 @@ void FileAccessFilesystemJAndroid::seek(uint64_t p_position) { void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) { if (_file_seek_end) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); env->CallVoidMethod(file_access_handler, _file_seek_end, id, p_position); } @@ -134,7 +134,7 @@ void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) { uint64_t FileAccessFilesystemJAndroid::get_position() const { if (_file_tell) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); return env->CallLongMethod(file_access_handler, _file_tell, id); } else { @@ -145,7 +145,7 @@ uint64_t FileAccessFilesystemJAndroid::get_position() const { uint64_t FileAccessFilesystemJAndroid::get_length() const { if (_file_get_size) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); return env->CallLongMethod(file_access_handler, _file_get_size, id); } else { @@ -156,7 +156,7 @@ uint64_t FileAccessFilesystemJAndroid::get_length() const { bool FileAccessFilesystemJAndroid::eof_reached() const { if (_file_eof) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use."); return env->CallBooleanMethod(file_access_handler, _file_eof, id); } else { @@ -169,7 +169,7 @@ void FileAccessFilesystemJAndroid::_set_eof(bool eof) { ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(file_access_handler, _file_set_eof, id, eof); } } @@ -181,6 +181,36 @@ uint8_t FileAccessFilesystemJAndroid::get_8() const { return byte; } +uint16_t FileAccessFilesystemJAndroid::get_16() const { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + uint16_t bytes = 0; + get_buffer(reinterpret_cast<uint8_t *>(&bytes), 2); + if (big_endian) { + bytes = BSWAP16(bytes); + } + return bytes; +} + +uint32_t FileAccessFilesystemJAndroid::get_32() const { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + uint32_t bytes = 0; + get_buffer(reinterpret_cast<uint8_t *>(&bytes), 4); + if (big_endian) { + bytes = BSWAP32(bytes); + } + return bytes; +} + +uint64_t FileAccessFilesystemJAndroid::get_64() const { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + uint64_t bytes = 0; + get_buffer(reinterpret_cast<uint8_t *>(&bytes), 8); + if (big_endian) { + bytes = BSWAP64(bytes); + } + return bytes; +} + String FileAccessFilesystemJAndroid::get_line() const { ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use."); @@ -235,7 +265,7 @@ uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_len } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); jobject j_buffer = env->NewDirectByteBuffer(p_dst, p_length); int length = env->CallIntMethod(file_access_handler, _file_read, id, j_buffer); @@ -250,6 +280,27 @@ void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) { store_buffer(&p_dest, 1); } +void FileAccessFilesystemJAndroid::store_16(uint16_t p_dest) { + if (big_endian) { + p_dest = BSWAP16(p_dest); + } + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 2); +} + +void FileAccessFilesystemJAndroid::store_32(uint32_t p_dest) { + if (big_endian) { + p_dest = BSWAP32(p_dest); + } + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 4); +} + +void FileAccessFilesystemJAndroid::store_64(uint64_t p_dest) { + if (big_endian) { + p_dest = BSWAP64(p_dest); + } + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 8); +} + void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) { if (_file_write) { ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); @@ -258,7 +309,7 @@ void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length); env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer); @@ -276,7 +327,7 @@ Error FileAccessFilesystemJAndroid::get_error() const { void FileAccessFilesystemJAndroid::flush() { if (_file_flush) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); env->CallVoidMethod(file_access_handler, _file_flush, id); } @@ -285,7 +336,7 @@ void FileAccessFilesystemJAndroid::flush() { bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) { if (_file_exists) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); String path = fix_path(p_path).simplify_path(); jstring js = env->NewStringUTF(path.utf8().get_data()); @@ -300,7 +351,7 @@ bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) { uint64_t FileAccessFilesystemJAndroid::_get_modified_time(const String &p_file) { if (_file_last_modified) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); String path = fix_path(p_file).simplify_path(); jstring js = env->NewStringUTF(path.utf8().get_data()); diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 739c8a0925..f33aa64ebe 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -77,6 +77,9 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF virtual uint8_t get_8() const override; ///< get a byte + virtual uint16_t get_16() const override; + virtual uint32_t get_32() const override; + virtual uint64_t get_64() const override; virtual String get_line() const override; ///< get a line virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; @@ -84,6 +87,9 @@ public: virtual void flush() override; virtual void store_8(uint8_t p_dest) override; ///< store a byte + virtual void store_16(uint16_t p_dest) override; + virtual void store_32(uint32_t p_dest) override; + virtual void store_64(uint64_t p_dest) override; virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_path) override; ///< return true if a file exists @@ -91,8 +97,13 @@ public: static void setup(jobject p_file_access_handler); virtual uint64_t _get_modified_time(const String &p_file) override; - virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + + virtual bool _get_hidden_attribute(const String &p_file) override { return false; } + virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } + virtual bool _get_read_only_attribute(const String &p_file) override { return false; } + virtual Error _set_read_only_attribute(const String &p_file, bool p_ro) override { return ERR_UNAVAILABLE; } virtual void close() override; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 542ab51660..4abc6548bf 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.godot.game" android:versionCode="1" android:versionName="1.0" android:installLocation="auto" > @@ -35,7 +34,7 @@ android:name=".GodotApp" android:label="@string/godot_project_name_string" android:theme="@style/GodotAppSplashTheme" - android:launchMode="singleInstance" + android:launchMode="singleInstancePerTask" android:excludeFromRecents="false" android:exported="true" android:screenOrientation="landscape" @@ -45,6 +44,7 @@ <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> diff --git a/platform/android/java/app/assetPacks/installTime/build.gradle b/platform/android/java/app/assetPacks/installTime/build.gradle index b06faac374..46fa046ed4 100644 --- a/platform/android/java/app/assetPacks/installTime/build.gradle +++ b/platform/android/java/app/assetPacks/installTime/build.gradle @@ -1,4 +1,6 @@ -apply plugin: 'com.android.asset-pack' +plugins { + id 'com.android.asset-pack' +} assetPack { packName = "installTime" // Directory name for the asset pack diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 01b148aeef..7797f4bc9d 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -1,17 +1,4 @@ // Gradle build config for Godot Engine's Android port. -buildscript { - apply from: 'config.gradle' - - repositories { - google() - mavenCentral() - } - dependencies { - classpath libraries.androidGradlePlugin - classpath libraries.kotlinGradlePlugin - } -} - plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' @@ -23,6 +10,8 @@ allprojects { repositories { google() mavenCentral() + gradlePluginPortal() + maven { url "https://plugins.gradle.org/m2/" } // Godot user plugins custom maven repos String[] mavenRepos = getGodotPluginsMavenRepos() @@ -42,8 +31,7 @@ configurations { } dependencies { - implementation libraries.kotlinStdLib - implementation libraries.androidxFragment + implementation "androidx.fragment:fragment:$versions.fragmentVersion" if (rootProject.findProject(":lib")) { implementation project(":lib") @@ -88,6 +76,8 @@ android { assetPacks = [":assetPacks:installTime"] + namespace = 'com.godot.game' + defaultConfig { // The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects. aaptOptions { @@ -126,6 +116,14 @@ android { if (shouldNotStrip()) { doNotStrip '**/*.so' } + + jniLibs { + // Setting this to true causes AGP to package compressed native libraries when building the app + // For more background, see: + // - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs + // - https://stackoverflow.com/a/44704840 + useLegacyPackaging shouldUseLegacyPackaging() + } } signingConfigs { @@ -241,3 +239,26 @@ task copyAndRenameReleaseAab(type: Copy) { into getExportPath() rename "build-release.aab", getExportFilename() } + +/** + * Used to validate the version of the Java SDK used for the Godot gradle builds. + */ +task validateJavaVersion { + if (JavaVersion.current() != versions.javaVersion) { + throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the required Java version for Godot gradle builds.") + } +} + +/* +When they're scheduled to run, the copy*AARToAppModule tasks generate dependencies for the 'app' +module, so we're ensuring the ':app:preBuild' task is set to run after those tasks. + */ +if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) { + preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule")) +} +if (rootProject.tasks.findByPath("copyDevAARToAppModule") != null) { + preBuild.mustRunAfter(rootProject.tasks.named("copyDevAARToAppModule")) +} +if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) { + preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule")) +} diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index e7c06628c8..f2c4a5d1b6 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,27 +1,20 @@ ext.versions = [ - androidGradlePlugin: '7.2.1', - compileSdk : 33, + androidGradlePlugin: '8.2.0', + compileSdk : 34, // Also update 'platform/android/export/export_plugin.cpp#OPENGL_MIN_SDK_VERSION' minSdk : 21, // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' - targetSdk : 33, - buildTools : '33.0.2', - kotlinVersion : '1.7.0', - fragmentVersion : '1.3.6', - nexusPublishVersion: '1.1.0', - javaVersion : 11, + targetSdk : 34, + buildTools : '34.0.0', + kotlinVersion : '1.9.20', + fragmentVersion : '1.6.2', + nexusPublishVersion: '1.3.0', + javaVersion : JavaVersion.VERSION_17, // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated. ndkVersion : '23.2.8568313' ] -ext.libraries = [ - androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", - kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", - kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion", - androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion", -] - ext.getExportPackageName = { -> // Retrieve the app id from the project property set by the Godot build command. String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : "" @@ -368,3 +361,26 @@ ext.shouldSign = { -> ext.shouldNotStrip = { -> return isAndroidStudio() || project.hasProperty("doNotStrip") } + +/** + * Whether to use the legacy convention of compressing all .so files in the APK. + * + * For more background, see: + * - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs + * - https://stackoverflow.com/a/44704840 + */ +ext.shouldUseLegacyPackaging = { -> + int minSdk = getExportMinSdkVersion() + if (minSdk < 23) { + // Enforce the default behavior for compatibility with device running api < 23 + return true + } + + String legacyPackagingFlag = project.hasProperty("compress_native_libraries") ? project.property("compress_native_libraries") : "" + if (legacyPackagingFlag != null && !legacyPackagingFlag.isEmpty()) { + return Boolean.parseBoolean(legacyPackagingFlag) + } + + // Default behavior for minSdk >= 23 + return false +} diff --git a/platform/android/java/app/gradle.properties b/platform/android/java/app/gradle.properties index d9f79b6818..2b6468c95e 100644 --- a/platform/android/java/app/gradle.properties +++ b/platform/android/java/app/gradle.properties @@ -23,3 +23,6 @@ org.gradle.warning.mode=all # Enable resource optimizations for release build. # NOTE: This is turned off for template release build in order to support the build legacy process. android.enableResourceOptimizations=true + +# Fix gradle build errors when the build path contains non-ASCII characters +android.overridePathCheck=true diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index b4524a3f60..dcac44e393 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -7,8 +7,10 @@ pluginManagement { id 'org.jetbrains.kotlin.android' version versions.kotlinVersion } repositories { - gradlePluginPortal() google() + mavenCentral() + gradlePluginPortal() + maven { url "https://plugins.gradle.org/m2/" } } } diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 1d2cc05715..9142d767b4 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -30,7 +30,7 @@ package com.godot.game; -import org.godotengine.godot.FullScreenGodotApp; +import org.godotengine.godot.GodotActivity; import android.os.Bundle; @@ -38,7 +38,7 @@ import android.os.Bundle; * Template activity for Godot Android builds. * Feel free to extend and modify this class for your custom logic. */ -public class GodotApp extends FullScreenGodotApp { +public class GodotApp extends GodotActivity { @Override public void onCreate(Bundle savedInstanceState) { setTheme(R.style.GodotAppMainTheme); diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index f94454e2a7..c609b33ef4 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -1,18 +1,3 @@ -buildscript { - apply from: 'app/config.gradle' - - repositories { - google() - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - } - dependencies { - classpath libraries.androidGradlePlugin - classpath libraries.kotlinGradlePlugin - classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' - } -} - plugins { id 'io.github.gradle-nexus.publish-plugin' } @@ -31,6 +16,8 @@ allprojects { repositories { google() mavenCentral() + gradlePluginPortal() + maven { url "https://plugins.gradle.org/m2/" } } } @@ -173,10 +160,21 @@ task zipGradleBuild(type: Zip) { destinationDirectory = file(binDir) } +/** + * Returns true if the scons build tasks responsible for generating the Godot native shared + * libraries should be excluded. + */ +def excludeSconsBuildTasks() { + return !isAndroidStudio() && !project.hasProperty("generateNativeLibs") +} + +/** + * Generates the list of build tasks that should be excluded from the build process.\ + */ def templateExcludedBuildTask() { // We exclude these gradle tasks so we can run the scons command manually. def excludedTasks = [] - if (!isAndroidStudio()) { + if (excludeSconsBuildTasks()) { logger.lifecycle("Excluding Android studio build tasks") for (String flavor : supportedFlavors) { String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor] @@ -190,23 +188,42 @@ def templateExcludedBuildTask() { return excludedTasks } -def templateBuildTasks() { +/** + * Generates the build tasks for the given flavor + * @param flavor Must be one of the supported flavors ('template' / 'editor') + */ +def generateBuildTasks(String flavor = "template") { + if (!supportedFlavors.contains(flavor)) { + throw new GradleException("Invalid build flavor: $flavor") + } + def tasks = [] - // Only build the apks and aar files for which we have native shared libraries. - for (String target : supportedFlavorsBuildTypes["template"]) { - File targetLibs = new File("lib/libs/" + target) - if (targetLibs != null + // Only build the apks and aar files for which we have native shared libraries unless we intend + // to run the scons build tasks. + boolean excludeSconsBuildTasks = excludeSconsBuildTasks() + boolean isTemplate = flavor == "template" + String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/" + for (String target : supportedFlavorsBuildTypes[flavor]) { + File targetLibs = new File(libsDir + target) + if (!excludeSconsBuildTasks || (targetLibs != null && targetLibs.isDirectory() && targetLibs.listFiles() != null - && targetLibs.listFiles().length > 0) { + && targetLibs.listFiles().length > 0)) { String capitalizedTarget = target.capitalize() - // Copy the generated aar library files to the build directory. - tasks += "copy" + capitalizedTarget + "AARToAppModule" - // Copy the generated aar library files to the bin directory. - tasks += "copy" + capitalizedTarget + "AARToBin" - // Copy the prebuilt binary templates to the bin directory. - tasks += "copy" + capitalizedTarget + "BinaryToBin" + if (isTemplate) { + // Copy the generated aar library files to the build directory. + tasks += "copy${capitalizedTarget}AARToAppModule" + // Copy the generated aar library files to the bin directory. + tasks += "copy${capitalizedTarget}AARToBin" + // Copy the prebuilt binary templates to the bin directory. + tasks += "copy${capitalizedTarget}BinaryToBin" + } else { + // Copy the generated editor apk to the bin directory. + tasks += "copyEditor${capitalizedTarget}ApkToBin" + // Copy the generated editor aab to the bin directory. + tasks += "copyEditor${capitalizedTarget}AabToBin" + } } else { logger.lifecycle("No native shared libs for target $target. Skipping build.") } @@ -265,27 +282,13 @@ task copyEditorDevAabToBin(type: Copy) { /** * Generate the Godot Editor Android apk. * - * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running - * this gradle task. The task will only build the apk(s) for which the shared libraries is - * available. + * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries + * must have been generated (via scons) prior to running this gradle task. + * The task will only build the apk(s) for which the shared libraries is available. */ task generateGodotEditor { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - - def tasks = [] - - for (String target : supportedFlavorsBuildTypes["editor"]) { - File targetLibs = new File("lib/libs/tools/" + target) - if (targetLibs != null - && targetLibs.isDirectory() - && targetLibs.listFiles() != null - && targetLibs.listFiles().length > 0) { - tasks += "copyEditor${target.capitalize()}ApkToBin" - tasks += "copyEditor${target.capitalize()}AabToBin" - } - } - - dependsOn = tasks + dependsOn = generateBuildTasks("editor") } /** @@ -293,7 +296,7 @@ task generateGodotEditor { */ task generateGodotTemplates { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = templateBuildTasks() + dependsOn = generateBuildTasks("template") finalizedBy 'zipGradleBuild' } @@ -303,10 +306,10 @@ task generateGodotTemplates { */ task generateDevTemplate { // add parameter to set symbols to true - gradle.startParameter.projectProperties += [doNotStrip: true] + gradle.startParameter.projectProperties += [doNotStrip: "true"] gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = templateBuildTasks() + dependsOn = generateBuildTasks("template") finalizedBy 'zipGradleBuild' } diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 38034aa47c..0f7ffeecae 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -2,14 +2,14 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'base' } dependencies { - implementation libraries.kotlinStdLib - implementation libraries.androidxFragment + implementation "androidx.fragment:fragment:$versions.fragmentVersion" implementation project(":lib") - implementation "androidx.window:window:1.0.0" + implementation "androidx.window:window:1.2.0" } ext { @@ -81,6 +81,8 @@ android { buildToolsVersion versions.buildTools ndkVersion versions.ndkVersion + namespace = "org.godotengine.editor" + defaultConfig { // The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device applicationId "org.godotengine.editor.v4" @@ -90,7 +92,10 @@ android { targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' - setProperty("archivesBaseName", "android_editor") + } + + base { + archivesName = "android_editor" } compileOptions { @@ -111,6 +116,10 @@ android { } } + buildFeatures { + buildConfig = true + } + buildTypes { dev { initWith debug diff --git a/platform/android/java/editor/src/.gitignore b/platform/android/java/editor/src/.gitignore new file mode 100644 index 0000000000..c081ec3425 --- /dev/null +++ b/platform/android/java/editor/src/.gitignore @@ -0,0 +1 @@ +!/debug diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index c7b2c8ad67..78dcddac0e 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="org.godotengine.editor" android:installLocation="auto"> <supports-screens @@ -35,7 +34,7 @@ <activity android:name=".GodotProjectManager" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:launchMode="singleInstance" + android:launchMode="singleTask" android:screenOrientation="userLandscape" android:exported="true" android:process=":GodotProjectManager"> @@ -45,6 +44,7 @@ <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @@ -53,7 +53,7 @@ android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:process=":GodotEditor" - android:launchMode="singleInstance" + android:launchMode="singleTask" android:screenOrientation="userLandscape" android:exported="false"> <layout android:defaultHeight="@dimen/editor_default_window_height" @@ -65,7 +65,7 @@ android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:label="@string/godot_project_name_string" android:process=":GodotGame" - android:launchMode="singleInstance" + android:launchMode="singleTask" android:exported="false" android:screenOrientation="userLandscape"> <layout android:defaultHeight="@dimen/editor_default_window_height" diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 64d3d4eca1..caf64bc933 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -39,7 +39,7 @@ import android.os.* import android.util.Log import android.widget.Toast import androidx.window.layout.WindowMetricsCalculator -import org.godotengine.godot.FullScreenGodotApp +import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotLib import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix @@ -55,7 +55,7 @@ import kotlin.math.min * * It also plays the role of the primary editor window. */ -open class GodotEditor : FullScreenGodotApp() { +open class GodotEditor : GodotActivity() { companion object { private val TAG = GodotEditor::class.java.simpleName @@ -115,7 +115,7 @@ open class GodotEditor : FullScreenGodotApp() { runOnUiThread { // Enable long press, panning and scaling gestures - godotFragment?.renderView?.inputHandler?.apply { + godotFragment?.godot?.renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) } @@ -225,16 +225,9 @@ open class GodotEditor : FullScreenGodotApp() { val runningProcesses = activityManager.runningAppProcesses for (runningProcess in runningProcesses) { if (runningProcess.processName.endsWith(processNameSuffix)) { - if (targetClass == null) { - // Killing process directly - Log.v(TAG, "Killing Godot process ${runningProcess.processName}") - Process.killProcess(runningProcess.pid) - } else { - // Activity is running; sending a request for self termination. - Log.v(TAG, "Sending force quit request to $targetClass running on process ${runningProcess.processName}") - val forceQuitIntent = Intent(this, targetClass).putExtra(EXTRA_FORCE_QUIT, true) - startActivity(forceQuitIntent) - } + // Killing process directly + Log.v(TAG, "Killing Godot process ${runningProcess.processName}") + Process.killProcess(runningProcess.pid) return true } } @@ -318,7 +311,7 @@ open class GodotEditor : FullScreenGodotApp() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array<String?>, + permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties index 39a0dcda16..c8abb52614 100644 --- a/platform/android/java/gradle.properties +++ b/platform/android/java/gradle.properties @@ -26,3 +26,6 @@ org.gradle.warning.mode=all # Disable resource optimizations for template release build. # NOTE: This is turned on for Godot Editor's gradle builds in order to improve the release build. android.enableResourceOptimizations=false + +# Fix gradle build errors when the build path contains non-ASCII characters +android.overridePathCheck=true diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index aa991fceae..471fefaf90 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Jan 17 12:08:26 PST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index f03a1dd47a..8240843876 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.godotengine.godot" android:versionCode="1" android:versionName="1.0"> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 4340250ad3..ed967b9660 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -10,8 +10,7 @@ ext { apply from: "../scripts/publish-module.gradle" dependencies { - implementation libraries.kotlinStdLib - implementation libraries.androidxFragment + implementation "androidx.fragment:fragment:$versions.fragmentVersion" } def pathToRootDir = "../../../../" @@ -39,6 +38,11 @@ android { jvmTarget = versions.javaVersion } + buildFeatures { + aidl = true + buildConfig = true + } + buildTypes { dev { initWith debug @@ -97,6 +101,7 @@ android { } boolean devBuild = buildType == "dev" + boolean debugSymbols = devBuild || isAndroidStudio() boolean runTests = devBuild boolean productionBuild = !devBuild boolean storeRelease = buildType == "release" @@ -164,7 +169,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "debug_symbols=${debugSymbols}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() } // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index 3e975449d8..91d272735e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -30,156 +30,10 @@ package org.godotengine.godot; -import org.godotengine.godot.utils.ProcessPhoenix; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; - /** - * Base activity for Android apps intending to use Godot as the primary and only screen. + * Base abstract activity for Android apps intending to use Godot as the primary screen. * - * It's also a reference implementation for how to setup and use the {@link Godot} fragment - * within an Android app. + * @deprecated Use {@link GodotActivity} */ -public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost { - private static final String TAG = FullScreenGodotApp.class.getSimpleName(); - - protected static final String EXTRA_FORCE_QUIT = "force_quit_requested"; - protected static final String EXTRA_NEW_LAUNCH = "new_launch_requested"; - - @Nullable - private Godot godotFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.godot_app_layout); - - handleStartIntent(getIntent(), true); - - Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container); - if (currentFragment instanceof Godot) { - Log.v(TAG, "Reusing existing Godot fragment instance."); - godotFragment = (Godot)currentFragment; - } else { - Log.v(TAG, "Creating new Godot fragment instance."); - godotFragment = initGodotInstance(); - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); - } - } - - @Override - public void onDestroy() { - Log.v(TAG, "Destroying Godot app..."); - super.onDestroy(); - terminateGodotInstance(godotFragment); - } - - @Override - public final void onGodotForceQuit(Godot instance) { - runOnUiThread(() -> { - terminateGodotInstance(instance); - }); - } - - private void terminateGodotInstance(Godot instance) { - if (instance == godotFragment) { - Log.v(TAG, "Force quitting Godot instance"); - ProcessPhoenix.forceQuit(FullScreenGodotApp.this); - } - } - - @Override - public final void onGodotRestartRequested(Godot instance) { - runOnUiThread(() -> { - if (instance == godotFragment) { - // It's very hard to properly de-initialize Godot on Android to restart the game - // from scratch. Therefore, we need to kill the whole app process and relaunch it. - // - // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including - // releasing and reloading native libs or resetting their state somehow and clearing static data). - Log.v(TAG, "Restarting Godot instance..."); - ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this); - } - }); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - - handleStartIntent(intent, false); - - if (godotFragment != null) { - godotFragment.onNewIntent(intent); - } - } - - private void handleStartIntent(Intent intent, boolean newLaunch) { - boolean forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false); - if (forceQuitRequested) { - Log.d(TAG, "Force quit requested, terminating.."); - ProcessPhoenix.forceQuit(this); - return; - } - - if (!newLaunch) { - boolean newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false); - if (newLaunchRequested) { - Log.d(TAG, "New launch requested, restarting.."); - - Intent restartIntent = new Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false); - ProcessPhoenix.triggerRebirth(this, restartIntent); - return; - } - } - } - - @CallSuper - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (godotFragment != null) { - godotFragment.onActivityResult(requestCode, resultCode, data); - } - } - - @CallSuper - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (godotFragment != null) { - godotFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - @Override - public void onBackPressed() { - if (godotFragment != null) { - godotFragment.onBackPressed(); - } else { - super.onBackPressed(); - } - } - - /** - * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}. - */ - @NonNull - protected Godot initGodotInstance() { - return new Godot(); - } - - @Nullable - protected final Godot getGodotFragment() { - return godotFragment; - } -} +@Deprecated +public abstract class FullScreenGodotApp extends GodotActivity {} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java deleted file mode 100644 index 9f2dec7317..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ /dev/null @@ -1,1195 +0,0 @@ -/**************************************************************************/ -/* Godot.java */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -package org.godotengine.godot; - -import static android.content.Context.MODE_PRIVATE; -import static android.content.Context.WINDOW_SERVICE; - -import org.godotengine.godot.input.GodotEditText; -import org.godotengine.godot.io.directory.DirectoryAccessHandler; -import org.godotengine.godot.io.file.FileAccessHandler; -import org.godotengine.godot.plugin.GodotPlugin; -import org.godotengine.godot.plugin.GodotPluginRegistry; -import org.godotengine.godot.tts.GodotTTS; -import org.godotengine.godot.utils.BenchmarkUtils; -import org.godotengine.godot.utils.GodotNetUtils; -import org.godotengine.godot.utils.PermissionsUtil; -import org.godotengine.godot.xr.XRMode; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.pm.ConfigurationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Messenger; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewTreeObserver; -import android.view.Window; -import android.view.WindowInsets; -import android.view.WindowInsetsAnimation; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.CallSuper; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import com.google.android.vending.expansion.downloader.DownloadProgressInfo; -import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; -import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; -import com.google.android.vending.expansion.downloader.Helpers; -import com.google.android.vending.expansion.downloader.IDownloaderClient; -import com.google.android.vending.expansion.downloader.IDownloaderService; -import com.google.android.vending.expansion.downloader.IStub; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -public class Godot extends Fragment implements SensorEventListener, IDownloaderClient { - private static final String TAG = Godot.class.getSimpleName(); - - private IStub mDownloaderClientStub; - private TextView mStatusText; - private TextView mProgressFraction; - private TextView mProgressPercent; - private TextView mAverageSpeed; - private TextView mTimeRemaining; - private ProgressBar mPB; - private ClipboardManager mClipboard; - - private View mDashboard; - private View mCellMessage; - - private Button mPauseButton; - private Button mWiFiSettingsButton; - - private XRMode xrMode = XRMode.REGULAR; - private boolean use_immersive = false; - private boolean use_debug_opengl = false; - private boolean mStatePaused; - private boolean activityResumed; - private int mState; - - private GodotHost godotHost; - private GodotPluginRegistry pluginRegistry; - - static private Intent mCurrentIntent; - - public void onNewIntent(Intent intent) { - mCurrentIntent = intent; - } - - static public Intent getCurrentIntent() { - return mCurrentIntent; - } - - private void setState(int newState) { - if (mState != newState) { - mState = newState; - mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState)); - } - } - - private void setButtonPausedState(boolean paused) { - mStatePaused = paused; - int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; - mPauseButton.setText(stringResourceID); - } - - private String[] command_line; - private boolean use_apk_expansion; - - private ViewGroup containerLayout; - public GodotRenderView mRenderView; - private boolean godot_initialized = false; - - private SensorManager mSensorManager; - private Sensor mAccelerometer; - private Sensor mGravity; - private Sensor mMagnetometer; - private Sensor mGyroscope; - - public GodotIO io; - public GodotNetUtils netUtils; - public GodotTTS tts; - private DirectoryAccessHandler directoryAccessHandler; - private FileAccessHandler fileAccessHandler; - - public interface ResultCallback { - void callback(int requestCode, int resultCode, Intent data); - } - public ResultCallback result_callback; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (getParentFragment() instanceof GodotHost) { - godotHost = (GodotHost)getParentFragment(); - } else if (getActivity() instanceof GodotHost) { - godotHost = (GodotHost)getActivity(); - } - } - - @Override - public void onDetach() { - super.onDetach(); - godotHost = null; - } - - @CallSuper - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (result_callback != null) { - result_callback.callback(requestCode, resultCode, data); - result_callback = null; - } - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainActivityResult(requestCode, resultCode, data); - } - } - - @CallSuper - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults); - } - - for (int i = 0; i < permissions.length; i++) { - GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED); - } - } - - /** - * Invoked on the render thread when the Godot setup is complete. - */ - @CallSuper - protected void onGodotSetupCompleted() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onGodotSetupCompleted(); - } - - if (godotHost != null) { - godotHost.onGodotSetupCompleted(); - } - } - - /** - * Invoked on the render thread when the Godot main loop has started. - */ - @CallSuper - protected void onGodotMainLoopStarted() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onGodotMainLoopStarted(); - } - - if (godotHost != null) { - godotHost.onGodotMainLoopStarted(); - } - } - - /** - * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer. - */ - @Keep - private boolean onVideoInit() { - final Activity activity = requireActivity(); - containerLayout = new FrameLayout(activity); - containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - - // GodotEditText layout - GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - (int)getResources().getDimension(R.dimen.text_edit_height))); - // ...add to FrameLayout - containerLayout.addView(editText); - - tts = new GodotTTS(activity); - - if (!GodotLib.setup(command_line, tts)) { - Log.e(TAG, "Unable to setup the Godot engine! Aborting..."); - alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit); - return false; - } - - if (usesVulkan()) { - if (!meetsVulkanRequirements(activity.getPackageManager())) { - alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit); - return false; - } - mRenderView = new GodotVulkanRenderView(activity, this); - } else { - // Fallback to openGl - mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl); - } - - View view = mRenderView.getView(); - containerLayout.addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - editText.setView(mRenderView); - io.setEdit(editText); - - // Listeners for keyboard height. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Report the height of virtual keyboard as it changes during the animation. - final View decorView = activity.getWindow().getDecorView(); - decorView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) { - int startBottom, endBottom; - @Override - public void onPrepare(@NonNull WindowInsetsAnimation animation) { - startBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; - } - - @NonNull - @Override - public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { - endBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; - return bounds; - } - - @NonNull - @Override - public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) { - // Find the IME animation. - WindowInsetsAnimation imeAnimation = null; - for (WindowInsetsAnimation animation : list) { - if ((animation.getTypeMask() & WindowInsets.Type.ime()) != 0) { - imeAnimation = animation; - break; - } - } - // Update keyboard height based on IME animation. - if (imeAnimation != null) { - float interpolatedFraction = imeAnimation.getInterpolatedFraction(); - // Linear interpolation between start and end values. - float keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction; - GodotLib.setVirtualKeyboardHeight((int)keyboardHeight); - } - return windowInsets; - } - - @Override - public void onEnd(@NonNull WindowInsetsAnimation animation) { - } - }); - } else { - // Infer the virtual keyboard height using visible area. - view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - // Don't allocate a new Rect every time the callback is called. - final Rect visibleSize = new Rect(); - - @Override - public void onGlobalLayout() { - final SurfaceView view = mRenderView.getView(); - view.getWindowVisibleDisplayFrame(visibleSize); - final int keyboardHeight = view.getHeight() - visibleSize.bottom; - GodotLib.setVirtualKeyboardHeight(keyboardHeight); - } - }); - } - - mRenderView.queueOnRenderThread(() -> { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onRegisterPluginWithGodotNative(); - } - setKeepScreenOn(Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); - }); - - // Include the returned non-null views in the Godot view hierarchy. - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - View pluginView = plugin.onMainCreate(activity); - if (pluginView != null) { - if (plugin.shouldBeOnTop()) { - containerLayout.addView(pluginView); - } else { - containerLayout.addView(pluginView, 0); - } - } - } - return true; - } - - /** - * Returns true if `Vulkan` is used for rendering. - */ - private boolean usesVulkan() { - final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method"); - final String renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver"); - return ("forward_plus".equals(renderer) || "mobile".equals(renderer)) && "vulkan".equals(renderingDevice); - } - - /** - * Returns true if the device meets the base requirements for Vulkan support, false otherwise. - */ - private boolean meetsVulkanRequirements(@Nullable PackageManager packageManager) { - if (packageManager == null) { - return false; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { - // Optional requirements.. log as warning if missing - Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1"); - } - - // Check for api version 1.0 - return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003); - } - - return false; - } - - public void setKeepScreenOn(final boolean p_enabled) { - runOnUiThread(() -> { - if (p_enabled) { - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - }); - } - - /** - * Used by the native code (java_godot_wrapper.h) to vibrate the device. - * @param durationMs - */ - @SuppressLint("MissingPermission") - @Keep - private void vibrate(int durationMs) { - if (durationMs > 0 && requestPermission("VIBRATE")) { - Vibrator v = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)); - } else { - // deprecated in API 26 - v.vibrate(durationMs); - } - } - } - } - - public void restart() { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } - } - - public void alert(final String message, final String title) { - alert(message, title, null); - } - - private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) { - Resources res = getResources(); - alert(res.getString(messageResId), res.getString(titleResId), okCallback); - } - - private void alert(final String message, final String title, @Nullable Runnable okCallback) { - final Activity activity = getActivity(); - runOnUiThread(() -> { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(message).setTitle(title); - builder.setPositiveButton( - "OK", - (dialog, id) -> { - if (okCallback != null) { - okCallback.run(); - } - dialog.cancel(); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - }); - } - - public int getGLESVersionCode() { - ActivityManager am = (ActivityManager)getContext().getSystemService(Context.ACTIVITY_SERVICE); - ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo(); - return deviceInfo.reqGlEsVersion; - } - - @CallSuper - protected String[] getCommandLine() { - String[] original = parseCommandLine(); - String[] updated; - List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null; - if (hostCommandLine == null || hostCommandLine.isEmpty()) { - updated = original; - } else { - updated = Arrays.copyOf(original, original.length + hostCommandLine.size()); - for (int i = 0; i < hostCommandLine.size(); i++) { - updated[original.length + i] = hostCommandLine.get(i); - } - } - return updated; - } - - private String[] parseCommandLine() { - InputStream is; - try { - is = getActivity().getAssets().open("_cl_"); - byte[] len = new byte[4]; - int r = is.read(len); - if (r < 4) { - return new String[0]; - } - int argc = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); - String[] cmdline = new String[argc]; - - for (int i = 0; i < argc; i++) { - r = is.read(len); - if (r < 4) { - return new String[0]; - } - int strlen = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); - if (strlen > 65535) { - return new String[0]; - } - byte[] arg = new byte[strlen]; - r = is.read(arg); - if (r == strlen) { - cmdline[i] = new String(arg, "UTF-8"); - } - } - return cmdline; - } catch (Exception e) { - // The _cl_ file can be missing with no adverse effect - return new String[0]; - } - } - - /** - * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused. - */ - @Keep - private boolean isActivityResumed() { - return activityResumed; - } - - /** - * Used by the native code (java_godot_wrapper.h) to access the Android surface. - */ - @Keep - private Surface getSurface() { - return mRenderView.getView().getHolder().getSurface(); - } - - /** - * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. - * @return The input fallback mapping for the current XR mode. - */ - @Keep - private String getInputFallbackMapping() { - return xrMode.inputFallbackMapping; - } - - String expansion_pack_path; - - private void initializeGodot() { - if (expansion_pack_path != null) { - String[] new_cmdline; - int cll = 0; - if (command_line != null) { - new_cmdline = new String[command_line.length + 2]; - cll = command_line.length; - for (int i = 0; i < command_line.length; i++) { - new_cmdline[i] = command_line[i]; - } - } else { - new_cmdline = new String[2]; - } - - new_cmdline[cll] = "--main-pack"; - new_cmdline[cll + 1] = expansion_pack_path; - command_line = new_cmdline; - } - - final Activity activity = getActivity(); - io = new GodotIO(activity); - netUtils = new GodotNetUtils(activity); - Context context = getContext(); - directoryAccessHandler = new DirectoryAccessHandler(context); - fileAccessHandler = new FileAccessHandler(context); - mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); - mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); - mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - - godot_initialized = GodotLib.initialize(activity, - this, - activity.getAssets(), - io, - netUtils, - directoryAccessHandler, - fileAccessHandler, - use_apk_expansion); - - result_callback = null; - } - - @Override - public void onServiceConnected(Messenger m) { - IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); - remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); - } - - @Override - public void onCreate(Bundle icicle) { - BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate"); - super.onCreate(icicle); - - final Activity activity = getActivity(); - Window window = activity.getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); - pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); - - // check for apk expansion API - boolean md5mismatch = false; - command_line = getCommandLine(); - String main_pack_md5 = null; - String main_pack_key = null; - - List<String> new_args = new LinkedList<>(); - - for (int i = 0; i < command_line.length; i++) { - boolean has_extra = i < command_line.length - 1; - if (command_line[i].equals(XRMode.REGULAR.cmdLineArg)) { - xrMode = XRMode.REGULAR; - } else if (command_line[i].equals(XRMode.OPENXR.cmdLineArg)) { - xrMode = XRMode.OPENXR; - } else if (command_line[i].equals("--debug_opengl")) { - use_debug_opengl = true; - } else if (command_line[i].equals("--use_immersive")) { - use_immersive = true; - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - UiChangeListener(); - } else if (command_line[i].equals("--use_apk_expansion")) { - use_apk_expansion = true; - } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { - main_pack_md5 = command_line[i + 1]; - i++; - } else if (has_extra && command_line[i].equals("--apk_expansion_key")) { - main_pack_key = command_line[i + 1]; - SharedPreferences prefs = activity.getSharedPreferences("app_data_keys", - MODE_PRIVATE); - Editor editor = prefs.edit(); - editor.putString("store_public_key", main_pack_key); - - editor.apply(); - i++; - } else if (command_line[i].equals("--benchmark")) { - BenchmarkUtils.setUseBenchmark(true); - new_args.add(command_line[i]); - } else if (has_extra && command_line[i].equals("--benchmark-file")) { - BenchmarkUtils.setUseBenchmark(true); - new_args.add(command_line[i]); - - // Retrieve the filepath - BenchmarkUtils.setBenchmarkFile(command_line[i + 1]); - new_args.add(command_line[i + 1]); - - i++; - } else if (command_line[i].trim().length() != 0) { - new_args.add(command_line[i]); - } - } - - if (new_args.isEmpty()) { - command_line = null; - } else { - command_line = new_args.toArray(new String[new_args.size()]); - } - if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) { - // check that environment is ok! - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - // show popup and die - } - - // Build the full path to the app's expansion files - try { - expansion_pack_path = Helpers.getSaveFilePath(getContext()); - expansion_pack_path += "/main." + activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode + "." + activity.getPackageName() + ".obb"; - } catch (Exception e) { - e.printStackTrace(); - } - - File f = new File(expansion_pack_path); - - boolean pack_valid = true; - - if (!f.exists()) { - pack_valid = false; - - } else if (obbIsCorrupted(expansion_pack_path, main_pack_md5)) { - pack_valid = false; - try { - f.delete(); - } catch (Exception e) { - } - } - - if (!pack_valid) { - Intent notifierIntent = new Intent(activity, activity.getClass()); - notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - - PendingIntent pendingIntent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - pendingIntent = PendingIntent.getActivity(activity, 0, - notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else { - pendingIntent = PendingIntent.getActivity(activity, 0, - notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - int startResult; - try { - startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired( - getContext(), - pendingIntent, - GodotDownloaderService.class); - - if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { - // This is where you do set up to display the download - // progress (next step in onCreateView) - mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, - GodotDownloaderService.class); - - return; - } - } catch (NameNotFoundException e) { - // TODO Auto-generated catch block - } - } - } - - mCurrentIntent = activity.getIntent(); - - initializeGodot(); - BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate"); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { - if (mDownloaderClientStub != null) { - View downloadingExpansionView = - inflater.inflate(R.layout.downloading_expansion, container, false); - mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar); - mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText); - mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction); - mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage); - mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed); - mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining); - mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard); - mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular); - mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton); - mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton); - - return downloadingExpansionView; - } - - return containerLayout; - } - - @Override - public void onDestroy() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainDestroy(); - } - - GodotLib.ondestroy(); - - super.onDestroy(); - - forceQuit(); - } - - @Override - public void onPause() { - super.onPause(); - activityResumed = false; - - if (!godot_initialized) { - if (null != mDownloaderClientStub) { - mDownloaderClientStub.disconnect(getActivity()); - } - return; - } - mRenderView.onActivityPaused(); - - mSensorManager.unregisterListener(this); - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainPause(); - } - } - - public boolean hasClipboard() { - return mClipboard.hasPrimaryClip(); - } - - public String getClipboard() { - ClipData clipData = mClipboard.getPrimaryClip(); - if (clipData == null) - return ""; - CharSequence text = clipData.getItemAt(0).getText(); - if (text == null) - return ""; - return text.toString(); - } - - public void setClipboard(String p_text) { - ClipData clip = ClipData.newPlainText("myLabel", p_text); - mClipboard.setPrimaryClip(clip); - } - - @Override - public void onResume() { - super.onResume(); - activityResumed = true; - if (!godot_initialized) { - if (null != mDownloaderClientStub) { - mDownloaderClientStub.connect(getActivity()); - } - return; - } - - mRenderView.onActivityResumed(); - - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - - if (use_immersive) { - Window window = getActivity().getWindow(); - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainResume(); - } - } - - public void UiChangeListener() { - final View decorView = getActivity().getWindow().getDecorView(); - decorView.setOnSystemUiVisibilityChangeListener(visibility -> { - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } - }); - } - - public float[] getRotatedValues(float values[]) { - if (values == null || values.length != 3) { - return values; - } - - Display display = - ((WindowManager)getActivity().getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); - int displayRotation = display.getRotation(); - - float[] rotatedValues = new float[3]; - switch (displayRotation) { - case Surface.ROTATION_0: - rotatedValues[0] = values[0]; - rotatedValues[1] = values[1]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_90: - rotatedValues[0] = -values[1]; - rotatedValues[1] = values[0]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_180: - rotatedValues[0] = -values[0]; - rotatedValues[1] = -values[1]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_270: - rotatedValues[0] = values[1]; - rotatedValues[1] = -values[0]; - rotatedValues[2] = values[2]; - break; - } - - return rotatedValues; - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (mRenderView == null) { - return; - } - - final int typeOfSensor = event.sensor.getType(); - switch (typeOfSensor) { - case Sensor.TYPE_ACCELEROMETER: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.accelerometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_GRAVITY: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.gravity(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_MAGNETIC_FIELD: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.magnetometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_GYROSCOPE: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.gyroscope(rotatedValues[0], rotatedValues[1], rotatedValues[2]); - }); - break; - } - } - } - - @Override - public final void onAccuracyChanged(Sensor sensor, int accuracy) { - // Do something here if sensor accuracy changes. - } - - public void onBackPressed() { - boolean shouldQuit = true; - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - if (plugin.onMainBackPressed()) { - shouldQuit = false; - } - } - - if (shouldQuit && mRenderView != null) { - mRenderView.queueOnRenderThread(GodotLib::back); - } - } - - /** - * Queue a runnable to be run on the render thread. - * <p> - * This must be called after the render thread has started. - */ - public final void runOnRenderThread(@NonNull Runnable action) { - if (mRenderView != null) { - mRenderView.queueOnRenderThread(action); - } - } - - public final void runOnUiThread(@NonNull Runnable action) { - if (getActivity() != null) { - getActivity().runOnUiThread(action); - } - } - - private void forceQuit() { - // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each - // native Godot components that is started in Godot#onVideoInit. - forceQuit(0); - } - - @Keep - private boolean forceQuit(int instanceId) { - if (godotHost == null) { - return false; - } - if (instanceId == 0) { - godotHost.onGodotForceQuit(this); - return true; - } else { - return godotHost.onGodotForceQuit(instanceId); - } - } - - private boolean obbIsCorrupted(String f, String main_pack_md5) { - try { - InputStream fis = new FileInputStream(f); - - // Create MD5 Hash - byte[] buffer = new byte[16384]; - - MessageDigest complete = MessageDigest.getInstance("MD5"); - int numRead; - do { - numRead = fis.read(buffer); - if (numRead > 0) { - complete.update(buffer, 0, numRead); - } - } while (numRead != -1); - - fis.close(); - byte[] messageDigest = complete.digest(); - - // Create Hex String - StringBuilder hexString = new StringBuilder(); - for (byte b : messageDigest) { - String s = Integer.toHexString(0xFF & b); - if (s.length() == 1) { - s = "0" + s; - } - hexString.append(s); - } - String md5str = hexString.toString(); - - if (!md5str.equals(main_pack_md5)) { - return true; - } - return false; - } catch (Exception e) { - e.printStackTrace(); - return true; - } - } - - public boolean requestPermission(String p_name) { - return PermissionsUtil.requestPermission(p_name, getActivity()); - } - - public boolean requestPermissions() { - return PermissionsUtil.requestManifestPermissions(getActivity()); - } - - public String[] getGrantedPermissions() { - return PermissionsUtil.getGrantedPermissions(getActivity()); - } - - @Keep - private String getCACertificates() { - return GodotNetUtils.getCACertificates(); - } - - /** - * The download state should trigger changes in the UI --- it may be useful - * to show the state as being indeterminate at times. This sample can be - * considered a guideline. - */ - @Override - public void onDownloadStateChanged(int newState) { - setState(newState); - boolean showDashboard = true; - boolean showCellMessage = false; - boolean paused; - boolean indeterminate; - switch (newState) { - case IDownloaderClient.STATE_IDLE: - // STATE_IDLE means the service is listening, so it's - // safe to start making remote service calls. - paused = false; - indeterminate = true; - break; - case IDownloaderClient.STATE_CONNECTING: - case IDownloaderClient.STATE_FETCHING_URL: - showDashboard = true; - paused = false; - indeterminate = true; - break; - case IDownloaderClient.STATE_DOWNLOADING: - paused = false; - showDashboard = true; - indeterminate = false; - break; - - case IDownloaderClient.STATE_FAILED_CANCELED: - case IDownloaderClient.STATE_FAILED: - case IDownloaderClient.STATE_FAILED_FETCHING_URL: - case IDownloaderClient.STATE_FAILED_UNLICENSED: - paused = true; - showDashboard = false; - indeterminate = false; - break; - case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: - case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: - showDashboard = false; - paused = true; - indeterminate = false; - showCellMessage = true; - break; - - case IDownloaderClient.STATE_PAUSED_BY_REQUEST: - paused = true; - indeterminate = false; - break; - case IDownloaderClient.STATE_PAUSED_ROAMING: - case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: - paused = true; - indeterminate = false; - break; - case IDownloaderClient.STATE_COMPLETED: - showDashboard = false; - paused = false; - indeterminate = false; - initializeGodot(); - return; - default: - paused = true; - indeterminate = true; - showDashboard = true; - } - int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE; - if (mDashboard.getVisibility() != newDashboardVisibility) { - mDashboard.setVisibility(newDashboardVisibility); - } - int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE; - if (mCellMessage.getVisibility() != cellMessageVisibility) { - mCellMessage.setVisibility(cellMessageVisibility); - } - - mPB.setIndeterminate(indeterminate); - setButtonPausedState(paused); - } - - @Override - public void onDownloadProgress(DownloadProgressInfo progress) { - mAverageSpeed.setText(getString(R.string.kilobytes_per_second, - Helpers.getSpeedString(progress.mCurrentSpeed))); - mTimeRemaining.setText(getString(R.string.time_remaining, - Helpers.getTimeRemaining(progress.mTimeRemaining))); - - mPB.setMax((int)(progress.mOverallTotal >> 8)); - mPB.setProgress((int)(progress.mOverallProgress >> 8)); - mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); - mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, - progress.mOverallTotal)); - } - - public void initInputDevices() { - mRenderView.initInputDevices(); - } - - @Keep - public GodotRenderView getRenderView() { // used by native side to get renderView - return mRenderView; - } - - @Keep - public DirectoryAccessHandler getDirectoryAccessHandler() { - return directoryAccessHandler; - } - - @Keep - public FileAccessHandler getFileAccessHandler() { - return fileAccessHandler; - } - - @Keep - private int createNewGodotInstance(String[] args) { - if (godotHost != null) { - return godotHost.onNewGodotInstanceRequested(args); - } - return 0; - } - - @Keep - private void beginBenchmarkMeasure(String label) { - BenchmarkUtils.beginBenchmarkMeasure(label); - } - - @Keep - private void endBenchmarkMeasure(String label) { - BenchmarkUtils.endBenchmarkMeasure(label); - } - - @Keep - private void dumpBenchmark(String benchmarkFile) { - BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile); - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt new file mode 100644 index 0000000000..e2e77e7796 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -0,0 +1,1064 @@ +/**************************************************************************/ +/* Godot.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.content.* +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.graphics.Rect +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.* +import android.util.Log +import android.view.* +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.FrameLayout +import androidx.annotation.Keep +import androidx.annotation.StringRes +import com.google.android.vending.expansion.downloader.* +import org.godotengine.godot.input.GodotEditText +import org.godotengine.godot.io.directory.DirectoryAccessHandler +import org.godotengine.godot.io.file.FileAccessHandler +import org.godotengine.godot.plugin.GodotPluginRegistry +import org.godotengine.godot.tts.GodotTTS +import org.godotengine.godot.utils.GodotNetUtils +import org.godotengine.godot.utils.PermissionsUtil +import org.godotengine.godot.utils.PermissionsUtil.requestPermission +import org.godotengine.godot.utils.beginBenchmarkMeasure +import org.godotengine.godot.utils.benchmarkFile +import org.godotengine.godot.utils.dumpBenchmark +import org.godotengine.godot.utils.endBenchmarkMeasure +import org.godotengine.godot.utils.useBenchmark +import org.godotengine.godot.xr.XRMode +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.* + +/** + * Core component used to interface with the native layer of the engine. + * + * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its + * lifecycle methods are properly invoked. + */ +class Godot(private val context: Context) : SensorEventListener { + + private companion object { + private val TAG = Godot::class.java.simpleName + } + + private val pluginRegistry: GodotPluginRegistry by lazy { + GodotPluginRegistry.getPluginRegistry() + } + private val mSensorManager: SensorManager by lazy { + requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager + } + private val mAccelerometer: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + } + private val mGravity: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + } + private val mMagnetometer: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + } + private val mGyroscope: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + } + private val mClipboard: ClipboardManager by lazy { + requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } + + private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> + if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { + val decorView = requireActivity().window.decorView + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + }} + + val tts = GodotTTS(context) + val directoryAccessHandler = DirectoryAccessHandler(context) + val fileAccessHandler = FileAccessHandler(context) + val netUtils = GodotNetUtils(context) + + /** + * Tracks whether [onCreate] was completed successfully. + */ + private var initializationStarted = false + + /** + * Tracks whether [GodotLib.initialize] was completed successfully. + */ + private var nativeLayerInitializeCompleted = false + + /** + * Tracks whether [GodotLib.setup] was completed successfully. + */ + private var nativeLayerSetupCompleted = false + + /** + * Tracks whether [onInitRenderView] was completed successfully. + */ + private var renderViewInitialized = false + private var primaryHost: GodotHost? = null + + var io: GodotIO? = null + + private var commandLine : MutableList<String> = ArrayList<String>() + private var xrMode = XRMode.REGULAR + private var expansionPackPath: String = "" + private var useApkExpansion = false + private var useImmersive = false + private var useDebugOpengl = false + private var darkMode = false; + + private var containerLayout: FrameLayout? = null + var renderView: GodotRenderView? = null + + /** + * Returns true if the native engine has been initialized through [onInitNativeLayer], false otherwise. + */ + private fun isNativeInitialized() = nativeLayerInitializeCompleted && nativeLayerSetupCompleted + + /** + * Returns true if the engine has been initialized, false otherwise. + */ + fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized + + /** + * Provides access to the primary host [Activity] + */ + fun getActivity() = primaryHost?.activity + private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null") + + /** + * Start initialization of the Godot engine. + * + * This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete + * initialization of the engine. + * + * @throws IllegalArgumentException exception if the specified expansion pack (if any) + * is invalid. + */ + fun onCreate(primaryHost: GodotHost) { + if (this.primaryHost != null || initializationStarted) { + Log.d(TAG, "OnCreate already invoked") + return + } + + darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + + beginBenchmarkMeasure("Startup", "Godot::onCreate") + try { + this.primaryHost = primaryHost + val activity = requireActivity() + val window = activity.window + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) + if (io == null) { + io = GodotIO(activity) + } + + // check for apk expansion API + commandLine = getCommandLine() + var mainPackMd5: String? = null + var mainPackKey: String? = null + val newArgs: MutableList<String> = ArrayList() + var i = 0 + while (i < commandLine.size) { + val hasExtra: Boolean = i < commandLine.size - 1 + if (commandLine[i] == XRMode.REGULAR.cmdLineArg) { + xrMode = XRMode.REGULAR + } else if (commandLine[i] == XRMode.OPENXR.cmdLineArg) { + xrMode = XRMode.OPENXR + } else if (commandLine[i] == "--debug_opengl") { + useDebugOpengl = true + } else if (commandLine[i] == "--use_immersive") { + useImmersive = true + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + registerUiChangeListener() + } else if (commandLine[i] == "--use_apk_expansion") { + useApkExpansion = true + } else if (hasExtra && commandLine[i] == "--apk_expansion_md5") { + mainPackMd5 = commandLine[i + 1] + i++ + } else if (hasExtra && commandLine[i] == "--apk_expansion_key") { + mainPackKey = commandLine[i + 1] + val prefs = activity.getSharedPreferences( + "app_data_keys", + Context.MODE_PRIVATE + ) + val editor = prefs.edit() + editor.putString("store_public_key", mainPackKey) + editor.apply() + i++ + } else if (commandLine[i] == "--benchmark") { + useBenchmark = true + newArgs.add(commandLine[i]) + } else if (hasExtra && commandLine[i] == "--benchmark-file") { + useBenchmark = true + newArgs.add(commandLine[i]) + + // Retrieve the filepath + benchmarkFile = commandLine[i + 1] + newArgs.add(commandLine[i + 1]) + + i++ + } else if (commandLine[i].trim().isNotEmpty()) { + newArgs.add(commandLine[i]) + } + i++ + } + commandLine = if (newArgs.isEmpty()) { mutableListOf() } else { newArgs } + if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) { + // Build the full path to the app's expansion files + try { + expansionPackPath = Helpers.getSaveFilePath(context) + expansionPackPath += "/main." + activity.packageManager.getPackageInfo( + activity.packageName, + 0 + ).versionCode + "." + activity.packageName + ".obb" + } catch (e: java.lang.Exception) { + Log.e(TAG, "Unable to build full path to the app's expansion files", e) + } + val f = File(expansionPackPath) + var packValid = true + if (!f.exists()) { + packValid = false + } else if (obbIsCorrupted(expansionPackPath, mainPackMd5)) { + packValid = false + try { + f.delete() + } catch (_: java.lang.Exception) { + } + } + if (!packValid) { + // Aborting engine initialization + throw IllegalArgumentException("Invalid expansion pack") + } + } + + initializationStarted = true + } catch (e: java.lang.Exception) { + // Clear the primary host and rethrow + this.primaryHost = null + initializationStarted = false + throw e + } finally { + endBenchmarkMeasure("Startup", "Godot::onCreate"); + } + } + + /** + * Initializes the native layer of the Godot engine. + * + * This must be preceded by [onCreate] and followed by [onInitRenderView] to complete + * initialization of the engine. + * + * @return false if initialization of the native layer fails, true otherwise. + * + * @throws IllegalStateException if [onCreate] has not been called. + */ + fun onInitNativeLayer(host: GodotHost): Boolean { + if (!initializationStarted) { + throw IllegalStateException("OnCreate must be invoked successfully prior to initializing the native layer") + } + if (isNativeInitialized()) { + Log.d(TAG, "OnInitNativeLayer already invoked") + return true + } + if (host != primaryHost) { + Log.e(TAG, "Native initialization is only supported for the primary host") + return false + } + + if (expansionPackPath.isNotEmpty()) { + commandLine.add("--main-pack") + commandLine.add(expansionPackPath) + } + val activity = requireActivity() + if (!nativeLayerInitializeCompleted) { + nativeLayerInitializeCompleted = GodotLib.initialize( + activity, + this, + activity.assets, + io, + netUtils, + directoryAccessHandler, + fileAccessHandler, + useApkExpansion, + ) + } + + if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { + nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) + if (!nativeLayerSetupCompleted) { + Log.e(TAG, "Unable to setup the Godot engine! Aborting...") + alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit) + } + } + return isNativeInitialized() + } + + /** + * Used to complete initialization of the view used by the engine for rendering. + * + * This must be preceded by [onCreate] and [onInitNativeLayer] in that order to properly + * initialize the engine. + * + * @param host The [GodotHost] that's initializing the render views + * @param providedContainerLayout Optional argument; if provided, this is reused to host the Godot's render views + * + * @return A [FrameLayout] instance containing Godot's render views if initialization is successful, null otherwise. + * + * @throws IllegalStateException if [onInitNativeLayer] has not been called + */ + @JvmOverloads + fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? { + if (!isNativeInitialized()) { + throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view") + } + + try { + val activity: Activity = host.activity + containerLayout = providedContainerLayout + containerLayout?.removeAllViews() + containerLayout?.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // GodotEditText layout + val editText = GodotEditText(activity) + editText.layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + activity.resources.getDimension(R.dimen.text_edit_height).toInt() + ) + // Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer. + editText.setBackgroundColor(Color.TRANSPARENT) + // ...add to FrameLayout + containerLayout?.addView(editText) + renderView = if (usesVulkan()) { + if (!meetsVulkanRequirements(activity.packageManager)) { + alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit) + return null + } + GodotVulkanRenderView(host, this) + } else { + // Fallback to openGl + GodotGLRenderView(host, this, xrMode, useDebugOpengl) + } + + if (host == primaryHost) { + renderView!!.startRenderer() + } + val view: View = renderView!!.view + containerLayout?.addView( + view, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + editText.setView(renderView) + io?.setEdit(editText) + + // Listeners for keyboard height. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Report the height of virtual keyboard as it changes during the animation. + val decorView = activity.window.decorView + decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + var startBottom = 0 + var endBottom = 0 + override fun onPrepare(animation: WindowInsetsAnimation) { + startBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom + } + + override fun onStart(animation: WindowInsetsAnimation, bounds: WindowInsetsAnimation.Bounds): WindowInsetsAnimation.Bounds { + endBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom + return bounds + } + + override fun onProgress(windowInsets: WindowInsets, list: List<WindowInsetsAnimation>): WindowInsets { + // Find the IME animation. + var imeAnimation: WindowInsetsAnimation? = null + for (animation in list) { + if (animation.typeMask and WindowInsets.Type.ime() != 0) { + imeAnimation = animation + break + } + } + // Update keyboard height based on IME animation. + if (imeAnimation != null) { + val interpolatedFraction = imeAnimation.interpolatedFraction + // Linear interpolation between start and end values. + val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction + GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt()) + } + return windowInsets + } + + override fun onEnd(animation: WindowInsetsAnimation) {} + }) + } else { + // Infer the virtual keyboard height using visible area. + view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + // Don't allocate a new Rect every time the callback is called. + val visibleSize = Rect() + override fun onGlobalLayout() { + val surfaceView = renderView!!.view + surfaceView.getWindowVisibleDisplayFrame(visibleSize) + val keyboardHeight = surfaceView.height - visibleSize.bottom + GodotLib.setVirtualKeyboardHeight(keyboardHeight) + } + }) + } + + if (host == primaryHost) { + renderView!!.queueOnRenderThread { + for (plugin in pluginRegistry.allPlugins) { + plugin.onRegisterPluginWithGodotNative() + } + setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))) + } + + // Include the returned non-null views in the Godot view hierarchy. + for (plugin in pluginRegistry.allPlugins) { + val pluginView = plugin.onMainCreate(activity) + if (pluginView != null) { + if (plugin.shouldBeOnTop()) { + containerLayout?.addView(pluginView) + } else { + containerLayout?.addView(pluginView, 0) + } + } + } + } + renderViewInitialized = true + } finally { + if (!renderViewInitialized) { + containerLayout?.removeAllViews() + containerLayout = null + } + } + return containerLayout + } + + fun onStart(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityStarted() + } + + fun onResume(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityResumed() + if (mAccelerometer != null) { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) + } + if (mGravity != null) { + mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME) + } + if (mMagnetometer != null) { + mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) + } + if (mGyroscope != null) { + mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME) + } + if (useImmersive) { + val window = requireActivity().window + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + } + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainResume() + } + } + + fun onPause(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityPaused() + mSensorManager.unregisterListener(this) + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainPause() + } + } + + fun onStop(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityStopped() + } + + fun onDestroy(primaryHost: GodotHost) { + if (this.primaryHost != primaryHost) { + return + } + + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainDestroy() + } + + runOnRenderThread { + GodotLib.ondestroy() + forceQuit() + } + } + + /** + * Configuration change callback + */ + fun onConfigurationChanged(newConfig: Configuration) { + var newDarkMode = newConfig.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + if (darkMode != newDarkMode) { + darkMode = newDarkMode + GodotLib.onNightModeChanged() + } + } + + /** + * Activity result callback + */ + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainActivityResult(requestCode, resultCode, data) + } + } + + /** + * Permissions request callback + */ + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String?>, + grantResults: IntArray + ) { + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults) + } + for (i in permissions.indices) { + GodotLib.requestPermissionResult( + permissions[i], + grantResults[i] == PackageManager.PERMISSION_GRANTED + ) + } + } + + /** + * Invoked on the render thread when the Godot setup is complete. + */ + private fun onGodotSetupCompleted() { + Log.d(TAG, "OnGodotSetupCompleted") + + // These properties are defined after Godot setup completion, so we retrieve them here. + val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")); + + runOnUiThread { + renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + setRotaryInputAxis(rotaryInputAxis) + } + } + + for (plugin in pluginRegistry.allPlugins) { + plugin.onGodotSetupCompleted() + } + primaryHost?.onGodotSetupCompleted() + } + + /** + * Invoked on the render thread when the Godot main loop has started. + */ + private fun onGodotMainLoopStarted() { + Log.d(TAG, "OnGodotMainLoopStarted") + + for (plugin in pluginRegistry.allPlugins) { + plugin.onGodotMainLoopStarted() + } + primaryHost?.onGodotMainLoopStarted() + } + + private fun restart() { + primaryHost?.onGodotRestartRequested(this) + } + + private fun registerUiChangeListener() { + val decorView = requireActivity().window.decorView + decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) + } + + @Keep + private fun alert(message: String, title: String) { + alert(message, title, null) + } + + private fun alert( + @StringRes messageResId: Int, + @StringRes titleResId: Int, + okCallback: Runnable? + ) { + val res: Resources = getActivity()?.resources ?: return + alert(res.getString(messageResId), res.getString(titleResId), okCallback) + } + + private fun alert(message: String, title: String, okCallback: Runnable?) { + val activity: Activity = getActivity() ?: return + runOnUiThread { + val builder = AlertDialog.Builder(activity) + builder.setMessage(message).setTitle(title) + builder.setPositiveButton( + "OK" + ) { dialog: DialogInterface, id: Int -> + okCallback?.run() + dialog.cancel() + } + val dialog = builder.create() + dialog.show() + } + } + + /** + * Queue a runnable to be run on the render thread. + * + * This must be called after the render thread has started. + */ + fun runOnRenderThread(action: Runnable) { + if (renderView != null) { + renderView!!.queueOnRenderThread(action) + } + } + + /** + * Runs the specified action on the UI thread. + * If the current thread is the UI thread, then the action is executed immediately. + * If the current thread is not the UI thread, the action is posted to the event queue + * of the UI thread. + */ + fun runOnUiThread(action: Runnable) { + val activity: Activity = getActivity() ?: return + activity.runOnUiThread(action) + } + + /** + * Returns true if the call is being made on the Ui thread. + */ + private fun isOnUiThread() = Looper.myLooper() == Looper.getMainLooper() + + /** + * Returns true if `Vulkan` is used for rendering. + */ + private fun usesVulkan(): Boolean { + val renderer = GodotLib.getGlobal("rendering/renderer/rendering_method") + val renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver") + return ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice + } + + /** + * Returns true if the device meets the base requirements for Vulkan support, false otherwise. + */ + private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean { + if (packageManager == null) { + return false + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { + // Optional requirements.. log as warning if missing + Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1") + } + + // Check for api version 1.0 + return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003) + } + return false + } + + private fun setKeepScreenOn(enabled: Boolean) { + runOnUiThread { + if (enabled) { + getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + getActivity()?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + /** + * Returns true if dark mode is supported, false otherwise. + */ + @Keep + private fun isDarkModeSupported(): Boolean { + return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED + } + + /** + * Returns true if dark mode is supported and enabled, false otherwise. + */ + @Keep + private fun isDarkMode(): Boolean { + return darkMode + } + + fun hasClipboard(): Boolean { + return mClipboard.hasPrimaryClip() + } + + fun getClipboard(): String? { + val clipData = mClipboard.primaryClip ?: return "" + val text = clipData.getItemAt(0).text ?: return "" + return text.toString() + } + + fun setClipboard(text: String?) { + val clip = ClipData.newPlainText("myLabel", text) + mClipboard.setPrimaryClip(clip) + } + + private fun forceQuit() { + forceQuit(0) + } + + @Keep + private fun forceQuit(instanceId: Int): Boolean { + if (primaryHost == null) { + return false + } + return if (instanceId == 0) { + primaryHost!!.onGodotForceQuit(this) + true + } else { + primaryHost!!.onGodotForceQuit(instanceId) + } + } + + fun onBackPressed(host: GodotHost) { + if (host != primaryHost) { + return + } + + var shouldQuit = true + for (plugin in pluginRegistry.allPlugins) { + if (plugin.onMainBackPressed()) { + shouldQuit = false + } + } + if (shouldQuit && renderView != null) { + renderView!!.queueOnRenderThread { GodotLib.back() } + } + } + + private fun getRotatedValues(values: FloatArray?): FloatArray? { + if (values == null || values.size != 3) { + return values + } + val display = + (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + val displayRotation = display.rotation + val rotatedValues = FloatArray(3) + when (displayRotation) { + Surface.ROTATION_0 -> { + rotatedValues[0] = values[0] + rotatedValues[1] = values[1] + rotatedValues[2] = values[2] + } + Surface.ROTATION_90 -> { + rotatedValues[0] = -values[1] + rotatedValues[1] = values[0] + rotatedValues[2] = values[2] + } + Surface.ROTATION_180 -> { + rotatedValues[0] = -values[0] + rotatedValues[1] = -values[1] + rotatedValues[2] = values[2] + } + Surface.ROTATION_270 -> { + rotatedValues[0] = values[1] + rotatedValues[1] = -values[0] + rotatedValues[2] = values[2] + } + } + return rotatedValues + } + + override fun onSensorChanged(event: SensorEvent) { + if (renderView == null) { + return + } + when (event.sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.accelerometer( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_GRAVITY -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.gravity( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_MAGNETIC_FIELD -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.magnetometer( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_GYROSCOPE -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.gyroscope( + rotatedValues!![0], rotatedValues[1], rotatedValues[2] + ) + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + + /** + * Used by the native code (java_godot_wrapper.h) to vibrate the device. + * @param durationMs + */ + @SuppressLint("MissingPermission") + @Keep + private fun vibrate(durationMs: Int) { + if (durationMs > 0 && requestPermission("VIBRATE")) { + val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + } else { + // deprecated in API 26 + vibratorService.vibrate(durationMs.toLong()) + } + } + } + + private fun getCommandLine(): MutableList<String> { + val original: MutableList<String> = parseCommandLine() + val hostCommandLine = primaryHost?.commandLine + if (!hostCommandLine.isNullOrEmpty()) { + original.addAll(hostCommandLine) + } + return original + } + + private fun parseCommandLine(): MutableList<String> { + val inputStream: InputStream + return try { + inputStream = requireActivity().assets.open("_cl_") + val len = ByteArray(4) + var r = inputStream.read(len) + if (r < 4) { + return mutableListOf() + } + val argc = + (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) + val cmdline = ArrayList<String>(argc) + for (i in 0 until argc) { + r = inputStream.read(len) + if (r < 4) { + return mutableListOf() + } + val strlen = + (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) + if (strlen > 65535) { + return mutableListOf() + } + val arg = ByteArray(strlen) + r = inputStream.read(arg) + if (r == strlen) { + cmdline.add(String(arg, StandardCharsets.UTF_8)) + } + } + cmdline + } catch (e: Exception) { + // The _cl_ file can be missing with no adverse effect + mutableListOf() + } + } + + /** + * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. + * @return The input fallback mapping for the current XR mode. + */ + @Keep + private fun getInputFallbackMapping(): String? { + return xrMode.inputFallbackMapping + } + + fun requestPermission(name: String?): Boolean { + return requestPermission(name, getActivity()) + } + + fun requestPermissions(): Boolean { + return PermissionsUtil.requestManifestPermissions(getActivity()) + } + + fun getGrantedPermissions(): Array<String?>? { + return PermissionsUtil.getGrantedPermissions(getActivity()) + } + + /** + * Return true if the given feature is supported. + */ + @Keep + private fun hasFeature(feature: String): Boolean { + for (plugin in pluginRegistry.allPlugins) { + if (plugin.supportsFeature(feature)) { + return true + } + } + return false + } + + /** + * Get the list of gdextension modules to register. + */ + @Keep + private fun getGDExtensionConfigFiles(): Array<String> { + val configFiles = mutableSetOf<String>() + for (plugin in pluginRegistry.allPlugins) { + configFiles.addAll(plugin.pluginGDExtensionLibrariesPaths) + } + + return configFiles.toTypedArray() + } + + @Keep + private fun getCACertificates(): String { + return GodotNetUtils.getCACertificates() + } + + private fun obbIsCorrupted(f: String, mainPackMd5: String): Boolean { + return try { + val fis: InputStream = FileInputStream(f) + + // Create MD5 Hash + val buffer = ByteArray(16384) + val complete = MessageDigest.getInstance("MD5") + var numRead: Int + do { + numRead = fis.read(buffer) + if (numRead > 0) { + complete.update(buffer, 0, numRead) + } + } while (numRead != -1) + fis.close() + val messageDigest = complete.digest() + + // Create Hex String + val hexString = StringBuilder() + for (b in messageDigest) { + var s = Integer.toHexString(0xFF and b.toInt()) + if (s.length == 1) { + s = "0$s" + } + hexString.append(s) + } + val md5str = hexString.toString() + md5str != mainPackMd5 + } catch (e: java.lang.Exception) { + e.printStackTrace() + true + } + } + + @Keep + private fun initInputDevices() { + renderView!!.initInputDevices() + } + + @Keep + private fun createNewGodotInstance(args: Array<String>): Int { + return primaryHost?.onNewGodotInstanceRequested(args) ?: 0 + } + + @Keep + private fun nativeBeginBenchmarkMeasure(scope: String, label: String) { + beginBenchmarkMeasure(scope, label) + } + + @Keep + private fun nativeEndBenchmarkMeasure(scope: String, label: String) { + endBenchmarkMeasure(scope, label) + } + + @Keep + private fun nativeDumpBenchmark(benchmarkFile: String) { + dumpBenchmark(fileAccessHandler, benchmarkFile) + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt new file mode 100644 index 0000000000..e01c5481d5 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -0,0 +1,182 @@ +/**************************************************************************/ +/* GodotActivity.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import androidx.annotation.CallSuper +import androidx.fragment.app.FragmentActivity +import org.godotengine.godot.utils.PermissionsUtil +import org.godotengine.godot.utils.ProcessPhoenix + +/** + * Base abstract activity for Android apps intending to use Godot as the primary screen. + * + * Also a reference implementation for how to setup and use the [GodotFragment] fragment + * within an Android app. + */ +abstract class GodotActivity : FragmentActivity(), GodotHost { + + companion object { + private val TAG = GodotActivity::class.java.simpleName + + @JvmStatic + protected val EXTRA_FORCE_QUIT = "force_quit_requested" + @JvmStatic + protected val EXTRA_NEW_LAUNCH = "new_launch_requested" + } + + /** + * Interaction with the [Godot] object is delegated to the [GodotFragment] class. + */ + protected var godotFragment: GodotFragment? = null + private set + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.godot_app_layout) + + handleStartIntent(intent, true) + + val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container) + if (currentFragment is GodotFragment) { + Log.v(TAG, "Reusing existing Godot fragment instance.") + godotFragment = currentFragment + } else { + Log.v(TAG, "Creating new Godot fragment instance.") + godotFragment = initGodotInstance() + supportFragmentManager.beginTransaction().replace(R.id.godot_fragment_container, godotFragment!!).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss() + } + } + + override fun onDestroy() { + Log.v(TAG, "Destroying Godot app...") + super.onDestroy() + if (godotFragment != null) { + terminateGodotInstance(godotFragment!!.godot) + } + } + + override fun onGodotForceQuit(instance: Godot) { + runOnUiThread { terminateGodotInstance(instance) } + } + + private fun terminateGodotInstance(instance: Godot) { + if (godotFragment != null && instance === godotFragment!!.godot) { + Log.v(TAG, "Force quitting Godot instance") + ProcessPhoenix.forceQuit(this) + } + } + + override fun onGodotRestartRequested(instance: Godot) { + runOnUiThread { + if (godotFragment != null && instance === godotFragment!!.godot) { + // It's very hard to properly de-initialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing static data). + Log.v(TAG, "Restarting Godot instance...") + ProcessPhoenix.triggerRebirth(this) + } + } + } + + override fun onNewIntent(newIntent: Intent) { + super.onNewIntent(newIntent) + intent = newIntent + + handleStartIntent(newIntent, false) + + godotFragment?.onNewIntent(newIntent) + } + + private fun handleStartIntent(intent: Intent, newLaunch: Boolean) { + val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false) + if (forceQuitRequested) { + Log.d(TAG, "Force quit requested, terminating..") + ProcessPhoenix.forceQuit(this) + return + } + if (!newLaunch) { + val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false) + if (newLaunchRequested) { + Log.d(TAG, "New launch requested, restarting..") + val restartIntent = Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false) + ProcessPhoenix.triggerRebirth(this, restartIntent) + return + } + } + } + + @CallSuper + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + godotFragment?.onActivityResult(requestCode, resultCode, data) + } + + @CallSuper + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults) + + // Logging the result of permission requests + if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE || requestCode == PermissionsUtil.REQUEST_SINGLE_PERMISSION_REQ_CODE) { + Log.d(TAG, "Received permissions request result..") + for (i in permissions.indices) { + val permissionGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED + Log.d(TAG, "Permission ${permissions[i]} ${if (permissionGranted) { "granted"} else { "denied" }}") + } + } + } + + override fun onBackPressed() { + godotFragment?.onBackPressed() ?: super.onBackPressed() + } + + override fun getActivity(): Activity? { + return this + } + + override fun getGodot(): Godot? { + return godotFragment?.godot + } + + /** + * Used to initialize the Godot fragment instance in [onCreate]. + */ + protected open fun initGodotInstance(): GodotFragment { + return GodotFragment() + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java new file mode 100644 index 0000000000..a323045e1b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -0,0 +1,475 @@ +/**************************************************************************/ +/* GodotFragment.java */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot; + +import org.godotengine.godot.plugin.GodotPlugin; +import org.godotengine.godot.utils.BenchmarkUtils; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Messenger; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; +import com.google.android.vending.expansion.downloader.IDownloaderService; +import com.google.android.vending.expansion.downloader.IStub; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Base fragment for Android apps intending to use Godot for part of the app's UI. + */ +public class GodotFragment extends Fragment implements IDownloaderClient, GodotHost { + private static final String TAG = GodotFragment.class.getSimpleName(); + + private IStub mDownloaderClientStub; + private TextView mStatusText; + private TextView mProgressFraction; + private TextView mProgressPercent; + private TextView mAverageSpeed; + private TextView mTimeRemaining; + private ProgressBar mPB; + + private View mDashboard; + private View mCellMessage; + + private Button mPauseButton; + private Button mWiFiSettingsButton; + + private FrameLayout godotContainerLayout; + private boolean mStatePaused; + private int mState; + + @Nullable + private GodotHost parentHost; + private Godot godot; + + static private Intent mCurrentIntent; + + public void onNewIntent(Intent intent) { + mCurrentIntent = intent; + } + + static public Intent getCurrentIntent() { + return mCurrentIntent; + } + + private void setState(int newState) { + if (mState != newState) { + mState = newState; + mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState)); + } + } + + private void setButtonPausedState(boolean paused) { + mStatePaused = paused; + int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; + mPauseButton.setText(stringResourceID); + } + + public interface ResultCallback { + void callback(int requestCode, int resultCode, Intent data); + } + public ResultCallback resultCallback; + + @Override + public Godot getGodot() { + return godot; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof GodotHost) { + parentHost = (GodotHost)getParentFragment(); + } else if (getActivity() instanceof GodotHost) { + parentHost = (GodotHost)getActivity(); + } + } + + @Override + public void onDetach() { + super.onDetach(); + parentHost = null; + } + + @CallSuper + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + godot.onConfigurationChanged(newConfig); + } + + @CallSuper + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCallback != null) { + resultCallback.callback(requestCode, resultCode, data); + resultCallback = null; + } + + godot.onActivityResult(requestCode, resultCode, data); + } + + @CallSuper + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + godot.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onServiceConnected(Messenger m) { + IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); + remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); + } + + @Override + public void onCreate(Bundle icicle) { + BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate"); + super.onCreate(icicle); + + final Activity activity = getActivity(); + mCurrentIntent = activity.getIntent(); + + godot = new Godot(requireContext()); + performEngineInitialization(); + BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate"); + } + + private void performEngineInitialization() { + try { + godot.onCreate(this); + + if (!godot.onInitNativeLayer(this)) { + throw new IllegalStateException("Unable to initialize engine native layer"); + } + + godotContainerLayout = godot.onInitRenderView(this); + if (godotContainerLayout == null) { + throw new IllegalStateException("Unable to initialize engine render view"); + } + } catch (IllegalArgumentException ignored) { + final Activity activity = getActivity(); + Intent notifierIntent = new Intent(activity, activity.getClass()); + notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + int startResult; + try { + startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(getContext(), pendingIntent, GodotDownloaderService.class); + + if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { + // This is where you do set up to display the download + // progress (next step in onCreateView) + mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, GodotDownloaderService.class); + return; + } + + // Restart engine initialization + performEngineInitialization(); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to start download service", e); + } + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle icicle) { + if (mDownloaderClientStub != null) { + View downloadingExpansionView = + inflater.inflate(R.layout.downloading_expansion, container, false); + mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar); + mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText); + mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction); + mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage); + mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed); + mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining); + mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard); + mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular); + mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton); + mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton); + + return downloadingExpansionView; + } + + return godotContainerLayout; + } + + @Override + public void onDestroy() { + godot.onDestroy(this); + super.onDestroy(); + } + + @Override + public void onPause() { + super.onPause(); + + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.disconnect(getActivity()); + } + return; + } + + godot.onPause(this); + } + + @Override + public void onStop() { + super.onStop(); + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.disconnect(getActivity()); + } + return; + } + + godot.onStop(this); + } + + @Override + public void onStart() { + super.onStart(); + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.connect(getActivity()); + } + return; + } + + godot.onStart(this); + } + + @Override + public void onResume() { + super.onResume(); + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.connect(getActivity()); + } + return; + } + + godot.onResume(this); + } + + public void onBackPressed() { + godot.onBackPressed(this); + } + + /** + * The download state should trigger changes in the UI --- it may be useful + * to show the state as being indeterminate at times. This sample can be + * considered a guideline. + */ + @Override + public void onDownloadStateChanged(int newState) { + setState(newState); + boolean showDashboard = true; + boolean showCellMessage = false; + boolean paused; + boolean indeterminate; + switch (newState) { + case IDownloaderClient.STATE_IDLE: + // STATE_IDLE means the service is listening, so it's + // safe to start making remote service calls. + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_CONNECTING: + case IDownloaderClient.STATE_FETCHING_URL: + showDashboard = true; + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_DOWNLOADING: + paused = false; + showDashboard = true; + indeterminate = false; + break; + + case IDownloaderClient.STATE_FAILED_CANCELED: + case IDownloaderClient.STATE_FAILED: + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + case IDownloaderClient.STATE_FAILED_UNLICENSED: + paused = true; + showDashboard = false; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: + showDashboard = false; + paused = true; + indeterminate = false; + showCellMessage = true; + break; + + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_ROAMING: + case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_COMPLETED: + showDashboard = false; + paused = false; + indeterminate = false; + performEngineInitialization(); + return; + default: + paused = true; + indeterminate = true; + showDashboard = true; + } + int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE; + if (mDashboard.getVisibility() != newDashboardVisibility) { + mDashboard.setVisibility(newDashboardVisibility); + } + int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE; + if (mCellMessage.getVisibility() != cellMessageVisibility) { + mCellMessage.setVisibility(cellMessageVisibility); + } + + mPB.setIndeterminate(indeterminate); + setButtonPausedState(paused); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + mAverageSpeed.setText(getString(R.string.kilobytes_per_second, + Helpers.getSpeedString(progress.mCurrentSpeed))); + mTimeRemaining.setText(getString(R.string.time_remaining, + Helpers.getTimeRemaining(progress.mTimeRemaining))); + + mPB.setMax((int)(progress.mOverallTotal >> 8)); + mPB.setProgress((int)(progress.mOverallProgress >> 8)); + mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); + mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, + progress.mOverallTotal)); + } + + @CallSuper + @Override + public List<String> getCommandLine() { + return parentHost != null ? parentHost.getCommandLine() : Collections.emptyList(); + } + + @CallSuper + @Override + public void onGodotSetupCompleted() { + if (parentHost != null) { + parentHost.onGodotSetupCompleted(); + } + } + + @CallSuper + @Override + public void onGodotMainLoopStarted() { + if (parentHost != null) { + parentHost.onGodotMainLoopStarted(); + } + } + + @Override + public void onGodotForceQuit(Godot instance) { + if (parentHost != null) { + parentHost.onGodotForceQuit(instance); + } + } + + @Override + public boolean onGodotForceQuit(int godotInstanceId) { + return parentHost != null && parentHost.onGodotForceQuit(godotInstanceId); + } + + @Override + public void onGodotRestartRequested(Godot instance) { + if (parentHost != null) { + parentHost.onGodotRestartRequested(instance); + } + } + + @Override + public int onNewGodotInstanceRequested(String[] args) { + if (parentHost != null) { + return parentHost.onNewGodotInstanceRequested(args); + } + return 0; + } + + @Override + @CallSuper + public Set<GodotPlugin> getHostPlugins(Godot engine) { + if (parentHost != null) { + return parentHost.getHostPlugins(engine); + } + return Collections.emptySet(); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index b465377743..81043ce782 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,10 +29,10 @@ /**************************************************************************/ package org.godotengine.godot; + import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.gl.GodotRenderer; import org.godotengine.godot.input.GodotInputHandler; -import org.godotengine.godot.utils.GLUtils; import org.godotengine.godot.xr.XRMode; import org.godotengine.godot.xr.ovr.OvrConfigChooser; import org.godotengine.godot.xr.ovr.OvrContextFactory; @@ -78,22 +78,23 @@ import java.io.InputStream; * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. */ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { + private final GodotHost host; private final Godot godot; private final GodotInputHandler inputHandler; private final GodotRenderer godotRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) { - super(context); - GLUtils.use_debug_opengl = p_use_debug_opengl; + public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) { + super(host.getActivity()); + this.host = host; this.godot = godot; this.inputHandler = new GodotInputHandler(this); this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } - init(xrMode, false); + init(xrMode, false, useDebugOpengl); } @Override @@ -113,17 +114,35 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Override public void onActivityPaused() { - onPause(); + queueEvent(() -> { + GodotLib.focusout(); + // Pause the renderer + godotRenderer.onActivityPaused(); + }); + } + + @Override + public void onActivityStopped() { + pauseGLThread(); } @Override public void onActivityResumed() { - onResume(); + queueEvent(() -> { + // Resume the renderer + godotRenderer.onActivityResumed(); + GodotLib.focusin(); + }); + } + + @Override + public void onActivityStarted() { + resumeGLThread(); } @Override public void onBackPressed() { - godot.onBackPressed(); + godot.onBackPressed(host); } @Override @@ -233,7 +252,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView return super.onResolvePointerIcon(me, pointerIndex); } - private void init(XRMode xrMode, boolean translucent) { + private void init(XRMode xrMode, boolean translucent, boolean useDebugOpengl) { setPreserveEGLContextOnPause(true); setFocusableInTouchMode(true); switch (xrMode) { @@ -262,7 +281,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView /* Setup the context factory for 2.0 rendering. * See ContextFactory class definition below */ - setEGLContextFactory(new RegularContextFactory()); + setEGLContextFactory(new RegularContextFactory(useDebugOpengl)); /* We need to choose an EGLConfig that matches the format of * our surface exactly. This is going to be done in our @@ -275,30 +294,11 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView new RegularConfigChooser(8, 8, 8, 8, 16, 0))); break; } - - /* Set the renderer responsible for frame rendering */ - setRenderer(godotRenderer); - } - - @Override - public void onResume() { - super.onResume(); - - queueEvent(() -> { - // Resume the renderer - godotRenderer.onActivityResumed(); - GodotLib.focusin(); - }); } @Override - public void onPause() { - super.onPause(); - - queueEvent(() -> { - GodotLib.focusout(); - // Pause the renderer - godotRenderer.onActivityPaused(); - }); + public void startRenderer() { + /* Set the renderer responsible for frame rendering */ + setRenderer(godotRenderer); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 7700b9b628..1862b9fa9b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -30,37 +30,42 @@ package org.godotengine.godot; +import org.godotengine.godot.plugin.GodotPlugin; + +import android.app.Activity; + import java.util.Collections; import java.util.List; +import java.util.Set; /** - * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment. + * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} engine. */ public interface GodotHost { /** - * Provides a set of command line parameters to setup the engine. + * Provides a set of command line parameters to setup the {@link Godot} engine. */ default List<String> getCommandLine() { return Collections.emptyList(); } /** - * Invoked on the render thread when the Godot setup is complete. + * Invoked on the render thread when setup of the {@link Godot} engine is complete. */ default void onGodotSetupCompleted() {} /** - * Invoked on the render thread when the Godot main loop has started. + * Invoked on the render thread when the {@link Godot} engine main loop has started. */ default void onGodotMainLoopStarted() {} /** - * Invoked on the render thread to terminate the given Godot instance. + * Invoked on the render thread to terminate the given {@link Godot} engine instance. */ default void onGodotForceQuit(Godot instance) {} /** - * Invoked on the render thread to terminate the Godot instance with the given id. + * Invoked on the render thread to terminate the {@link Godot} engine instance with the given id. * @param godotInstanceId id of the Godot instance to terminate. See {@code onNewGodotInstanceRequested} * * @return true if successful, false otherwise. @@ -86,4 +91,21 @@ public interface GodotHost { default int onNewGodotInstanceRequested(String[] args) { return 0; } + + /** + * Provide access to the Activity hosting the {@link Godot} engine. + */ + Activity getActivity(); + + /** + * Provide access to the hosted {@link Godot} engine. + */ + Godot getGodot(); + + /** + * Returns a set of {@link GodotPlugin} to be registered with the hosted {@link Godot} engine. + */ + default Set<GodotPlugin> getHostPlugins(Godot engine) { + return Collections.emptySet(); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index edcd9c4d1f..4b51bd778d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -178,12 +178,10 @@ public class GodotIO { } public int[] getDisplaySafeArea() { - DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); - Display display = activity.getWindowManager().getDefaultDisplay(); - Point size = new Point(); - display.getRealSize(size); + Rect rect = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); - int[] result = { 0, 0, size.x, size.y }; + int[] result = { rect.left, rect.top, rect.right, rect.bottom }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets(); DisplayCutout cutout = insets.getDisplayCutout(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index b9ecd6971d..d0c3d4a687 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -114,7 +114,7 @@ public class GodotLib { /** * Dispatch mouse events */ - public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative); + public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY); public static native void magnify(float x, float y, float factor); @@ -220,6 +220,11 @@ public class GodotLib { public static native void requestPermissionResult(String p_permission, boolean p_result); /** + * Invoked on the theme light/dark mode change. + */ + public static native void onNightModeChanged(); + + /** * Invoked on the GL thread to configure the height of the virtual keyboard. */ public static native void setVirtualKeyboardHeight(int p_height); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 00243dab2a..5b2f9f57c7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -39,11 +39,21 @@ public interface GodotRenderView { void initInputDevices(); + /** + * Starts the thread that will drive Godot's rendering. + */ + void startRenderer(); + void queueOnRenderThread(Runnable event); void onActivityPaused(); + + void onActivityStopped(); + void onActivityResumed(); + void onActivityStarted(); + void onBackPressed(); GodotInputHandler getInputHandler(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt new file mode 100644 index 0000000000..795dc921c7 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt @@ -0,0 +1,56 @@ +package org.godotengine.godot + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.util.Log + +/** + * Godot service responsible for hosting the Godot engine instance. + * + * Note: Still in development, so it's made private and inaccessible until completed. + */ +private class GodotService : Service() { + + companion object { + private val TAG = GodotService::class.java.simpleName + } + + private var boundIntent: Intent? = null + private val godot by lazy { + Godot(applicationContext) + } + + override fun onCreate() { + super.onCreate() + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? { + if (boundIntent != null) { + Log.d(TAG, "GodotService already bound") + return null + } + + boundIntent = intent + return GodotHandle(godot) + } + + override fun onRebind(intent: Intent?) { + super.onRebind(intent) + } + + override fun onUnbind(intent: Intent?): Boolean { + return super.onUnbind(intent) + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + } + + class GodotHandle(val godot: Godot) : Binder() +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 681e182adb..a1ee9bd6b4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -35,7 +35,6 @@ import org.godotengine.godot.vulkan.VkRenderer; import org.godotengine.godot.vulkan.VkSurfaceView; import android.annotation.SuppressLint; -import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -52,14 +51,16 @@ import androidx.annotation.Keep; import java.io.InputStream; public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { + private final GodotHost host; private final Godot godot; private final GodotInputHandler mInputHandler; private final VkRenderer mRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotVulkanRenderView(Context context, Godot godot) { - super(context); + public GodotVulkanRenderView(GodotHost host, Godot godot) { + super(host.getActivity()); + this.host = host; this.godot = godot; mInputHandler = new GodotInputHandler(this); mRenderer = new VkRenderer(); @@ -67,6 +68,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); + } + + @Override + public void startRenderer() { startRenderer(mRenderer); } @@ -87,17 +92,35 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Override public void onActivityPaused() { - onPause(); + queueOnVkThread(() -> { + GodotLib.focusout(); + // Pause the renderer + mRenderer.onVkPause(); + }); + } + + @Override + public void onActivityStopped() { + pauseRenderThread(); + } + + @Override + public void onActivityStarted() { + resumeRenderThread(); } @Override public void onActivityResumed() { - onResume(); + queueOnVkThread(() -> { + // Resume the renderer + mRenderer.onVkResume(); + GodotLib.focusin(); + }); } @Override public void onBackPressed() { - godot.onBackPressed(); + godot.onBackPressed(host); } @Override @@ -206,26 +229,4 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV } return super.onResolvePointerIcon(me, pointerIndex); } - - @Override - public void onResume() { - super.onResume(); - - queueOnVkThread(() -> { - // Resume the renderer - mRenderer.onVkResume(); - GodotLib.focusin(); - }); - } - - @Override - public void onPause() { - super.onPause(); - - queueOnVkThread(() -> { - GodotLib.focusout(); - // Pause the renderer - mRenderer.onVkPause(); - }); - } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index 8449c08b88..ef97aaeab9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -122,8 +122,8 @@ import javax.microedition.khronos.opengles.GL10; * <p> * <h3>Activity Life-cycle</h3> * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients - * are required to call {@link #onPause()} when the activity stops and - * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * are required to call {@link #pauseGLThread()} when the activity stops and + * {@link #resumeGLThread()} when the activity starts. These calls allow GLSurfaceView to * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate * the OpenGL display. * <p> @@ -339,8 +339,8 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * setRenderer is called: * <ul> * <li>{@link #getRenderMode()} - * <li>{@link #onPause()} - * <li>{@link #onResume()} + * <li>{@link #pauseGLThread()} + * <li>{@link #resumeGLThread()} * <li>{@link #queueEvent(Runnable)} * <li>{@link #requestRender()} * <li>{@link #setRenderMode(int)} @@ -568,6 +568,7 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } + // -- GODOT start -- /** * Pause the rendering thread, optionally tearing down the EGL context * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. @@ -578,22 +579,23 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * * Must not be called before a renderer has been set. */ - public void onPause() { + protected final void pauseGLThread() { mGLThread.onPause(); } /** * Resumes the rendering thread, re-creating the OpenGL context if necessary. It - * is the counterpart to {@link #onPause()}. + * is the counterpart to {@link #pauseGLThread()}. * * This method should typically be called in * {@link android.app.Activity#onStart Activity.onStart}. * * Must not be called before a renderer has been set. */ - public void onResume() { + protected final void resumeGLThread() { mGLThread.onResume(); } + // -- GODOT end -- /** * Queue a runnable to be run on the GL rendering thread. This can be used @@ -867,7 +869,7 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback */ public interface EGLConfigChooser { /** - * Choose a configuration from the list. Implementors typically + * Choose a configuration from the list. Implementers typically * implement this method by calling * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index a7064dfc1d..dc8a0e54bb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -33,10 +33,14 @@ package org.godotengine.godot.input; import org.godotengine.godot.*; import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.text.InputFilter; import android.text.InputType; +import android.text.TextUtils; +import android.text.method.DigitsKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; @@ -44,6 +48,7 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import java.lang.ref.WeakReference; +import java.util.Locale; public class GodotEditText extends EditText { // =========================================================== @@ -136,6 +141,7 @@ public class GodotEditText extends EditText { } int inputType = InputType.TYPE_CLASS_TEXT; + String acceptCharacters = null; switch (edit.getKeyboardType()) { case KEYBOARD_TYPE_DEFAULT: inputType = InputType.TYPE_CLASS_TEXT; @@ -147,7 +153,8 @@ public class GodotEditText extends EditText { inputType = InputType.TYPE_CLASS_NUMBER; break; case KEYBOARD_TYPE_NUMBER_DECIMAL: - inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED; + inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL; + acceptCharacters = "0123456789,.- "; break; case KEYBOARD_TYPE_PHONE: inputType = InputType.TYPE_CLASS_PHONE; @@ -164,6 +171,14 @@ public class GodotEditText extends EditText { } edit.setInputType(inputType); + if (!TextUtils.isEmpty(acceptCharacters)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + edit.setKeyListener(DigitsKeyListener.getInstance(Locale.getDefault())); + } else { + edit.setKeyListener(DigitsKeyListener.getInstance(acceptCharacters)); + } + } + edit.mInputWrapper.setOriginText(text); edit.addTextChangedListener(edit.mInputWrapper); final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); @@ -209,6 +224,13 @@ public class GodotEditText extends EditText { mRenderView.getView().requestFocus(); } + // When a hardware keyboard is connected, all key events come through so we can route them + // directly to the engine. + // This is not the case when using a soft keyboard, requiring extra processing from this class. + if (hasHardwareKeyboard()) { + return mRenderView.getInputHandler().onKeyDown(keyCode, keyEvent); + } + // pass event to godot in special cases if (needHandlingInGodot(keyCode, keyEvent) && mRenderView.getInputHandler().onKeyDown(keyCode, keyEvent)) { return true; @@ -219,6 +241,13 @@ public class GodotEditText extends EditText { @Override public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + // When a hardware keyboard is connected, all key events come through so we can route them + // directly to the engine. + // This is not the case when using a soft keyboard, requiring extra processing from this class. + if (hasHardwareKeyboard()) { + return mRenderView.getInputHandler().onKeyUp(keyCode, keyEvent); + } + if (needHandlingInGodot(keyCode, keyEvent) && mRenderView.getInputHandler().onKeyUp(keyCode, keyEvent)) { return true; } else { @@ -235,10 +264,20 @@ public class GodotEditText extends EditText { isModifiedKey; } + boolean hasHardwareKeyboard() { + Configuration config = getResources().getConfiguration(); + return config.keyboard != Configuration.KEYBOARD_NOKEYS && + config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; + } + // =========================================================== // Methods // =========================================================== public void showKeyboard(String p_existing_text, VirtualKeyboardType p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) { + if (hasHardwareKeyboard()) { + return; + } + int maxInputLength = (p_max_input_length <= 0) ? Integer.MAX_VALUE : p_max_input_length; if (p_cursor_start == -1) { // cursor position not given this.mOriginText = p_existing_text; @@ -262,6 +301,10 @@ public class GodotEditText extends EditText { } public void hideKeyboard() { + if (hasHardwareKeyboard()) { + return; + } + final Message msg = new Message(); msg.what = HANDLER_CLOSE_IME_KEYBOARD; msg.obj = this; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index e26c9d39c2..89fbb9f580 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -210,21 +210,23 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } override fun onScroll( - originEvent: MotionEvent, + originEvent: MotionEvent?, terminusEvent: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { if (scaleInProgress) { if (dragInProgress) { - // Cancel the drag - GodotInputHandler.handleMotionEvent( - originEvent.source, - MotionEvent.ACTION_CANCEL, - originEvent.buttonState, - originEvent.x, - originEvent.y - ) + if (originEvent != null) { + // Cancel the drag + GodotInputHandler.handleMotionEvent( + originEvent.source, + MotionEvent.ACTION_CANCEL, + originEvent.buttonState, + originEvent.x, + originEvent.y + ) + } dragInProgress = false } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 185d03fe39..fe971cf442 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -57,6 +57,9 @@ import java.util.Set; public class GodotInputHandler implements InputManager.InputDeviceListener { private static final String TAG = GodotInputHandler.class.getSimpleName(); + private static final int ROTARY_INPUT_VERTICAL_AXIS = 1; + private static final int ROTARY_INPUT_HORIZONTAL_AXIS = 0; + private final SparseIntArray mJoystickIds = new SparseIntArray(4); private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4); @@ -71,6 +74,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { */ private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; + private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); mRenderView = godotView; @@ -95,13 +100,20 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { /** * Enable multi-fingers pan & scale gestures. This is false by default. - * + * <p> * Note: This may interfere with multi-touch handling / support. */ public void enablePanningAndScalingGestures(boolean enable) { this.godotGestureHandler.setPanningAndScalingEnabled(enable); } + /** + * On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default. + */ + public void setRotaryInputAxis(int axis) { + rotaryInputAxis = axis; + } + private boolean isKeyEventGameDevice(int source) { // Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD) if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD)) @@ -306,7 +318,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return; } - // Assign first available number. Re-use numbers where possible. + // Assign first available number. Reuse numbers where possible. final int id = assignJoystickIdNumber(deviceId); final Joystick joystick = new Joystick(); @@ -433,7 +445,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } private static boolean isMouseEvent(int eventSource) { - boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & (InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_STYLUS)) == InputDevice.SOURCE_STYLUS); + boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mouseSource = mouseSource || ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE); } @@ -470,13 +482,41 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final float y = event.getY(); final int buttonsMask = event.getButtonState(); - final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); - final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + final float pressure = event.getPressure(); + + // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. + final float orientation = event.getOrientation(); + + // Tilt is zero is perpendicular to the screen and pi/2 is flat on the surface. + final float tilt = event.getAxisValue(MotionEvent.AXIS_TILT); + + float tiltMult = (float)Math.sin(tilt); + + // To be consistent with expected tilt. + final float tiltX = (float)-Math.sin(orientation) * tiltMult; + final float tiltY = (float)Math.cos(orientation) * tiltMult; + + float verticalFactor = 0; + float horizontalFactor = 0; + + // If event came from RotaryEncoder (Bezel or Crown rotate event on Wear OS smart watches), + // convert it to mouse wheel event. + if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) { + if (rotaryInputAxis == ROTARY_INPUT_HORIZONTAL_AXIS) { + horizontalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL); + } else { + // If rotaryInputAxis is not ROTARY_INPUT_HORIZONTAL_AXIS then use default ROTARY_INPUT_VERTICAL_AXIS axis. + verticalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL); + } + } else { + verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } boolean sourceMouseRelative = false; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE); } - return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative); + return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative, pressure, tiltX, tiltY); } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y) { @@ -484,6 +524,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { + return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, 1, 0, 0); + } + + static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -511,7 +555,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative); + GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); return true; } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java index 48aa231c7a..c0912ca4dc 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java @@ -57,27 +57,28 @@ import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Base class for the Godot Android plugins. + * Base class for Godot Android plugins. * <p> - * A Godot Android plugin is a regular Android library packaged as an aar archive file with the following caveats: + * A Godot Android plugin is an Android library with the following requirements: * <p> - * - The library must have a dependency on the Godot Android library (godot-lib.aar). - * A stable version is available for each release. + * - The plugin must have a dependency on the Godot Android library: `implementation "org.godotengine:godot:<godotLibVersion>"` + * <p> + * - The plugin must include a <meta-data> tag in its Android manifest with the following format: + * <meta-data android:name="org.godotengine.plugin.v2.[PluginName]" android:value="[plugin.init.ClassFullName]" /> * <p> - * - The library must include a <meta-data> tag in its manifest file setup as follow: - * <meta-data android:name="org.godotengine.plugin.v1.[PluginName]" android:value="[plugin.init.ClassFullName]" /> * Where: + * <p> * - 'PluginName' is the name of the plugin. - * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class + * <p> + * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin init class * extending {@link GodotPlugin}. + * <p> + * A Godot Android plugin can also define and provide c/c++ gdextension libraries, which will be + * automatically bundled by the aar build system. + * GDExtension ('*.gdextension') config files must be located in the project 'assets' directory and + * their paths specified by {@link GodotPlugin#getPluginGDExtensionLibrariesPaths()}. * - * A plugin can also define and provide c/c++ gdextension libraries and nativescripts for the target - * app/game to leverage. - * The shared library for the gdextension library will be automatically bundled by the aar build - * system. - * Godot '*.gdextension' resource files must however be manually defined in the project - * 'assets' directory. The recommended path for these resources in the 'assets' directory should be: - * 'godot/plugin/v1/[PluginName]/' + * @see <a href="https://docs.godotengine.org/en/stable/tutorials/platform/android/index.html">Android plugins</a> */ public abstract class GodotPlugin { private static final String TAG = GodotPlugin.class.getSimpleName(); @@ -85,6 +86,10 @@ public abstract class GodotPlugin { private final Godot godot; private final ConcurrentHashMap<String, SignalInfo> registeredSignals = new ConcurrentHashMap<>(); + /** + * Base constructor passing a {@link Godot} instance through which the plugin can access Godot's + * APIs and lifecycle events. + */ public GodotPlugin(Godot godot) { this.godot = godot; } @@ -97,7 +102,7 @@ public abstract class GodotPlugin { } /** - * Provides access to the underlying {@link Activity}. + * Provides access to the hosting {@link Activity}. */ @Nullable protected Activity getActivity() { @@ -106,33 +111,16 @@ public abstract class GodotPlugin { /** * Register the plugin with Godot native code. - * - * This method is invoked on the render thread. + * <p> + * This method is invoked by the Godot Engine on the render thread. */ public final void onRegisterPluginWithGodotNative() { registeredSignals.putAll( - registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals(), - getPluginGDExtensionLibrariesPaths())); - } - - /** - * Register the plugin with Godot native code. - * - * This method must be invoked on the render thread. - */ - public static void registerPluginWithGodotNative(Object pluginObject, - GodotPluginInfoProvider pluginInfoProvider) { - registerPluginWithGodotNative(pluginObject, pluginInfoProvider.getPluginName(), - Collections.emptyList(), pluginInfoProvider.getPluginSignals(), - pluginInfoProvider.getPluginGDExtensionLibrariesPaths()); - - // Notify that registration is complete. - pluginInfoProvider.onPluginRegistered(); + registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals())); } private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject, - String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals, - Set<String> pluginGDExtensionLibrariesPaths) { + String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals) { nativeRegisterSingleton(pluginName, pluginObject); Set<Method> filteredMethods = new HashSet<>(); @@ -176,23 +164,18 @@ public abstract class GodotPlugin { registeredSignals.put(signalName, signalInfo); } - // Get the list of gdextension libraries to register. - if (!pluginGDExtensionLibrariesPaths.isEmpty()) { - nativeRegisterGDExtensionLibraries(pluginGDExtensionLibrariesPaths.toArray(new String[0])); - } - return registeredSignals; } /** - * Invoked once during the Godot Android initialization process after creation of the + * Invoked once during the initialization process after creation of the * {@link org.godotengine.godot.GodotRenderView} view. * <p> - * The plugin can return a non-null {@link View} layout in order to add it to the Godot view + * The plugin can return a non-null {@link View} layout which will be added to the Godot view * hierarchy. - * - * Use shouldBeOnTop() to set whether the plugin's {@link View} should be added on top or behind - * the main Godot view. + * <p> + * Use {@link GodotPlugin#shouldBeOnTop()} to specify whether the plugin's {@link View} should + * be added on top or behind the main Godot view. * * @see Activity#onCreate(Bundle) * @return the plugin's view to be included; null if no views should be included. @@ -235,44 +218,52 @@ public abstract class GodotPlugin { public boolean onMainBackPressed() { return false; } /** - * Invoked on the render thread when the Godot setup is complete. + * Invoked on the render thread when set up of the Godot engine is complete. + * <p> + * This is invoked before {@link GodotPlugin#onGodotMainLoopStarted()}. */ public void onGodotSetupCompleted() {} /** * Invoked on the render thread when the Godot main loop has started. + * + * This is invoked after {@link GodotPlugin#onGodotSetupCompleted()}. */ public void onGodotMainLoopStarted() {} /** - * Invoked once per frame on the GL thread after the frame is drawn. + * When using the OpenGL renderer, this is invoked once per frame on the GL thread after the + * frame is drawn. */ public void onGLDrawFrame(GL10 gl) {} /** - * Called on the GL thread after the surface is created and whenever the OpenGL ES surface size - * changes. + * When using the OpenGL renderer, this is called on the GL thread after the surface is created + * and whenever the OpenGL ES surface size changes. */ public void onGLSurfaceChanged(GL10 gl, int width, int height) {} /** - * Called on the GL thread when the surface is created or recreated. + * When using the OpenGL renderer, this is called on the GL thread when the surface is created + * or recreated. */ public void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} /** - * Invoked once per frame on the Vulkan thread after the frame is drawn. + * When using the Vulkan renderer, this is invoked once per frame on the Vulkan thread after + * the frame is drawn. */ public void onVkDrawFrame() {} /** - * Called on the Vulkan thread after the surface is created and whenever the surface size - * changes. + * When using the Vulkan renderer, this is called on the Vulkan thread after the surface is + * created and whenever the surface size changes. */ public void onVkSurfaceChanged(Surface surface, int width, int height) {} /** - * Called on the Vulkan thread when the surface is created or recreated. + * When using the Vulkan renderer, this is called on the Vulkan thread when the surface is + * created or recreated. */ public void onVkSurfaceCreated(Surface surface) {} @@ -287,7 +278,7 @@ public abstract class GodotPlugin { /** * Returns the list of methods to be exposed to Godot. * - * @deprecated Used the {@link UsedByGodot} annotation instead. + * @deprecated Use the {@link UsedByGodot} annotation instead. */ @NonNull @Deprecated @@ -304,19 +295,19 @@ public abstract class GodotPlugin { } /** - * Returns the paths for the plugin's gdextension libraries. - * - * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file. + * Returns the paths for the plugin's gdextension libraries (if any). + * <p> + * Each returned path must be relative to the 'assets' directory and point to a '*.gdextension' file. */ @NonNull - protected Set<String> getPluginGDExtensionLibrariesPaths() { + public Set<String> getPluginGDExtensionLibrariesPaths() { return Collections.emptySet(); } /** - * Returns whether the plugin's {@link View} returned in onMainCreate() should be placed on - * top of the main Godot view. - * + * Returns whether the plugin's {@link View} returned in + * {@link GodotPlugin#onMainCreate(Activity)} should be placed on top of the main Godot view. + * <p> * Returning false causes the plugin's {@link View} to be placed behind, which can be useful * when used with transparency in order to let the Godot view handle inputs. */ @@ -325,6 +316,15 @@ public abstract class GodotPlugin { } /** + * Returns whether the plugin supports the given feature tag. + * + * @see <a href="https://docs.godotengine.org/en/stable/tutorials/export/feature_tags.html">Feature tags</a> + */ + public boolean supportsFeature(String featureTag) { + return false; + } + + /** * Runs the specified action on the UI thread. If the current thread is the UI * thread, then the action is executed immediately. If the current thread is * not the UI thread, the action is posted to the event queue of the UI thread. @@ -359,7 +359,7 @@ public abstract class GodotPlugin { } emitSignal(getGodot(), getPluginName(), signalInfo, signalArgs); } catch (IllegalArgumentException exception) { - Log.w(TAG, exception.getMessage()); + Log.w(TAG, exception); if (BuildConfig.DEBUG) { throw exception; } @@ -368,7 +368,7 @@ public abstract class GodotPlugin { /** * Emit a Godot signal. - * @param godot + * @param godot Godot instance * @param pluginName Name of the Godot plugin the signal will be emitted from. The plugin must already be registered with the Godot engine. * @param signalInfo Information about the signal to emit. * @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the given {@link SignalInfo} parameter. @@ -397,7 +397,7 @@ public abstract class GodotPlugin { godot.runOnRenderThread(() -> nativeEmitSignal(pluginName, signalInfo.getName(), signalArgs)); } catch (IllegalArgumentException exception) { - Log.w(TAG, exception.getMessage()); + Log.w(TAG, exception); if (BuildConfig.DEBUG) { throw exception; } @@ -420,13 +420,7 @@ public abstract class GodotPlugin { private static native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params); /** - * Used to register gdextension libraries bundled by the plugin. - * @param gdextensionPaths Paths to the libraries relative to the 'assets' directory. - */ - private static native void nativeRegisterGDExtensionLibraries(String[] gdextensionPaths); - - /** - * Used to complete registration of the {@link GodotPlugin} instance's methods. + * Used to complete registration of the {@link GodotPlugin} instance's signals. * @param pluginName Name of the plugin * @param signalName Name of the signal to register * @param signalParamTypes Signal parameters types diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java index c2428de2e1..711bca02e7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java @@ -42,8 +42,8 @@ import android.util.Log; import androidx.annotation.Nullable; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.Collection; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -52,14 +52,20 @@ import java.util.concurrent.ConcurrentHashMap; public final class GodotPluginRegistry { private static final String TAG = GodotPluginRegistry.class.getSimpleName(); + /** + * Prefix used for version 1 of the Godot plugin, mostly compatible with Godot 3.x + */ private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1."; + /** + * Prefix used for version 2 of the Godot plugin, compatible with Godot 4.2+ + */ + private static final String GODOT_PLUGIN_V2_NAME_PREFIX = "org.godotengine.plugin.v2."; private static GodotPluginRegistry instance; private final ConcurrentHashMap<String, GodotPlugin> registry; - private GodotPluginRegistry(Godot godot) { + private GodotPluginRegistry() { registry = new ConcurrentHashMap<>(); - loadPlugins(godot); } /** @@ -86,12 +92,14 @@ public final class GodotPluginRegistry { * documentation. * * @param godot Godot instance + * @param runtimePlugins Set of plugins provided at runtime for registration * @return A singleton instance of {@link GodotPluginRegistry}. This ensures that only one instance * of each Godot Android plugins is available at runtime. */ - public static GodotPluginRegistry initializePluginRegistry(Godot godot) { + public static GodotPluginRegistry initializePluginRegistry(Godot godot, Set<GodotPlugin> runtimePlugins) { if (instance == null) { - instance = new GodotPluginRegistry(godot); + instance = new GodotPluginRegistry(); + instance.loadPlugins(godot, runtimePlugins); } return instance; @@ -101,7 +109,7 @@ public final class GodotPluginRegistry { * Return the plugin registry if it's initialized. * Throws a {@link IllegalStateException} exception if not. * - * @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot)} has not been called prior to calling this method. + * @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot, Set)} has not been called prior to calling this method. */ public static GodotPluginRegistry getPluginRegistry() throws IllegalStateException { if (instance == null) { @@ -111,7 +119,16 @@ public final class GodotPluginRegistry { return instance; } - private void loadPlugins(Godot godot) { + private void loadPlugins(Godot godot, Set<GodotPlugin> runtimePlugins) { + // Register the runtime plugins + if (runtimePlugins != null && !runtimePlugins.isEmpty()) { + for (GodotPlugin plugin : runtimePlugins) { + Log.i(TAG, "Registering runtime plugin " + plugin.getPluginName()); + registry.put(plugin.getPluginName(), plugin); + } + } + + // Register the manifest plugins try { final Activity activity = godot.getActivity(); ApplicationInfo appInfo = activity @@ -123,11 +140,17 @@ public final class GodotPluginRegistry { return; } - int godotPluginV1NamePrefixLength = GODOT_PLUGIN_V1_NAME_PREFIX.length(); for (String metaDataName : metaData.keySet()) { // Parse the meta-data looking for entry with the Godot plugin name prefix. - if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) { - String pluginName = metaDataName.substring(godotPluginV1NamePrefixLength).trim(); + String pluginName = null; + if (metaDataName.startsWith(GODOT_PLUGIN_V2_NAME_PREFIX)) { + pluginName = metaDataName.substring(GODOT_PLUGIN_V2_NAME_PREFIX.length()).trim(); + } else if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) { + pluginName = metaDataName.substring(GODOT_PLUGIN_V1_NAME_PREFIX.length()).trim(); + Log.w(TAG, "Godot v1 plugin are deprecated in Godot 4.2 and higher: " + pluginName); + } + + if (!TextUtils.isEmpty(pluginName)) { Log.i(TAG, "Initializing Godot plugin " + pluginName); // Retrieve the plugin class full name. @@ -149,15 +172,7 @@ public final class GodotPluginRegistry { } registry.put(pluginName, pluginHandle); Log.i(TAG, "Completed initialization for Godot plugin " + pluginHandle.getPluginName()); - } catch (ClassNotFoundException e) { - Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); - } catch (IllegalAccessException e) { - Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); - } catch (InstantiationException e) { - Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); - } catch (NoSuchMethodException e) { - Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); - } catch (InvocationTargetException e) { + } catch (Exception e) { Log.w(TAG, "Unable to load Godot plugin " + pluginName, e); } } else { @@ -165,7 +180,7 @@ public final class GodotPluginRegistry { } } } - } catch (PackageManager.NameNotFoundException e) { + } catch (Exception e) { Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java index edace53e7f..dce6753b7a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -33,6 +33,7 @@ package org.godotengine.godot.tts; import org.godotengine.godot.GodotLib; import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; @@ -62,7 +63,7 @@ public class GodotTTS extends UtteranceProgressListener { final private static int EVENT_CANCEL = 2; final private static int EVENT_BOUNDARY = 3; - final private Activity activity; + private final Context context; private TextToSpeech synth; private LinkedList<GodotUtterance> queue; final private Object lock = new Object(); @@ -71,8 +72,8 @@ public class GodotTTS extends UtteranceProgressListener { private boolean speaking; private boolean paused; - public GodotTTS(Activity p_activity) { - activity = p_activity; + public GodotTTS(Context context) { + this.context = context; } private void updateTTS() { @@ -188,7 +189,7 @@ public class GodotTTS extends UtteranceProgressListener { * Initialize synth and query. */ public void init() { - synth = new TextToSpeech(activity, null); + synth = new TextToSpeech(context, null); queue = new LinkedList<GodotUtterance>(); synth.setOnUtteranceProgressListener(this); diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt index 1552c8f082..69748c0a8d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -41,7 +41,7 @@ import org.godotengine.godot.io.file.FileAccessFlags import org.godotengine.godot.io.file.FileAccessHandler import org.json.JSONObject import java.nio.ByteBuffer -import java.util.concurrent.ConcurrentSkipListMap +import java.util.Collections /** * Contains benchmark related utilities methods @@ -51,44 +51,47 @@ private const val TAG = "GodotBenchmark" var useBenchmark = false var benchmarkFile = "" -private val startBenchmarkFrom = ConcurrentSkipListMap<String, Long>() -private val benchmarkTracker = ConcurrentSkipListMap<String, Double>() +private val startBenchmarkFrom = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Long>()) +private val benchmarkTracker = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Double>()) /** - * Start measuring and tracing the execution of a given section of code using the given label. + * Start measuring and tracing the execution of a given section of code using the given label + * within the given scope. * * Must be followed by a call to [endBenchmarkMeasure]. * * Note: Only enabled on 'editorDev' build variant. */ -fun beginBenchmarkMeasure(label: String) { +fun beginBenchmarkMeasure(scope: String, label: String) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } - startBenchmarkFrom[label] = SystemClock.elapsedRealtime() + val key = Pair(scope, label) + startBenchmarkFrom[key] = SystemClock.elapsedRealtime() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Trace.beginAsyncSection(label, 0) + Trace.beginAsyncSection("[$scope] $label", 0) } } /** - * End measuring and tracing of the section of code with the given label. + * End measuring and tracing of the section of code with the given label within the given scope. * * Must be preceded by a call [beginBenchmarkMeasure] * * * Note: Only enabled on 'editorDev' build variant. */ -fun endBenchmarkMeasure(label: String) { +fun endBenchmarkMeasure(scope: String, label: String) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } - val startTime = startBenchmarkFrom[label] ?: return + val key = Pair(scope, label) + val startTime = startBenchmarkFrom[key] ?: return val total = SystemClock.elapsedRealtime() - startTime - benchmarkTracker[label] = total / 1000.0 + benchmarkTracker[key] = total / 1_000.0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Trace.endAsyncSection(label, 0) + Trace.endAsyncSection("[$scope] $label", 0) } } @@ -107,8 +110,16 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = ben return } + val results = LinkedHashMap<String, String>() + for (entry in benchmarkTracker) { + if (!results.containsKey(entry.key.first)) { + results[entry.key.first] = "" + } + results[entry.key.first] += "\t\t- ${entry.key.second}: ${entry.value * 1_000.0} msec.\n" + } + val printOut = - benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n") + results.map { "\t- [${it.key}]\n ${it.value}" }.joinToString("\n") Log.i(TAG, "BENCHMARK:\n$printOut") if (fileAccessHandler != null && !filepath.isNullOrBlank()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java index 7db02968bb..2c7b73ae4d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java @@ -44,8 +44,6 @@ public class GLUtils { public static final boolean DEBUG = false; - public static boolean use_debug_opengl = false; - private static final String[] ATTRIBUTES_NAMES = new String[] { "EGL_BUFFER_SIZE", "EGL_ALPHA_SIZE", diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java index c31d56a3e1..dca190a2fc 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java @@ -36,7 +36,8 @@ import android.net.wifi.WifiManager; import android.util.Base64; import android.util.Log; -import java.io.StringWriter; +import androidx.annotation.NonNull; + import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.Enumeration; @@ -50,9 +51,9 @@ public class GodotNetUtils { /* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */ private WifiManager.MulticastLock multicastLock; - public GodotNetUtils(Activity p_activity) { - if (PermissionsUtil.hasManifestPermission(p_activity, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) { - WifiManager wifi = (WifiManager)p_activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + public GodotNetUtils(Context context) { + if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) { + WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); multicastLock = wifi.createMulticastLock("GodotMulticastLock"); multicastLock.setReferenceCounted(true); } @@ -91,7 +92,7 @@ public class GodotNetUtils { * @see https://developer.android.com/reference/java/security/KeyStore . * @return A string of concatenated X509 certificates in PEM format. */ - public static String getCACertificates() { + public static @NonNull String getCACertificates() { try { KeyStore ks = KeyStore.getInstance("AndroidCAStore"); StringBuilder writer = new StringBuilder(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index a94188c405..737b4ac20b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -32,6 +32,7 @@ package org.godotengine.godot.utils; import android.Manifest; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -52,13 +53,12 @@ import java.util.Set; /** * This class includes utility functions for Android permissions related operations. */ - public final class PermissionsUtil { private static final String TAG = PermissionsUtil.class.getSimpleName(); - static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; - static final int REQUEST_CAMERA_PERMISSION = 2; - static final int REQUEST_VIBRATE_PERMISSION = 3; + public static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; + public static final int REQUEST_CAMERA_PERMISSION = 2; + public static final int REQUEST_VIBRATE_PERMISSION = 3; public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002; public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; @@ -70,7 +70,7 @@ public final class PermissionsUtil { * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a> * @param permissionName the name of the requested permission. * @param activity the caller activity for this method. - * @return true/false. "true" if permission was granted otherwise returns "false". + * @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched. */ public static boolean requestPermission(String permissionName, Activity activity) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { @@ -124,7 +124,7 @@ public final class PermissionsUtil { /** * Request dangerous permissions which are defined in the Android manifest file from the user. * @param activity the caller activity for this method. - * @return true/false. "true" if all permissions were granted otherwise returns "false". + * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched. */ public static boolean requestManifestPermissions(Activity activity) { return requestManifestPermissions(activity, null); @@ -134,7 +134,7 @@ public final class PermissionsUtil { * Request dangerous permissions which are defined in the Android manifest file from the user. * @param activity the caller activity for this method. * @param excludes Set of permissions to exclude from the request - * @return true/false. "true" if all permissions were granted otherwise returns "false". + * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched. */ public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { @@ -160,6 +160,7 @@ public final class PermissionsUtil { try { if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + Log.d(TAG, "Requesting permission " + manifestPermission); try { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); @@ -173,6 +174,7 @@ public final class PermissionsUtil { PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Requesting permission " + manifestPermission); requestedPermissions.add(manifestPermission); } } @@ -193,13 +195,13 @@ public final class PermissionsUtil { /** * With this function you can get the list of dangerous permissions that have been granted to the Android application. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @return granted permissions list */ - public static String[] getGrantedPermissions(Activity activity) { + public static String[] getGrantedPermissions(Context context) { String[] manifestPermissions; try { - manifestPermissions = getManifestPermissions(activity); + manifestPermissions = getManifestPermissions(context); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return new String[0]; @@ -215,9 +217,9 @@ public final class PermissionsUtil { grantedPermissions.add(manifestPermission); } } else { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission); int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) { grantedPermissions.add(manifestPermission); } } @@ -232,13 +234,13 @@ public final class PermissionsUtil { /** * Check if the given permission is in the AndroidManifest.xml file. - * @param activity the caller activity for this method. - * @param permission the permession to look for in the manifest file. + * @param context the caller context for this method. + * @param permission the permission to look for in the manifest file. * @return "true" if the permission is in the manifest file of the activity, "false" otherwise. */ - public static boolean hasManifestPermission(Activity activity, String permission) { + public static boolean hasManifestPermission(Context context, String permission) { try { - for (String p : getManifestPermissions(activity)) { + for (String p : getManifestPermissions(context)) { if (permission.equals(p)) return true; } @@ -250,13 +252,13 @@ public final class PermissionsUtil { /** * Returns the permissions defined in the AndroidManifest.xml file. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @return manifest permissions list * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - private static String[] getManifestPermissions(Activity activity) throws PackageManager.NameNotFoundException { - PackageManager packageManager = activity.getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS); + private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); if (packageInfo.requestedPermissions == null) return new String[0]; return packageInfo.requestedPermissions; @@ -264,13 +266,13 @@ public final class PermissionsUtil { /** * Returns the information of the desired permission. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @param permission the name of the permission. * @return permission info object * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - private static PermissionInfo getPermissionInfo(Activity activity, String permission) throws PackageManager.NameNotFoundException { - PackageManager packageManager = activity.getPackageManager(); + private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException { + PackageManager packageManager = context.getPackageManager(); return packageManager.getPermissionInfo(permission, 0); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt index 3828004198..791b425444 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt @@ -99,7 +99,7 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf * * Must not be called before a [VkRenderer] has been set. */ - open fun onResume() { + protected fun resumeRenderThread() { vkThread.onResume() } @@ -108,7 +108,7 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf * * Must not be called before a [VkRenderer] has been set. */ - open fun onPause() { + protected fun pauseRenderThread() { vkThread.onPause() } diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index 1a126ff765..01ee41e30b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -51,12 +51,22 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory { private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private final boolean mUseDebugOpengl; + + public RegularContextFactory() { + this(false); + } + + public RegularContextFactory(boolean useDebugOpengl) { + this.mUseDebugOpengl = useDebugOpengl; + } + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { Log.w(TAG, "creating OpenGL ES 3.0 context :"); GLUtils.checkEglError(TAG, "Before eglCreateContext", egl); EGLContext context; - if (GLUtils.use_debug_opengl) { + if (mUseDebugOpengl) { int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); } else { diff --git a/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml index dc180375d5..8072ee00db 100644 --- a/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml +++ b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="utf-8"?> -<manifest package="org.godotengine.godot" /> +<manifest /> diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle index 5e810ae1ba..a728241181 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -9,6 +9,8 @@ android { buildToolsVersion versions.buildTools ndkVersion versions.ndkVersion + namespace = "org.godotengine.godot" + defaultConfig { minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 466ffebf22..3137e74244 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -5,12 +5,15 @@ pluginManagement { plugins { id 'com.android.application' version versions.androidGradlePlugin id 'com.android.library' version versions.androidGradlePlugin + id 'com.android.asset-pack' version versions.androidGradlePlugin id 'org.jetbrains.kotlin.android' version versions.kotlinVersion id 'io.github.gradle-nexus.publish-plugin' version versions.nexusPublishVersion } repositories { - gradlePluginPortal() google() + mavenCentral() + gradlePluginPortal() + maven { url "https://plugins.gradle.org/m2/" } } } diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index edc934e927..d6455cbf1c 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -50,14 +50,14 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, } int pc = E.param_types.size(); - if (pc > p_argcount) { + if (p_argcount < pc) { r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; - r_error.argument = pc; + r_error.expected = pc; continue; } - if (pc < p_argcount) { + if (p_argcount > pc) { r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; - r_error.argument = pc; + r_error.expected = pc; continue; } uint32_t *ptypes = E.param_types.ptrw(); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index b54491e0e1..85d5cf2796 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -135,7 +135,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); - return godot_java->on_video_init(env); + return true; } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) { @@ -274,12 +274,12 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, } // Called on the UI thread -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative, jfloat p_pressure, jfloat p_tilt_x, jfloat p_tilt_y) { if (step.get() <= 0) { return; } - input_handler->process_mouse_event(p_event_type, p_button_mask, Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y), p_double_click, p_source_mouse_relative); + input_handler->process_mouse_event(p_event_type, p_button_mask, Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y), p_double_click, p_source_mouse_relative, p_pressure, Vector2(p_tilt_x, p_tilt_y)); } // Called on the UI thread @@ -484,7 +484,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * env->DeleteLocalRef(jobj); } - MessageQueue::get_singleton()->push_callp(obj, str_method, argptrs, count); + Callable(obj, str_method).call_deferredp(argptrs, count); +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) { + DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton(); + if (ds) { + ds->emit_system_theme_changed(); + } } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index ee6a19034c..f32ffc291a 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -45,7 +45,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative, jfloat p_pressure, jfloat p_tilt_x, jfloat p_tilt_y); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jboolean p_double_tap); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y); @@ -66,6 +66,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); } diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index e5b04e4866..5f554aa2d6 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -38,7 +38,7 @@ #include <android/log.h> #include <jni.h> -// Class that makes functions in java/src/org/godotengine/godot/GodotView.java callable from C++ +// Class that makes functions in java/src/org/godotengine/godot/GodotRenderView.java callable from C++ class GodotJavaViewWrapper { private: jclass _cls; diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 862d9f0436..3c950bb1b1 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -58,12 +58,12 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ } // get some Godot method pointers... - _on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()Z"); _restart = p_env->GetMethodID(godot_class, "restart", "()V"); _finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z"); _set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V"); _alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V"); - _get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I"); + _is_dark_mode_supported = p_env->GetMethodID(godot_class, "isDarkModeSupported", "()Z"); + _is_dark_mode = p_env->GetMethodID(godot_class, "isDarkMode", "()Z"); _get_clipboard = p_env->GetMethodID(godot_class, "getClipboard", "()Ljava/lang/String;"); _set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V"); _has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z"); @@ -72,20 +72,17 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); - _get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;"); - _is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z"); _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V"); _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); - _begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V"); - _end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V"); - _dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V"); - - // get some Activity method pointers... - _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); + _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V"); + _end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V"); + _dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V"); + _get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;"); + _has_feature = p_env->GetMethodID(godot_class, "hasFeature", "(Ljava/lang/String;)Z"); } GodotJavaWrapper::~GodotJavaWrapper() { @@ -105,29 +102,6 @@ jobject GodotJavaWrapper::get_activity() { return activity; } -jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env) { - if (godot_class) { - if (p_env == nullptr) { - p_env = get_jni_env(); - } - ERR_FAIL_NULL_V(p_env, nullptr); - jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class); - return p_env->GetStaticObjectField(godot_class, fid); - } else { - return nullptr; - } -} - -jobject GodotJavaWrapper::get_class_loader() { - if (_get_class_loader) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, nullptr); - return env->CallObjectMethod(activity, _get_class_loader); - } else { - return nullptr; - } -} - GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { if (godot_view != nullptr) { return godot_view; @@ -143,17 +117,6 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { return godot_view; } -bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) { - if (_on_video_init) { - if (p_env == nullptr) { - p_env = get_jni_env(); - } - ERR_FAIL_NULL_V(p_env, false); - return p_env->CallBooleanMethod(godot_instance, _on_video_init); - } - return false; -} - void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) { if (_on_godot_setup_completed) { if (p_env == nullptr) { @@ -212,13 +175,24 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { } } -int GodotJavaWrapper::get_gles_version_code() { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, 0); - if (_get_GLES_version_code) { - return env->CallIntMethod(godot_instance, _get_GLES_version_code); +bool GodotJavaWrapper::is_dark_mode_supported() { + if (_is_dark_mode_supported) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + return env->CallBooleanMethod(godot_instance, _is_dark_mode_supported); + } else { + return false; + } +} + +bool GodotJavaWrapper::is_dark_mode() { + if (_is_dark_mode) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + return env->CallBooleanMethod(godot_instance, _is_dark_mode); + } else { + return false; } - return 0; } bool GodotJavaWrapper::has_get_clipboard() { @@ -314,6 +288,25 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { return permissions_list; } +Vector<String> GodotJavaWrapper::get_gdextension_list_config_file() const { + Vector<String> config_file_list; + if (_get_gdextension_list_config_file) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, config_file_list); + jobject config_file_list_object = env->CallObjectMethod(godot_instance, _get_gdextension_list_config_file); + jobjectArray *arr = reinterpret_cast<jobjectArray *>(&config_file_list_object); + + jsize len = env->GetArrayLength(*arr); + for (int i = 0; i < len; i++) { + jstring j_config_file = (jstring)env->GetObjectArrayElement(*arr, i); + String config_file = jstring_to_string(j_config_file, env); + config_file_list.push_back(config_file); + env->DeleteLocalRef(j_config_file); + } + } + return config_file_list; +} + String GodotJavaWrapper::get_ca_certificates() const { if (_get_ca_certificates) { JNIEnv *env = get_jni_env(); @@ -333,26 +326,6 @@ void GodotJavaWrapper::init_input_devices() { } } -jobject GodotJavaWrapper::get_surface() { - if (_get_surface) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, nullptr); - return env->CallObjectMethod(godot_instance, _get_surface); - } else { - return nullptr; - } -} - -bool GodotJavaWrapper::is_activity_resumed() { - if (_is_activity_resumed) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, false); - return env->CallBooleanMethod(godot_instance, _is_activity_resumed); - } else { - return false; - } -} - void GodotJavaWrapper::vibrate(int p_duration_ms) { if (_vibrate) { JNIEnv *env = get_jni_env(); @@ -361,7 +334,7 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) { } } -int GodotJavaWrapper::create_new_godot_instance(List<String> args) { +int GodotJavaWrapper::create_new_godot_instance(const List<String> &args) { if (_create_new_godot_instance) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, 0); @@ -375,21 +348,23 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> args) { } } -void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) { +void GodotJavaWrapper::begin_benchmark_measure(const String &p_context, const String &p_label) { if (_begin_benchmark_measure) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); + jstring j_context = env->NewStringUTF(p_context.utf8().get_data()); jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); - env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label); + env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_context, j_label); } } -void GodotJavaWrapper::end_benchmark_measure(const String &p_label) { +void GodotJavaWrapper::end_benchmark_measure(const String &p_context, const String &p_label) { if (_end_benchmark_measure) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); + jstring j_context = env->NewStringUTF(p_context.utf8().get_data()); jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); - env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label); + env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_context, j_label); } } @@ -401,3 +376,15 @@ void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) { env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file); } } + +bool GodotJavaWrapper::has_feature(const String &p_feature) const { + if (_has_feature) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + + jstring j_feature = env->NewStringUTF(p_feature.utf8().get_data()); + return env->CallBooleanMethod(godot_instance, _has_feature, j_feature); + } else { + return false; + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 1efdffd71b..93998021a9 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -39,7 +39,7 @@ #include <android/log.h> #include <jni.h> -// Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++ +// Class that makes functions in java/src/org/godotengine/godot/Godot.kt callable from C++ class GodotJavaWrapper { private: jobject godot_instance; @@ -49,51 +49,48 @@ private: GodotJavaViewWrapper *godot_view = nullptr; - jmethodID _on_video_init = nullptr; jmethodID _restart = nullptr; jmethodID _finish = nullptr; jmethodID _set_keep_screen_on = nullptr; jmethodID _alert = nullptr; - jmethodID _get_GLES_version_code = nullptr; + jmethodID _is_dark_mode_supported = nullptr; + jmethodID _is_dark_mode = nullptr; jmethodID _get_clipboard = nullptr; jmethodID _set_clipboard = nullptr; jmethodID _has_clipboard = nullptr; jmethodID _request_permission = nullptr; jmethodID _request_permissions = nullptr; jmethodID _get_granted_permissions = nullptr; + jmethodID _get_gdextension_list_config_file = nullptr; jmethodID _get_ca_certificates = nullptr; jmethodID _init_input_devices = nullptr; - jmethodID _get_surface = nullptr; - jmethodID _is_activity_resumed = nullptr; jmethodID _vibrate = nullptr; jmethodID _get_input_fallback_mapping = nullptr; jmethodID _on_godot_setup_completed = nullptr; jmethodID _on_godot_main_loop_started = nullptr; - jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; jmethodID _get_render_view = nullptr; jmethodID _begin_benchmark_measure = nullptr; jmethodID _end_benchmark_measure = nullptr; jmethodID _dump_benchmark = nullptr; + jmethodID _has_feature = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); ~GodotJavaWrapper(); jobject get_activity(); - jobject get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env = nullptr); - jobject get_class_loader(); GodotJavaViewWrapper *get_godot_view(); - bool on_video_init(JNIEnv *p_env = nullptr); void on_godot_setup_completed(JNIEnv *p_env = nullptr); void on_godot_main_loop_started(JNIEnv *p_env = nullptr); void restart(JNIEnv *p_env = nullptr); bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0); void set_keep_screen_on(bool p_enabled); void alert(const String &p_message, const String &p_title); - int get_gles_version_code(); + bool is_dark_mode_supported(); + bool is_dark_mode(); bool has_get_clipboard(); String get_clipboard(); bool has_set_clipboard(); @@ -105,14 +102,18 @@ public: Vector<String> get_granted_permissions() const; String get_ca_certificates() const; void init_input_devices(); - jobject get_surface(); - bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); - int create_new_godot_instance(List<String> args); - void begin_benchmark_measure(const String &p_label); - void end_benchmark_measure(const String &p_label); + int create_new_godot_instance(const List<String> &args); + void begin_benchmark_measure(const String &p_context, const String &p_label); + void end_benchmark_measure(const String &p_context, const String &p_label); void dump_benchmark(const String &benchmark_file); + + // Return the list of gdextensions config file. + Vector<String> get_gdextension_list_config_file() const; + + // Return true if the given feature is supported. + bool has_feature(const String &p_feature) const; }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp index 1299ad032a..a2befdc9be 100644 --- a/platform/android/net_socket_android.cpp +++ b/platform/android/net_socket_android.cpp @@ -106,7 +106,7 @@ Error NetSocketAndroid::set_broadcasting_enabled(bool p_enabled) { return OK; } -Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, String p_if_name) { +Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) { Error err = NetSocketPosix::join_multicast_group(p_multi_address, p_if_name); if (err != OK) { return err; @@ -120,7 +120,7 @@ Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, S return OK; } -Error NetSocketAndroid::leave_multicast_group(const IPAddress &p_multi_address, String p_if_name) { +Error NetSocketAndroid::leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) { Error err = NetSocketPosix::leave_multicast_group(p_multi_address, p_if_name); if (err != OK) { return err; diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h index f0c2b70529..e5f46d3236 100644 --- a/platform/android/net_socket_android.h +++ b/platform/android/net_socket_android.h @@ -67,8 +67,8 @@ public: virtual void close(); virtual Error set_broadcasting_enabled(bool p_enabled); - virtual Error join_multicast_group(const IPAddress &p_multi_address, String p_if_name); - virtual Error leave_multicast_group(const IPAddress &p_multi_address, String p_if_name); + virtual Error join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name); + virtual Error leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name); NetSocketAndroid() {} ~NetSocketAndroid(); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index c040d8c4c6..82e7fdb320 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -39,6 +39,8 @@ #include "net_socket_android.h" #include "core/config/project_settings.h" +#include "core/extension/gdextension_manager.h" +#include "core/io/xml_parser.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" @@ -58,10 +60,10 @@ String _remove_symlink(const String &dir) { // Change directory to the external data directory. chdir(dir.utf8().get_data()); // Get the actual directory without the potential symlink. - char dir_name_wihout_symlink[2048]; - getcwd(dir_name_wihout_symlink, 2048); + char dir_name_without_symlink[2048]; + getcwd(dir_name_without_symlink, 2048); // Convert back to a String. - String dir_without_symlink(dir_name_wihout_symlink); + String dir_without_symlink(dir_name_without_symlink); // Restore original current directory. chdir(current_dir_name); return dir_without_symlink; @@ -160,13 +162,41 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = p_path; + bool so_file_exists = true; if (!FileAccess::exists(path)) { path = p_path.get_file(); + so_file_exists = false; } p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); + if (!p_library_handle && so_file_exists) { + // The library may be on the sdcard and thus inaccessible. Try to copy it to the internal + // directory. + uint64_t so_modified_time = FileAccess::get_modified_time(p_path); + String dynamic_library_path = get_dynamic_libraries_path().path_join(String::num_uint64(so_modified_time)); + String internal_path = dynamic_library_path.path_join(p_path.get_file()); + + bool internal_so_file_exists = FileAccess::exists(internal_path); + if (!internal_so_file_exists) { + Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path); + if (da_ref.is_valid()) { + Error create_dir_result = da_ref->make_dir_recursive(dynamic_library_path); + if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) { + internal_so_file_exists = da_ref->copy(path, internal_path) == OK; + } + } + } + + if (internal_so_file_exists) { + p_library_handle = dlopen(internal_path.utf8().get_data(), RTLD_NOW); + if (p_library_handle) { + path = internal_path; + } + } + } + ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); if (r_resolved_path != nullptr) { @@ -294,15 +324,21 @@ void OS_Android::main_loop_end() { void OS_Android::main_loop_focusout() { DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT); + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); + } audio_driver_android.set_pause(true); } void OS_Android::main_loop_focusin() { DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_IN); + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN); + } audio_driver_android.set_pause(false); } -Error OS_Android::shell_open(String p_uri) { +Error OS_Android::shell_open(const String &p_uri) { return godot_io_java->open_uri(p_uri); } @@ -584,6 +620,10 @@ String OS_Android::get_user_data_dir() const { return "."; } +String OS_Android::get_dynamic_libraries_path() const { + return get_cache_path().path_join("dynamic_libraries"); +} + String OS_Android::get_cache_path() const { if (!cache_dir_cache.is_empty()) { return cache_dir_cache; @@ -674,15 +714,15 @@ String OS_Android::get_config_path() const { return get_user_data_dir().path_join("config"); } -void OS_Android::benchmark_begin_measure(const String &p_what) { +void OS_Android::benchmark_begin_measure(const String &p_context, const String &p_what) { #ifdef TOOLS_ENABLED - godot_java->begin_benchmark_measure(p_what); + godot_java->begin_benchmark_measure(p_context, p_what); #endif } -void OS_Android::benchmark_end_measure(const String &p_what) { +void OS_Android::benchmark_end_measure(const String &p_context, const String &p_what) { #ifdef TOOLS_ENABLED - godot_java->end_benchmark_measure(p_what); + godot_java->end_benchmark_measure(p_context, p_what); #endif } @@ -715,6 +755,11 @@ bool OS_Android::_check_internal_feature_support(const String &p_feature) { return true; } #endif + + if (godot_java->has_feature(p_feature)) { + return true; + } + return false; } @@ -791,5 +836,13 @@ Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_por return err; } +void OS_Android::load_platform_gdextensions() const { + Vector<String> extension_list_config_file = godot_java->get_gdextension_list_config_file(); + for (String config_file_path : extension_list_config_file) { + GDExtensionManager::LoadStatus err = GDExtensionManager::get_singleton()->load_extension(config_file_path); + ERR_CONTINUE_MSG(err == GDExtensionManager::LOAD_STATUS_FAILED, "Error loading platform extension: " + config_file_path); + } +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index abcc412588..31ee7389df 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -113,7 +113,7 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual String get_name() const override; virtual String get_distribution_name() const override; @@ -134,7 +134,7 @@ public: void set_native_window(ANativeWindow *p_native_window); ANativeWindow *get_native_window() const; - virtual Error shell_open(String p_uri) override; + virtual Error shell_open(const String &p_uri) override; virtual Vector<String> get_system_fonts() const override; virtual String get_system_font_path(const String &p_font_name, int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override; @@ -165,13 +165,19 @@ public: virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override; - virtual void benchmark_begin_measure(const String &p_what) override; - virtual void benchmark_end_measure(const String &p_what) override; + virtual void benchmark_begin_measure(const String &p_context, const String &p_what) override; + virtual void benchmark_end_measure(const String &p_context, const String &p_what) override; virtual void benchmark_dump() override; + virtual void load_platform_gdextensions() const override; + virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); ~OS_Android(); + +private: + // Location where we relocate external dynamic libraries to make them accessible. + String get_dynamic_libraries_path() const; }; #endif // OS_ANDROID_H diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java b/platform/android/platform_gl.h index 63999a8321..af6edb103d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java +++ b/platform/android/platform_gl.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* GodotPluginInfoProvider.java */ +/* platform_gl.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,45 +28,16 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -package org.godotengine.godot.plugin; +#ifndef PLATFORM_GL_H +#define PLATFORM_GL_H -import androidx.annotation.NonNull; +#ifndef GLES_API_ENABLED +#define GLES_API_ENABLED // Allow using GLES. +#endif -import java.util.Collections; -import java.util.Set; +#include <EGL/egl.h> +#include <EGL/eglext.h> +#include <GLES3/gl3.h> +#include <GLES3/gl3ext.h> -/** - * Provides the set of information expected from a Godot plugin. - */ -public interface GodotPluginInfoProvider { - /** - * Returns the name of the plugin. - */ - @NonNull - String getPluginName(); - - /** - * Returns the list of signals to be exposed to Godot. - */ - @NonNull - default Set<SignalInfo> getPluginSignals() { - return Collections.emptySet(); - } - - /** - * Returns the paths for the plugin's gdextension libraries (if any). - * - * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file. - */ - @NonNull - default Set<String> getPluginGDExtensionLibrariesPaths() { - return Collections.emptySet(); - } - - /** - * This is invoked on the render thread when the plugin described by this instance has been - * registered. - */ - default void onPluginRegistered() { - } -} +#endif // PLATFORM_GL_H diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 5d48c4e248..fd60ba4ae7 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -129,31 +129,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS singleton->emit_signalp(StringName(signal_name), args, count); } - -JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDExtensionLibraries(JNIEnv *env, jclass clazz, jobjectArray gdextension_paths) { - int gdextension_count = env->GetArrayLength(gdextension_paths); - if (gdextension_count == 0) { - return; - } - - // Retrieve the current list of gdextension libraries. - Array singletons; - if (ProjectSettings::get_singleton()->has_setting("gdextension/singletons")) { - singletons = GLOBAL_GET("gdextension/singletons"); - } - - // Insert the libraries provided by the plugin - for (int i = 0; i < gdextension_count; i++) { - jstring relative_path = (jstring)env->GetObjectArrayElement(gdextension_paths, i); - - String path = "res://" + jstring_to_string(relative_path, env); - if (!singletons.has(path)) { - singletons.push_back(path); - } - env->DeleteLocalRef(relative_path); - } - - // Insert the updated list back into project settings. - ProjectSettings::get_singleton()->set("gdextension/singletons", singletons); -} } diff --git a/platform/android/plugin/godot_plugin_jni.h b/platform/android/plugin/godot_plugin_jni.h index 36a992246d..baa29a79ea 100644 --- a/platform/android/plugin/godot_plugin_jni.h +++ b/platform/android/plugin/godot_plugin_jni.h @@ -39,7 +39,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jclass clazz, jstring sname, jstring name, jstring ret, jobjectArray args); JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types); JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_params); -JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDExtensionLibraries(JNIEnv *env, jclass clazz, jobjectArray gdextension_paths); } #endif // GODOT_PLUGIN_JNI_H diff --git a/platform/android/vulkan_context_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp index 01e6d14438..9232126b04 100644 --- a/platform/android/vulkan_context_android.cpp +++ b/platform/android/rendering_context_driver_vulkan_android.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* vulkan_context_android.cpp */ +/* rendering_context_driver_vulkan_android.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#include "vulkan_context_android.h" +#include "rendering_context_driver_vulkan_android.h" #ifdef VULKAN_ENABLED @@ -38,32 +38,32 @@ #include <vulkan/vulkan.h> #endif -const char *VulkanContextAndroid::_get_platform_surface_extension() const { +const char *RenderingContextDriverVulkanAndroid::_get_platform_surface_extension() const { return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME; } -Error VulkanContextAndroid::window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height) { - VkAndroidSurfaceCreateInfoKHR createInfo; - createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; - createInfo.pNext = nullptr; - createInfo.flags = 0; - createInfo.window = p_window; +RenderingContextDriver::SurfaceID RenderingContextDriverVulkanAndroid::surface_create(const void *p_platform_data) { + const WindowPlatformData *wpd = (const WindowPlatformData *)(p_platform_data); - VkSurfaceKHR surface; - VkResult err = vkCreateAndroidSurfaceKHR(get_instance(), &createInfo, nullptr, &surface); - if (err != VK_SUCCESS) { - ERR_FAIL_V_MSG(ERR_CANT_CREATE, "vkCreateAndroidSurfaceKHR failed with error " + itos(err)); - } + VkAndroidSurfaceCreateInfoKHR create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; + create_info.window = wpd->window; - return _window_create(DisplayServer::MAIN_WINDOW_ID, p_vsync_mode, surface, p_width, p_height); + VkSurfaceKHR vk_surface = VK_NULL_HANDLE; + VkResult err = vkCreateAndroidSurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface); + ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID()); + + Surface *surface = memnew(Surface); + surface->vk_surface = vk_surface; + return SurfaceID(surface); } -bool VulkanContextAndroid::_use_validation_layers() { - uint32_t count = 0; - _get_preferred_validation_layers(&count, nullptr); +bool RenderingContextDriverVulkanAndroid::_use_validation_layers() const { + TightLocalVector<const char *> layer_names; + Error err = _find_validation_layers(layer_names); // On Android, we use validation layers automatically if they were explicitly linked with the app. - return count > 0; + return (err == OK) && !layer_names.is_empty(); } #endif // VULKAN_ENABLED diff --git a/platform/android/vulkan_context_android.h b/platform/android/rendering_context_driver_vulkan_android.h index f253149ef6..a2a42eef24 100644 --- a/platform/android/vulkan_context_android.h +++ b/platform/android/rendering_context_driver_vulkan_android.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* vulkan_context_android.h */ +/* rendering_context_driver_vulkan_android.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,28 +28,32 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef VULKAN_CONTEXT_ANDROID_H -#define VULKAN_CONTEXT_ANDROID_H +#ifndef RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H +#define RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H #ifdef VULKAN_ENABLED -#include "drivers/vulkan/vulkan_context.h" +#include "drivers/vulkan/rendering_context_driver_vulkan.h" struct ANativeWindow; -class VulkanContextAndroid : public VulkanContext { - virtual const char *_get_platform_surface_extension() const override; +class RenderingContextDriverVulkanAndroid : public RenderingContextDriverVulkan { +private: + virtual const char *_get_platform_surface_extension() const override final; -public: - Error window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height); +protected: + SurfaceID surface_create(const void *p_platform_data) override final; + bool _use_validation_layers() const override final; - VulkanContextAndroid() = default; - ~VulkanContextAndroid() override = default; +public: + struct WindowPlatformData { + ANativeWindow *window; + }; -protected: - bool _use_validation_layers() override; + RenderingContextDriverVulkanAndroid() = default; + ~RenderingContextDriverVulkanAndroid() override = default; }; #endif // VULKAN_ENABLED -#endif // VULKAN_CONTEXT_ANDROID_H +#endif // RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp index aef59c2584..93517d8045 100644 --- a/platform/android/tts_android.cpp +++ b/platform/android/tts_android.cpp @@ -54,7 +54,7 @@ void TTS_Android::setup(jobject p_tts) { bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); if (tts_enabled) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); tts = env->NewGlobalRef(p_tts); @@ -103,7 +103,7 @@ bool TTS_Android::is_speaking() { if (_is_speaking) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(tts, _is_speaking); } else { return false; @@ -115,7 +115,7 @@ bool TTS_Android::is_paused() { if (_is_paused) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(tts, _is_paused); } else { return false; @@ -127,7 +127,7 @@ Array TTS_Android::get_voices() { Array list; if (_get_voices) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, list); + ERR_FAIL_NULL_V(env, list); jobject voices_object = env->CallObjectMethod(tts, _get_voices); jobjectArray *arr = reinterpret_cast<jobjectArray *>(&voices_object); @@ -165,7 +165,7 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum if (_speak) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); @@ -178,7 +178,7 @@ void TTS_Android::pause() { if (_pause_speaking) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(tts, _pause_speaking); } } @@ -188,7 +188,7 @@ void TTS_Android::resume() { if (_resume_speaking) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(tts, _resume_speaking); } } @@ -203,7 +203,7 @@ void TTS_Android::stop() { if (_stop_speaking) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(tts, _stop_speaking); } } |