diff options
Diffstat (limited to 'platform/android')
62 files changed, 1150 insertions, 445 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 bd194478d9..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; } @@ -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,10 +344,12 @@ 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; } 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 a417ef454b..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(): @@ -28,6 +28,7 @@ def get_opts(): "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), ] @@ -50,7 +51,7 @@ def get_min_sdk_version(platform): return int(platform.split("-")[1]) -def get_android_ndk_root(env): +def get_android_ndk_root(env: "SConsEnvironment"): return env["ANDROID_HOME"] + "/ndk/" + get_ndk_version() @@ -68,12 +69,13 @@ 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_HOME"] if not os.path.exists(get_android_ndk_root(env)): @@ -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: @@ -200,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 d24d3fa389..972a7dbe6a 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -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; 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 dd5ab46bd7..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 @@ -125,6 +127,16 @@ bool DisplayServerAndroid::is_dark_mode() const { 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); @@ -515,20 +527,41 @@ 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); + } - ERR_FAIL_NULL(context_vulkan); - VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID); - context_vulkan->window_destroy(MAIN_WINDOW_ID); + VSyncMode last_vsync_mode = rendering_context->window_get_vsync_mode(MAIN_WINDOW_ID); + rendering_context->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 @@ -551,30 +584,51 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis } #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 + + 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; } - rendering_device_vulkan = memnew(RenderingDeviceVulkan); - rendering_device_vulkan->initialize(context_vulkan); + 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(); } @@ -587,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 } @@ -687,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 54912212dc..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,8 +105,11 @@ 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; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index dae968378b..7fce5359ae 100644 --- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -37,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> @@ -119,7 +131,10 @@ 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]. @@ -259,8 +274,7 @@ <member name="permissions/custom_permissions" type="PackedStringArray" setter="" getter=""> Array of custom permission strings. </member> - <member name="permissions/delete_cache_files" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/delete_cache_files" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/delete_packages" type="bool" setter="" getter=""> Allows an application to delete packages. See [url=https://developer.android.com/reference/android/Manifest.permission#DELETE_PACKAGES]DELETE_PACKAGES[/url]. @@ -295,8 +309,7 @@ <member name="permissions/get_package_size" type="bool" setter="" getter=""> Allows an application to find out the space used by any package. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_PACKAGE_SIZE]GET_PACKAGE_SIZE[/url]. </member> - <member name="permissions/get_tasks" type="bool" setter="" getter=""> - Deprecated in API level 21. + <member name="permissions/get_tasks" type="bool" setter="" getter="" deprecated="Deprecated in API level 21."> </member> <member name="permissions/get_top_activity_info" type="bool" setter="" getter=""> Allows an application to retrieve private information about the current top activity. @@ -364,13 +377,11 @@ <member name="permissions/nfc" type="bool" setter="" getter=""> Allows applications to perform I/O operations over NFC. See [url=https://developer.android.com/reference/android/Manifest.permission#NFC]NFC[/url]. </member> - <member name="permissions/persistent_activity" type="bool" setter="" getter=""> + <member name="permissions/persistent_activity" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> Allow an application to make its activities persistent. - Deprecated in API level 15. </member> - <member name="permissions/process_outgoing_calls" type="bool" setter="" getter=""> + <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="" deprecated="Deprecated in API level 29."> Allows an application to see the number being dialed during an outgoing call with the option to redirect the call to a different number or abort the call altogether. See [url=https://developer.android.com/reference/android/Manifest.permission#PROCESS_OUTGOING_CALLS]PROCESS_OUTGOING_CALLS[/url]. - Deprecated in API level 29. </member> <member name="permissions/read_calendar" type="bool" setter="" getter=""> Allows an application to read the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALENDAR]READ_CALENDAR[/url]. @@ -381,9 +392,8 @@ <member name="permissions/read_contacts" type="bool" setter="" getter=""> Allows an application to read the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CONTACTS]READ_CONTACTS[/url]. </member> - <member name="permissions/read_external_storage" type="bool" setter="" getter=""> + <member name="permissions/read_external_storage" type="bool" setter="" getter="" deprecated="Deprecated in API level 33."> Allows an application to read from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE]READ_EXTERNAL_STORAGE[/url]. - Deprecated in API level 33. </member> <member name="permissions/read_frame_buffer" type="bool" setter="" getter=""> Allows an application to take screen shots and more generally get access to the frame buffer data. @@ -391,8 +401,7 @@ <member name="permissions/read_history_bookmarks" type="bool" setter="" getter=""> Allows an application to read (but not write) the user's browsing history and bookmarks. </member> - <member name="permissions/read_input_state" type="bool" setter="" getter=""> - Deprecated in API level 16. + <member name="permissions/read_input_state" type="bool" setter="" getter="" deprecated="Deprecated in API level 16."> </member> <member name="permissions/read_logs" type="bool" setter="" getter=""> Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url]. @@ -439,8 +448,7 @@ <member name="permissions/reorder_tasks" type="bool" setter="" getter=""> Allows an application to change the Z-order of tasks. See [url=https://developer.android.com/reference/android/Manifest.permission#REORDER_TASKS]REORDER_TASKS[/url]. </member> - <member name="permissions/restart_packages" type="bool" setter="" getter=""> - Deprecated in API level 15. + <member name="permissions/restart_packages" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> </member> <member name="permissions/send_respond_via_message" type="bool" setter="" getter=""> Allows an application (Phone) to send a request to other applications to handle the respond-via-message action during incoming calls. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_RESPOND_VIA_MESSAGE]SEND_RESPOND_VIA_MESSAGE[/url]. @@ -469,8 +477,7 @@ <member name="permissions/set_pointer_speed" type="bool" setter="" getter=""> Allows low-level access to setting the pointer speed. </member> - <member name="permissions/set_preferred_applications" type="bool" setter="" getter=""> - Deprecated in API level 15. + <member name="permissions/set_preferred_applications" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> </member> <member name="permissions/set_process_limit" type="bool" setter="" getter=""> Allows an application to set the maximum number of (not needed) application processes that can be running. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_PROCESS_LIMIT]SET_PROCESS_LIMIT[/url]. @@ -496,8 +503,7 @@ <member name="permissions/subscribed_feeds_read" type="bool" setter="" getter=""> Allows an application to allow access the subscribed feeds ContentProvider. </member> - <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/system_alert_window" type="bool" setter="" getter=""> Allows an app to create windows using the type WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, shown on top of all other apps. See [url=https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW]SYSTEM_ALERT_WINDOW[/url]. @@ -505,8 +511,7 @@ <member name="permissions/transmit_ir" type="bool" setter="" getter=""> Allows using the device's IR transmitter, if available. See [url=https://developer.android.com/reference/android/Manifest.permission#TRANSMIT_IR]TRANSMIT_IR[/url]. </member> - <member name="permissions/uninstall_shortcut" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/update_device_stats" type="bool" setter="" getter=""> Allows an application to update device statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#UPDATE_DEVICE_STATS]UPDATE_DEVICE_STATS[/url]. diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index ee1ec2790d..138714634f 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -42,12 +42,15 @@ void register_android_exporter_types() { void register_android_exporter() { #ifndef ANDROID_ENABLED + 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); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index f0ee405b41..db0352db01 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -45,9 +45,10 @@ #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" @@ -208,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"); @@ -250,12 +253,12 @@ 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) { @@ -291,7 +294,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { // 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"); @@ -376,14 +379,15 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } else if (p.begins_with("ro.build.version.sdk=")) { d.api_level = p.get_slice("=", 1).to_int(); } else if (p.begins_with("ro.product.cpu.abi=")) { - d.description += "CPU: " + p.get_slice("=", 1).strip_edges() + "\n"; + d.architecture = p.get_slice("=", 1).strip_edges(); + d.description += "CPU: " + d.architecture + "\n"; } else if (p.begins_with("ro.product.manufacturer=")) { d.description += "Manufacturer: " + p.get_slice("=", 1).strip_edges() + "\n"; } else if (p.begins_with("ro.board.platform=")) { d.description += "Chipset: " + p.get_slice("=", 1).strip_edges() + "\n"; } else if (p.begins_with("ro.opengles.version=")) { uint32_t opengl = p.get_slice("=", 1).to_int(); - d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl)&0xFF) + "\n"; + d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n"; } } @@ -412,7 +416,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } - if (EDITOR_GET("export/android/shutdown_adb_on_exit")) { + if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { return; //adb not configured @@ -423,6 +427,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 { @@ -460,7 +483,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; } @@ -474,7 +497,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 { @@ -537,13 +561,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; } @@ -781,11 +798,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); @@ -812,6 +828,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) { @@ -874,7 +900,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres 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); @@ -1118,7 +1144,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; @@ -1612,7 +1638,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). @@ -1631,15 +1661,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, @@ -1647,26 +1668,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, @@ -1677,7 +1702,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()) { @@ -1685,7 +1710,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()) { @@ -1693,7 +1718,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); } } } @@ -1747,6 +1772,11 @@ 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 == "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"); if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) { @@ -1801,7 +1831,10 @@ 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. @@ -1830,10 +1863,10 @@ 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", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), "")); @@ -1879,6 +1912,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"; } @@ -1939,6 +1993,12 @@ String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const { return s; } +String EditorExportPlatformAndroid::get_device_architecture(int p_index) const { + ERR_FAIL_INDEX_V(p_index, devices.size(), ""); + MutexLock lock(device_lock); + return devices[p_index].architecture; +} + Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER); @@ -2118,8 +2178,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"; } @@ -2131,13 +2200,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"); @@ -2236,6 +2305,54 @@ 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; @@ -2291,7 +2408,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"; @@ -2336,6 +2453,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"; @@ -2412,6 +2555,19 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit valid = false; } + 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"); int min_sdk_int = VULKAN_MIN_SDK_VERSION; if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. @@ -2446,6 +2602,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; } @@ -2647,34 +2810,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; @@ -2690,7 +2856,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) { @@ -2749,6 +2915,8 @@ String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_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); @@ -2768,9 +2936,10 @@ bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExpor } 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 || first_build; + 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) { @@ -2782,12 +2951,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); @@ -2836,34 +3012,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); @@ -2871,12 +3064,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); @@ -2902,8 +3097,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 @@ -2912,7 +3110,7 @@ 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")); @@ -2929,6 +3127,7 @@ 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; @@ -2966,6 +3165,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP 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"); } @@ -2992,6 +3192,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP 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 @@ -3106,10 +3307,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); @@ -3302,10 +3499,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; @@ -3392,6 +3585,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); } } @@ -3425,6 +3619,7 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() { 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 a2d0417c5d..b968302449 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -76,6 +76,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String name; String description; int api_level = 0; + String architecture; }; struct APKExportData { @@ -90,6 +91,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { #endif // DISABLE_DEPRECATED String last_plugin_names; uint64_t last_gradle_build_time = 0; + String last_gradle_build_dir; Vector<Device> devices; SafeFlag devices_changed; @@ -97,8 +99,10 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { #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; @@ -174,10 +178,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, @@ -190,14 +190,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; @@ -218,6 +222,8 @@ public: virtual String get_option_tooltip(int p_index) const override; + virtual String get_device_architecture(int p_index) const override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; @@ -226,8 +232,11 @@ 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; @@ -245,9 +254,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/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 0915009235..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)); diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 2498394add..9f8e476f73 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -63,6 +63,7 @@ static const int XR_MODE_OPENXR = 1; struct CustomExportData { String assets_directory; + String libs_directory; bool debug; Vector<String> libs; }; @@ -94,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); 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 3aa4ca98fc..ec613b6687 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -66,12 +66,18 @@ 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 diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index beea73fd61..46d9728632 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -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."); @@ -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."); diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 0c3f8d7259..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 diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 079f629b12..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" > 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 a91e7bc7ce..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 : 17, + 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/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/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 1405b6c737..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 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 0d7017ae71..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 @@ -91,6 +91,10 @@ open class GodotEditor : GodotActivity() { private val commandLineParams = ArrayList<String>() override fun onCreate(savedInstanceState: Bundle?) { + // We exclude certain permissions from the set we request at startup, as they'll be + // requested on demand based on use-cases. + PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) + val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) Log.d(TAG, "Received parameters ${params.contentToString()}") updateCommandLineParams(params) 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/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 2278b46f6c..e2e77e7796 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -37,6 +37,7 @@ 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 @@ -149,6 +150,7 @@ class Godot(private val context: Context) : SensorEventListener { 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 @@ -184,7 +186,9 @@ class Godot(private val context: Context) : SensorEventListener { return } - beginBenchmarkMeasure("Godot::onCreate") + 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() @@ -286,7 +290,7 @@ class Godot(private val context: Context) : SensorEventListener { initializationStarted = false throw e } finally { - endBenchmarkMeasure("Godot::onCreate"); + endBenchmarkMeasure("Startup", "Godot::onCreate"); } } @@ -376,6 +380,8 @@ class Godot(private val context: Context) : SensorEventListener { 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()) { @@ -484,6 +490,14 @@ class Godot(private val context: Context) : SensorEventListener { return containerLayout } + fun onStart(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityStarted() + } + fun onResume(host: GodotHost) { if (host != primaryHost) { return @@ -528,6 +542,14 @@ class Godot(private val context: Context) : SensorEventListener { } } + fun onStop(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityStopped() + } + fun onDestroy(primaryHost: GodotHost) { if (this.primaryHost != primaryHost) { return @@ -544,6 +566,17 @@ class Godot(private val context: Context) : SensorEventListener { } /** + * 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?) { @@ -580,11 +613,13 @@ class Godot(private val context: Context) : SensorEventListener { // 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) } } @@ -715,7 +750,7 @@ class Godot(private val context: Context) : SensorEventListener { */ @Keep private fun isDarkModeSupported(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED } /** @@ -723,10 +758,7 @@ class Godot(private val context: Context) : SensorEventListener { */ @Keep private fun isDarkMode(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - } - return false + return darkMode } fun hasClipboard(): Boolean { @@ -1016,13 +1048,13 @@ class Godot(private val context: Context) : SensorEventListener { } @Keep - private fun nativeBeginBenchmarkMeasure(label: String) { - beginBenchmarkMeasure(label) + private fun nativeBeginBenchmarkMeasure(scope: String, label: String) { + beginBenchmarkMeasure(scope, label) } @Keep - private fun nativeEndBenchmarkMeasure(label: String) { - endBenchmarkMeasure(label) + private fun nativeEndBenchmarkMeasure(scope: String, label: String) { + endBenchmarkMeasure(scope, label) } @Keep diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index a60f6e997e..e01c5481d5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -30,7 +30,6 @@ package org.godotengine.godot -import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager @@ -65,10 +64,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { private set override fun onCreate(savedInstanceState: Bundle?) { - // We exclude certain permissions from the set we request at startup, as they'll be - // requested on demand based on use-cases. - PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) - super.onCreate(savedInstanceState) setContentView(R.layout.godot_app_layout) @@ -156,7 +151,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { super.onRequestPermissionsResult(requestCode, permissions, grantResults) godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) { + // 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 diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index 120e1722e5..a323045e1b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -38,6 +38,7 @@ 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; @@ -147,6 +148,13 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH @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) { @@ -172,7 +180,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH @Override public void onCreate(Bundle icicle) { - BenchmarkUtils.beginBenchmarkMeasure("GodotFragment::onCreate"); + BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate"); super.onCreate(icicle); final Activity activity = getActivity(); @@ -180,7 +188,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH godot = new Godot(requireContext()); performEngineInitialization(); - BenchmarkUtils.endBenchmarkMeasure("GodotFragment::onCreate"); + BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate"); } private void performEngineInitialization() { @@ -271,6 +279,32 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH } @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()) { 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 52350c12a6..81043ce782 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -114,12 +114,30 @@ 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 @@ -283,26 +301,4 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView /* 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(); - }); - } } 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 fee50e93c2..d0c3d4a687 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -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 ebf3a6b2fb..5b2f9f57c7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -47,8 +47,13 @@ public interface GodotRenderView { 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/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 48708152be..a1ee9bd6b4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -92,12 +92,30 @@ 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 @@ -211,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 56397bb2c2..bd8c58ad69 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 @@ -1671,7 +1673,24 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mWantRenderNotification = true; mRequestRender = true; mRenderComplete = false; - mFinishDrawingRunnable = finishDrawing; + + // fix lost old callback when continuous call requestRenderAndNotify + // + // If continuous call requestRenderAndNotify before trigger old + // callback, old callback will lose, cause VRI will wait for SV's + // draw to finish forever not calling finishDraw. + // https://android.googlesource.com/platform/frameworks/base/+/044fce0b826f2da3a192aac56785b5089143e693%5E%21/ + //+++++++++++++++++++++++++++++++++++++++++++++++++++ + final Runnable oldCallback = mFinishDrawingRunnable; + mFinishDrawingRunnable = () -> { + if (oldCallback != null) { + oldCallback.run(); + } + if (finishDrawing != null) { + finishDrawing.run(); + } + }; + //---------------------------------------------------- sGLThreadManager.notifyAll(); } 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 3070a8a207..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 @@ -34,10 +34,13 @@ 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; @@ -45,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 { // =========================================================== @@ -137,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; @@ -148,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; @@ -165,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); 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 38c115ad7f..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; @@ -102,6 +107,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { 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)) @@ -484,8 +496,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final float tiltX = (float)-Math.sin(orientation) * tiltMult; final float tiltY = (float)Math.cos(orientation) * tiltMult; - final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL); - final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + 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); 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/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index 9a82204467..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 @@ -56,9 +56,9 @@ import java.util.Set; 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) { @@ -235,7 +235,7 @@ public final class PermissionsUtil { /** * Check if the given permission is in the AndroidManifest.xml file. * @param context the caller context for this method. - * @param permission the permession to look for in the manifest file. + * @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(Context context, String permission) { 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 01ee41e30b..1f0d8592b3 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 @@ -66,11 +66,15 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory { GLUtils.checkEglError(TAG, "Before eglCreateContext", egl); EGLContext context; + int[] debug_attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; 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); + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, debug_attrib_list); + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + Log.w(TAG, "creating 'OpenGL Debug' context failed"); + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + } } else { - int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); } GLUtils.checkEglError(TAG, "After eglCreateContext", egl); 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_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 50075ed3f5..85d5cf2796 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -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 1ddda6c1c8..f32ffc291a 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -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_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index cb6ebf14a8..3c950bb1b1 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -78,8 +78,8 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _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, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V"); - _end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V"); + _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"); @@ -334,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); @@ -348,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); } } diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 920df958c2..93998021a9 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -104,9 +104,9 @@ public: void init_input_devices(); 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. 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 92dc5f909f..82e7fdb320 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -162,7 +162,7 @@ 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)) { @@ -324,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); } @@ -708,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 } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index f88f3e0518..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,8 +165,8 @@ 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; 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 |