diff options
Diffstat (limited to 'platform/android')
76 files changed, 2713 insertions, 1879 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index e4d04f1df9..dfc921cc54 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -21,7 +21,7 @@ android_files = [ "android_keys_utils.cpp", "display_server_android.cpp", "plugin/godot_plugin_jni.cpp", - "vulkan/vulkan_context_android.cpp", + "vulkan_context_android.cpp", ] env_android = env.Clone() @@ -56,7 +56,10 @@ if lib_arch_dir != "": if env.dev_build: lib_type_dir = "dev" elif env.debug_features: - lib_type_dir = "debug" + if env.editor_build and env["store_release"]: + lib_type_dir = "release" + else: + lib_type_dir = "debug" else: # Release lib_type_dir = "release" diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index 63045237e9..f6a0776017 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -64,7 +64,7 @@ void AndroidInputHandler::_set_key_modifier_state(Ref<InputEventWithModifiers> e } } -void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed) { +void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed, bool p_echo) { static char32_t prev_wc = 0; char32_t unicode = p_unicode; if ((p_unicode & 0xfffffc00) == 0xd800) { @@ -88,7 +88,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod ev.instantiate(); Key physical_keycode = godot_code_from_android_code(p_physical_keycode); - Key keycode = physical_keycode; + Key keycode; if (unicode == '\b') { // 0x08 keycode = Key::BACKSPACE; } else if (unicode == '\t') { // 0x09 @@ -125,6 +125,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod ev->set_key_label(fix_key_label(p_key_label, keycode)); ev->set_unicode(fix_unicode(unicode)); ev->set_pressed(p_pressed); + ev->set_echo(p_echo); _set_key_modifier_state(ev, keycode); @@ -138,22 +139,19 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod } void AndroidInputHandler::_cancel_all_touch() { - _parse_all_touch(false, false, true); + _parse_all_touch(false, true); touch.clear(); } -void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index) { +void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled, bool p_double_tap) { if (touch.size()) { //end all if exist for (int i = 0; i < touch.size(); i++) { Ref<InputEventScreenTouch> ev; ev.instantiate(); - if (reset_index) { - ev->set_index(-1); - } else { - ev->set_index(touch[i].id); - } + ev->set_index(touch[i].id); ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); ev->set_position(touch[i].pos); ev->set_double_tap(p_double_tap); Input::get_singleton()->parse_input_event(ev); @@ -180,7 +178,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const } //send touch - _parse_all_touch(true, p_double_tap); + _parse_all_touch(true, false, p_double_tap); } break; case AMOTION_EVENT_ACTION_MOVE: { //motion @@ -257,11 +255,11 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const void AndroidInputHandler::_cancel_mouse_event_info(bool p_source_mouse_relative) { buttons_state = BitField<MouseButtonMask>(); - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, true, false, p_source_mouse_relative); mouse_event_info.valid = false; } -void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) { +void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative) { if (!mouse_event_info.valid) { return; } @@ -278,6 +276,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even hover_prev_pos = mouse_event_info.pos; } ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); BitField<MouseButtonMask> changed_button_mask = BitField<MouseButtonMask>(buttons_state.operator int64_t() ^ event_buttons_mask.operator int64_t()); buttons_state = event_buttons_mask; @@ -289,7 +288,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even } void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative) { - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, false, p_source_mouse_relative); mouse_event_info.valid = false; } @@ -318,7 +317,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an mouse_event_info.valid = true; mouse_event_info.pos = p_event_pos; - _parse_mouse_event_info(event_buttons_mask, true, p_double_click, p_source_mouse_relative); + _parse_mouse_event_info(event_buttons_mask, true, false, p_double_click, p_source_mouse_relative); } break; case AMOTION_EVENT_ACTION_CANCEL: { diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 2badd32636..c74c5020e3 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -83,13 +83,13 @@ private: void _wheel_button_click(BitField<MouseButtonMask> event_buttons_mask, const Ref<InputEventMouseButton> &ev, MouseButton wheel_button, float factor); - void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative); + void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative); void _release_mouse_event_info(bool p_source_mouse_relative = false); void _cancel_mouse_event_info(bool p_source_mouse_relative = false); - void _parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index = false); + void _parse_all_touch(bool p_pressed, bool p_canceled = false, bool p_double_tap = false); void _release_all_touch(); @@ -101,7 +101,7 @@ public: void process_magnify(Point2 p_pos, float p_factor); void process_pan(Point2 p_pos, Vector2 p_delta); void process_joy_event(JoypadEvent p_event); - void process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed); + void process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed, bool p_echo); }; #endif // ANDROID_INPUT_HANDLER_H diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h index 3a587dd680..5cf5628a8b 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -31,8 +31,9 @@ #ifndef ANDROID_KEYS_UTILS_H #define ANDROID_KEYS_UTILS_H +#include "core/os/keyboard.h" + #include <android/input.h> -#include <core/os/keyboard.h> #define AKEYCODE_MAX 0xFFFF @@ -60,6 +61,7 @@ static AndroidGodotCodePair android_godot_code_pairs[] = { { AKEYCODE_DPAD_DOWN, Key::DOWN }, // (20) Directional Pad Down key. { AKEYCODE_DPAD_LEFT, Key::LEFT }, // (21) Directional Pad Left key. { AKEYCODE_DPAD_RIGHT, Key::RIGHT }, // (22) Directional Pad Right key. + { AKEYCODE_DPAD_CENTER, Key::ENTER }, // (23) Directional Pad Center key. { AKEYCODE_VOLUME_UP, Key::VOLUMEUP }, // (24) Volume Up key. { AKEYCODE_VOLUME_DOWN, Key::VOLUMEDOWN }, // (25) Volume Down key. { AKEYCODE_POWER, Key::STANDBY }, // (26) Power key. diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 757ca315a7..60c369951c 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -30,10 +30,11 @@ #include "api.h" -#include "core/config/engine.h" #include "java_class_wrapper.h" #include "jni_singleton.h" +#include "core/config/engine.h" + #if !defined(ANDROID_ENABLED) static JavaClassWrapper *java_class_wrapper = nullptr; #endif diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 455ed259ec..a2d1c08168 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -33,8 +33,9 @@ #include "core/config/engine.h" #include "core/variant/variant.h" + #ifdef ANDROID_ENABLED -#include "platform/android/jni_utils.h" +#include "jni_utils.h" #endif class JNISingleton : public Object { diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 5fc32132e3..51e89c720d 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -272,7 +272,8 @@ Error AudioDriverOpenSL::input_start() { return init_input_device(); } - return OK; + WARN_PRINT("Unable to start audio capture - No RECORD_AUDIO permission"); + return ERR_UNAUTHORIZED; } Error AudioDriverOpenSL::input_stop() { diff --git a/platform/android/detect.py b/platform/android/detect.py index 7515d0020d..33c6565789 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -9,10 +9,6 @@ if TYPE_CHECKING: from SCons import Environment -def is_active(): - return True - - def get_name(): return "Android" @@ -22,6 +18,8 @@ def can_build(): def get_opts(): + from SCons.Variables import BoolVariable + return [ ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), ( @@ -29,6 +27,7 @@ def get_opts(): 'Target platform (android-<api>, e.g. "android-' + str(get_min_target_api()) + '")', "android-" + str(get_min_target_api()), ), + BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False), ] @@ -171,10 +170,6 @@ def configure(env: "Environment"): env["RANLIB"] = compiler_path + "/llvm-ranlib" env["AS"] = compiler_path + "/clang" - # Disable exceptions on template builds - if not env.editor_build: - env.Append(CXXFLAGS=["-fno-exceptions"]) - env.Append( CCFLAGS=( "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7b41ad87bd..6bd09fe00a 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -30,10 +30,11 @@ #include "dir_access_jandroid.h" -#include "core/string/print_string.h" #include "string_android.h" #include "thread_jandroid.h" +#include "core/string/print_string.h" + jobject DirAccessJAndroid::dir_access_handler = nullptr; jclass DirAccessJAndroid::cls = nullptr; jmethodID DirAccessJAndroid::_dir_open = nullptr; diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 05b7d47957..5ee4c85659 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -31,9 +31,11 @@ #ifndef DIR_ACCESS_JANDROID_H #define DIR_ACCESS_JANDROID_H +#include "java_godot_lib_jni.h" + #include "core/io/dir_access.h" #include "drivers/unix/dir_access_unix.h" -#include "java_godot_lib_jni.h" + #include <stdio.h> /// Android implementation of the DirAccess interface used to provide access to diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index e07e0e1149..f02b292868 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -30,19 +30,23 @@ #include "display_server_android.h" -#include "core/config/project_settings.h" #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" #include "tts_android.h" +#include "core/config/project_settings.h" + #if defined(VULKAN_ENABLED) +#include "vulkan_context_android.h" + #include "drivers/vulkan/rendering_device_vulkan.h" -#include "platform/android/vulkan/vulkan_context_android.h" #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" #endif + #ifdef GLES3_ENABLED #include "drivers/gles3/rasterizer_gles3.h" + #include <EGL/egl.h> #endif @@ -449,6 +453,10 @@ void DisplayServerAndroid::window_move_to_foreground(DisplayServer::WindowID p_w // Not supported on Android. } +bool DisplayServerAndroid::window_is_focused(WindowID p_window) const { + return true; +} + bool DisplayServerAndroid::window_can_draw(DisplayServer::WindowID p_window) const { return true; } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index ad1cbddb08..e0ad2cb916 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -178,6 +178,7 @@ public: virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; + virtual bool window_is_focused(WindowID p_window = MAIN_WINDOW_ID) const override; virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index 11129ca149..d270980d72 100644 --- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> +<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> <brief_description> Exporter for Android. </brief_description> @@ -59,14 +59,17 @@ <member name="keystore/debug" type="String" setter="" getter=""> Path of the debug keystore file. Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PATH[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore[/code] if empty. </member> <member name="keystore/debug_password" type="String" setter="" getter=""> Password for the debug keystore file. Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_pass[/code] if both it and [member keystore/debug] are empty. </member> <member name="keystore/debug_user" type="String" setter="" getter=""> User name for the debug keystore file. Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_USER[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_user[/code] if both it and [member keystore/debug] are empty. </member> <member name="keystore/release" type="String" setter="" getter=""> Path of the release keystore file. @@ -87,7 +90,7 @@ Foreground layer of the application adaptive icon file. </member> <member name="launcher_icons/main_192x192" type="String" setter="" getter=""> - Application icon file. If left empty, project icon is used instead. + Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon]. </member> <member name="package/app_category" type="int" setter="" getter=""> Application category for the Play Store. @@ -101,6 +104,16 @@ <member name="package/retain_data_on_uninstall" type="bool" setter="" getter=""> If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. </member> + <member name="package/show_as_launcher_app" type="bool" setter="" getter=""> + If [code]true[/code], the user will be able to set this app as the system launcher in Android preferences. + </member> + <member name="package/show_in_android_tv" type="bool" setter="" getter=""> + If [code]true[/code], this app will show in Android TV launcher UI. + </member> + <member name="package/show_in_app_library" type="bool" setter="" getter=""> + If [code]true[/code], this app will show in the device's app library. + [b]Note:[/b] This is [code]true[/code] by default. + </member> <member name="package/signed" type="bool" setter="" getter=""> If [code]true[/code], package signing is enabled. </member> @@ -141,7 +154,7 @@ Allows an application to act as an AccountAuthenticator for the AccountManager. </member> <member name="permissions/battery_stats" type="bool" setter="" getter=""> - Allows an application to collect battery statistics. Sett [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. + Allows an application to collect battery statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. </member> <member name="permissions/bind_accessibility_service" type="bool" setter="" getter=""> Must be required by an AccessibilityService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_ACCESSIBILITY_SERVICE]BIND_ACCESSIBILITY_SERVICE[/url]. @@ -423,7 +436,7 @@ Allows an application to record audio. See [url=https://developer.android.com/reference/android/Manifest.permission#RECORD_AUDIO]RECORD_AUDIO[/url]. </member> <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]. + 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. @@ -570,16 +583,10 @@ If [code]true[/code], allows the application to participate in the backup and restore infrastructure. </member> <member name="version/code" type="int" setter="" getter=""> - Machine-readable application version. + Machine-readable application version. This must be incremented for every new release pushed to the Play Store. </member> <member name="version/name" type="String" setter="" getter=""> - Application version visible to the user. - </member> - <member name="xr_features/hand_tracking" type="int" setter="" getter=""> - </member> - <member name="xr_features/hand_tracking_frequency" type="int" setter="" getter=""> - </member> - <member name="xr_features/passthrough" type="int" setter="" getter=""> + Application version visible to the user. Falls back to [member ProjectSettings.application/config/version] if left empty. </member> <member name="xr_features/xr_mode" type="int" setter="" getter=""> </member> diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 1e048100d1..32b46271fd 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -30,10 +30,11 @@ #include "export.h" +#include "export_plugin.h" + #include "core/os/os.h" #include "editor/editor_settings.h" #include "editor/export/editor_export.h" -#include "export_plugin.h" void register_android_exporter_types() { GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformAndroid); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index d8dd453faf..20aaed1e3e 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -31,6 +31,8 @@ #include "export_plugin.h" #include "gradle_export_util.h" +#include "logo_svg.gen.h" +#include "run_icon_svg.gen.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" @@ -46,10 +48,9 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "main/splash.gen.h" -#include "platform/android/logo_svg.gen.h" -#include "platform/android/run_icon_svg.gen.h" +#include "scene/resources/image_texture.h" -#include "modules/modules_enabled.gen.h" // For svg. +#include "modules/modules_enabled.gen.h" // For mono and svg. #ifdef MODULE_SVG_ENABLED #include "modules/svg/image_loader_svg.h" #endif @@ -260,30 +261,32 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); while (!ea->quit_request.is_set()) { - // Check for plugins updates +#ifndef DISABLE_DEPRECATED + // Check for android plugins updates { // Nothing to do if we already know the plugins have changed. - if (!ea->plugins_changed.is_set()) { + if (!ea->android_plugins_changed.is_set()) { Vector<PluginConfigAndroid> loaded_plugins = get_plugins(); - MutexLock lock(ea->plugins_lock); + MutexLock lock(ea->android_plugins_lock); - if (ea->plugins.size() != loaded_plugins.size()) { - ea->plugins_changed.set(); + if (ea->android_plugins.size() != loaded_plugins.size()) { + ea->android_plugins_changed.set(); } else { - for (int i = 0; i < ea->plugins.size(); i++) { - if (ea->plugins[i].name != loaded_plugins[i].name) { - ea->plugins_changed.set(); + for (int i = 0; i < ea->android_plugins.size(); i++) { + if (ea->android_plugins[i].name != loaded_plugins[i].name) { + ea->android_plugins_changed.set(); break; } } } - if (ea->plugins_changed.is_set()) { - ea->plugins = loaded_plugins; + if (ea->android_plugins_changed.is_set()) { + ea->android_plugins = loaded_plugins; } } } +#endif // DISABLE_DEPRECATED // Check for devices updates String adb = get_adb_path(); @@ -627,6 +630,7 @@ Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_abis() return abis; } +#ifndef DISABLE_DEPRECATED /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; @@ -693,6 +697,7 @@ Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_enabled_plugins(con return enabled_plugins; } +#endif // DISABLE_DEPRECATED Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method) { zip_fileinfo zipfi = get_zip_fileinfo(); @@ -827,16 +832,6 @@ void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> r_permissions.push_back("android.permission.INTERNET"); } } - - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - if (xr_mode_index == XR_MODE_OPENXR) { - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required - if (hand_tracking_index > XR_HAND_TRACKING_NONE) { - if (r_permissions.find("com.oculus.permission.HAND_TRACKING") == -1) { - r_permissions.push_back("com.oculus.permission.HAND_TRACKING"); - } - } - } } void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) { @@ -860,8 +855,23 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres } } - manifest_text += _get_xr_features_tag(p_preset, _uses_vulkan()); - manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms)); + if (_uses_vulkan()) { + manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; + manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; + } + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + const String contents = export_plugins[i]->get_android_manifest_element_contents(Ref<EditorExportPlatform>(this), p_debug); + if (!contents.is_empty()) { + manifest_text += contents; + manifest_text += "\n"; + } + } + } + + manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug); manifest_text += "</manifest>\n"; String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); @@ -894,7 +904,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p uint32_t string_table_ends = 0; Vector<uint8_t> stable_extra; - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); int version_code = p_preset->get("version/code"); String package_name = p_preset->get("package/unique_name"); @@ -1720,7 +1730,7 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport } } else if (p_name == "gradle_build/use_gradle_build") { bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); - String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(Ref<EditorExportPreset>(p_preset))); + String enabled_plugins_names = _get_plugins_names(Ref<EditorExportPreset>(p_preset)); if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { return TTR("\"Use Gradle Build\" must be enabled to use the plugins."); } @@ -1730,22 +1740,6 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { return TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); } - } else if (p_name == "xr_features/hand_tracking") { - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int hand_tracking = p_preset->get("xr_features/hand_tracking"); - if (xr_mode_index != XR_MODE_OPENXR) { - if (hand_tracking > XR_HAND_TRACKING_NONE) { - return TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); - } - } - } else if (p_name == "xr_features/passthrough") { - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (xr_mode_index != XR_MODE_OPENXR) { - if (passthrough_mode > XR_PASSTHROUGH_NONE) { - return TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); - } - } } else if (p_name == "gradle_build/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) { @@ -1807,12 +1801,14 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true)); +#ifndef DISABLE_DEPRECATED Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { print_verbose("Found Android plugin " + plugins_configs[i].name); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false)); } - plugins_changed.clear(); + android_plugins_changed.clear(); +#endif // DISABLE_DEPRECATED // Android supports multiple architectures in an app bundle, so // we expose each option as a checkbox in the export dialog. @@ -1833,7 +1829,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio 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::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); @@ -1841,6 +1837,9 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/retain_data_on_uninstall"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/exclude_from_recents"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_android_tv"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_app_library"), true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_as_launcher_app"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_icon_option, PROPERTY_HINT_FILE, "*.png"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_foreground_option, PROPERTY_HINT_FILE, "*.png"), "")); @@ -1849,9 +1848,6 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE, false, true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking_frequency", PROPERTY_HINT_ENUM, "Low,High"), XR_HAND_TRACKING_FREQUENCY_LOW)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true)); @@ -1889,12 +1885,14 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_logo() const { } bool EditorExportPlatformAndroid::should_update_export_options() { - bool export_options_changed = plugins_changed.is_set(); - if (export_options_changed) { +#ifndef DISABLE_DEPRECATED + if (android_plugins_changed.is_set()) { // don't clear unless we're reporting true, to avoid race - plugins_changed.clear(); + android_plugins_changed.clear(); + return true; } - return export_options_changed; +#endif // DISABLE_DEPRECATED + return false; } bool EditorExportPlatformAndroid::poll_export() { @@ -1983,7 +1981,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, String output; bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install"); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String package_name = p_preset->get("package/unique_name"); if (remove_prev) { @@ -2227,11 +2225,16 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_ return apksigner_path; } -bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { String err; bool valid = false; const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); +#ifdef MODULE_MONO_ENABLED + // Android export is still a work in progress, keep a message as a warning. + err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n"; +#endif + // Look for export templates (first official, and if defined custom templates). if (!gradle_build_enabled) { @@ -2286,7 +2289,8 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n"; } - if (!FileAccess::exists(dk)) { + // Use OR to make the export UI able to show this error. + if ((p_debug || !dk.is_empty()) && !FileAccess::exists(dk)) { dk = EDITOR_GET("export/android/debug_keystore"); if (!FileAccess::exists(dk)) { valid = false; @@ -2303,7 +2307,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n"; } - if (!rk.is_empty() && !FileAccess::exists(rk)) { + if (!p_debug && !rk.is_empty() && !FileAccess::exists(rk)) { valid = false; err += TTR("Release keystore incorrectly configured in the export preset.") + "\n"; } @@ -2323,7 +2327,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito valid = false; } - // Validate that adb is available + // Validate that adb is available. String adb_path = get_adb_path(); if (!FileAccess::exists(adb_path)) { err += TTR("Unable to find Android SDK platform-tools' adb command."); @@ -2345,7 +2349,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); } - // Validate that apksigner is available + // Validate that apksigner is available. String apksigner_path = get_apksigner_path(target_sdk_version.to_int()); if (!FileAccess::exists(apksigner_path)) { err += TTR("Unable to find Android SDK build-tools' apksigner command."); @@ -2542,7 +2546,6 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) { return ERR_SKIP; } - } else { keystore = release_keystore; password = release_password; @@ -2686,6 +2689,64 @@ String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformA return ret; } +String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const { + Vector<String> names; + +#ifndef DISABLE_DEPRECATED + PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names); +#endif // DISABLE_DEPRECATED + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + names.push_back(export_plugins[i]->get_name()); + } + } + + String plugins_names = String("|").join(names); + return plugins_names; +} + +String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const { + String absolute_path; + if (!p_android_library_path.is_empty()) { + if (p_android_library_path.is_absolute_path()) { + absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path); + } else { + const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path); + absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path); + } + } + return absolute_path; +} + +bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) { + bool first_build = last_gradle_build_time == 0; + bool have_plugins_changed = false; + + String plugin_names = _get_plugins_names(p_preset); + + if (!first_build) { + have_plugins_changed = plugin_names != last_plugin_names; +#ifndef DISABLE_DEPRECATED + if (!have_plugins_changed) { + Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset); + for (int i = 0; i < enabled_plugins.size(); i++) { + if (enabled_plugins.get(i).last_updated > last_gradle_build_time) { + have_plugins_changed = true; + break; + } + } + } +#endif // DISABLE_DEPRECATED + } + + last_gradle_build_time = OS::get_singleton()->get_unix_time(); + last_plugin_names = plugin_names; + + return have_plugins_changed || first_build; +} + Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { int export_format = int(p_preset->get("gradle_build/export_format")); bool should_sign = p_preset->get("package/signed"); @@ -2830,7 +2891,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String package_name = get_package_name(p_preset->get("package/unique_name")); String version_code = itos(p_preset->get("version/code")); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String min_sdk_version = p_preset->get("gradle_build/min_sdk"); if (!min_sdk_version.is_valid_int()) { min_sdk_version = itos(VULKAN_MIN_SDK_VERSION); @@ -2843,11 +2904,40 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String sign_flag = should_sign ? "true" : "false"; String zipalign_flag = "true"; + Vector<String> android_libraries; + Vector<String> android_dependencies; + Vector<String> android_dependencies_maven_repos; + +#ifndef DISABLE_DEPRECATED Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset); - String local_plugins_binaries = PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins); - String remote_plugins_binaries = PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins); - String custom_maven_repos = PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins); - bool clean_build_required = is_clean_build_required(enabled_plugins); + PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries); + PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies); + PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos); +#endif // DISABLE_DEPRECATED + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { + PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug); + for (int k = 0; k < export_plugin_android_libraries.size(); k++) { + const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]); + if (!resolved_android_library_path.is_empty()) { + android_libraries.push_back(resolved_android_library_path); + } + } + + PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug); + android_dependencies.append_array(export_plugin_android_dependencies); + + PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug); + android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos); + } + } + + bool clean_build_required = _is_clean_build_required(p_preset); + String combined_android_libraries = String("|").join(android_libraries); + String combined_android_dependencies = String("|").join(android_dependencies); + String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos); List<String> cmdline; if (clean_build_required) { @@ -2871,9 +2961,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk. cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk. cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs. - cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies. - cmdline.push_back("-Pplugins_remote_binaries=" + remote_plugins_binaries); // argument to specify the list of plugins remote dependencies. - cmdline.push_back("-Pplugins_maven_repos=" + custom_maven_repos); // argument to specify the list of custom maven repos for the plugins dependencies. + cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins. + cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins. + cmdline.push_back("-Pplugins_maven_repos=" + combined_android_dependencies_maven_repos); // argument to specify the list of maven repos for android dependencies provided by plugins. cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned. cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed. cmdline.push_back("-Pgodot_editor_version=" + String(VERSION_FULL_CONFIG)); @@ -3019,7 +3109,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String cmdline = p_preset->get("command_line/extra_args"); - String version_name = p_preset->get("version/name"); + String version_name = p_preset->get_version("version/name"); String package_name = p_preset->get("package/unique_name"); String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); @@ -3291,16 +3381,17 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() { Ref<Image> img = memnew(Image); const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); + ImageLoaderSVG::create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); logo = ImageTexture::create_from_image(img); - img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); + ImageLoaderSVG::create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); run_icon = ImageTexture::create_from_image(img); #endif devices_changed.set(); - plugins_changed.set(); +#ifndef DISABLE_DEPRECATED + android_plugins_changed.set(); +#endif // DISABLE_DEPRECATED #ifndef ANDROID_ENABLED 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 390b8c6465..a2d0417c5d 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -31,7 +31,9 @@ #ifndef ANDROID_EXPORT_PLUGIN_H #define ANDROID_EXPORT_PLUGIN_H +#ifndef DISABLE_DEPRECATED #include "godot_plugin_config.h" +#endif // DISABLE_DEPRECATED #include "core/io/zip_io.h" #include "core/os/os.h" @@ -81,11 +83,14 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { EditorProgress *ep = nullptr; }; - mutable Vector<PluginConfigAndroid> plugins; +#ifndef DISABLE_DEPRECATED + mutable Vector<PluginConfigAndroid> android_plugins; + mutable SafeFlag android_plugins_changed; + Mutex android_plugins_lock; +#endif // DISABLE_DEPRECATED String last_plugin_names; uint64_t last_gradle_build_time = 0; - mutable SafeFlag plugins_changed; - Mutex plugins_lock; + Vector<Device> devices; SafeFlag devices_changed; Mutex device_lock; @@ -128,12 +133,14 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Vector<ABI> get_abis(); +#ifndef DISABLE_DEPRECATED /// List the gdap files in the directory specified by the p_path parameter. static Vector<String> list_gdap_files(const String &p_path); static Vector<PluginConfigAndroid> get_plugins(); static Vector<PluginConfigAndroid> get_enabled_plugins(const Ref<EditorExportPreset> &p_presets); +#endif // DISABLE_DEPRECATED static Error store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method = Z_DEFLATED); @@ -219,33 +226,16 @@ public: static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false); - virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + 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; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; - inline bool is_clean_build_required(Vector<PluginConfigAndroid> enabled_plugins) { - String plugin_names = PluginConfigAndroid::get_plugins_names(enabled_plugins); - bool first_build = last_gradle_build_time == 0; - bool have_plugins_changed = false; - - if (!first_build) { - have_plugins_changed = plugin_names != last_plugin_names; - if (!have_plugins_changed) { - for (int i = 0; i < enabled_plugins.size(); i++) { - if (enabled_plugins.get(i).last_updated > last_gradle_build_time) { - have_plugins_changed = true; - break; - } - } - } - } + String _get_plugins_names(const Ref<EditorExportPreset> &p_preset) const; - last_gradle_build_time = OS::get_singleton()->get_unix_time(); - last_plugin_names = plugin_names; + String _resolve_export_plugin_android_library_path(const String &p_android_library_path) const; - return have_plugins_changed || first_build; - } + bool _is_clean_build_required(const Ref<EditorExportPreset> &p_preset); String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path); diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp index 56431c25de..cdec5f55b7 100644 --- a/platform/android/export/godot_plugin_config.cpp +++ b/platform/android/export/godot_plugin_config.cpp @@ -29,6 +29,9 @@ /**************************************************************************/ #include "godot_plugin_config.h" + +#ifndef DISABLE_DEPRECATED + /* * Set of prebuilt plugins. * Currently unused, this is just for future reference: @@ -144,10 +147,8 @@ PluginConfigAndroid PluginConfigAndroid::load_plugin_config(Ref<ConfigFile> conf return plugin_config; } -String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs) { - String plugins_binaries; +void PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> binaries; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { @@ -155,56 +156,44 @@ String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<Plug } if (config.binary_type == binary_type) { - binaries.push_back(config.binary); + r_result.push_back(config.binary); } if (binary_type == PluginConfigAndroid::BINARY_TYPE_LOCAL) { - binaries.append_array(config.local_dependencies); + r_result.append_array(config.local_dependencies); } if (binary_type == PluginConfigAndroid::BINARY_TYPE_REMOTE) { - binaries.append_array(config.remote_dependencies); + r_result.append_array(config.remote_dependencies); } } - - plugins_binaries = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(binaries); } - - return plugins_binaries; } -String PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs) { - String custom_maven_repos; +void PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> repos_urls; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { continue; } - repos_urls.append_array(config.custom_maven_repos); + r_result.append_array(config.custom_maven_repos); } - - custom_maven_repos = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(repos_urls); } - return custom_maven_repos; } -String PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs) { - String plugins_names; +void PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) { if (!plugins_configs.is_empty()) { - Vector<String> names; for (int i = 0; i < plugins_configs.size(); i++) { PluginConfigAndroid config = plugins_configs[i]; if (!config.valid_config) { continue; } - names.push_back(config.name); + r_result.push_back(config.name); } - plugins_names = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(names); } - - return plugins_names; } + +#endif // DISABLE_DEPRECATED diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h index bef00979a9..8c56d00187 100644 --- a/platform/android/export/godot_plugin_config.h +++ b/platform/android/export/godot_plugin_config.h @@ -31,6 +31,8 @@ #ifndef ANDROID_GODOT_PLUGIN_CONFIG_H #define ANDROID_GODOT_PLUGIN_CONFIG_H +#ifndef DISABLE_DEPRECATED + #include "core/config/project_settings.h" #include "core/error/error_list.h" #include "core/io/config_file.h" @@ -67,8 +69,6 @@ struct PluginConfigAndroid { inline static const char *BINARY_TYPE_LOCAL = "local"; inline static const char *BINARY_TYPE_REMOTE = "remote"; - inline static const char *PLUGIN_VALUE_SEPARATOR = "|"; - // Set to true when the config file is properly loaded. bool valid_config = false; // Unix timestamp of last change to this plugin. @@ -96,11 +96,13 @@ struct PluginConfigAndroid { static PluginConfigAndroid load_plugin_config(Ref<ConfigFile> config_file, const String &path); - static String get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); - static String get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); - static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs); + static void get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result); }; +#endif // DISABLE_DEPRECATED + #endif // ANDROID_GODOT_PLUGIN_CONFIG_H diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index ba4487cc4d..0915009235 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -254,34 +254,7 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_screen_sizes; } -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan) { - String manifest_xr_features; - int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); - bool uses_xr = xr_mode_index == XR_MODE_OPENXR; - if (uses_xr) { - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required - if (hand_tracking_index == XR_HAND_TRACKING_OPTIONAL) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; - } else if (hand_tracking_index == XR_HAND_TRACKING_REQUIRED) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"true\" />\n"; - } - - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (passthrough_mode == XR_PASSTHROUGH_OPTIONAL) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"com.oculus.feature.PASSTHROUGH\" android:required=\"false\" />\n"; - } else if (passthrough_mode == XR_PASSTHROUGH_REQUIRED) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"com.oculus.feature.PASSTHROUGH\" android:required=\"true\" />\n"; - } - } - - if (p_uses_vulkan) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; - } - return manifest_xr_features; -} - -String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr) { +String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) { String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " @@ -294,40 +267,46 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr orientation, bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); - if (p_uses_xr) { - manifest_activity_text += " <intent-filter>\n" - " <action android:name=\"android.intent.action.MAIN\" />\n" - " <category android:name=\"android.intent.category.LAUNCHER\" />\n" - "\n" - " <!-- Enable access to OpenXR on Oculus mobile devices, no-op on other Android\n" - " platforms. -->\n" - " <category android:name=\"com.oculus.intent.category.VR\" />\n" - "\n" - " <!-- OpenXR category tag to indicate the activity starts in an immersive OpenXR mode. \n" - " See https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#android-runtime-category. -->\n" - " <category android:name=\"org.khronos.openxr.intent.category.IMMERSIVE_HMD\" />\n" - "\n" - " <!-- Enable VR access on HTC Vive Focus devices. -->\n" - " <category android:name=\"com.htc.intent.category.VRAPP\" />\n" - " </intent-filter>\n"; - } else { - manifest_activity_text += " <intent-filter>\n" - " <action android:name=\"android.intent.action.MAIN\" />\n" - " <category android:name=\"android.intent.category.LAUNCHER\" />\n" - " </intent-filter>\n"; + manifest_activity_text += " <intent-filter>\n" + " <action android:name=\"android.intent.action.MAIN\" />\n" + " <category android:name=\"android.intent.category.DEFAULT\" />\n"; + + bool show_in_app_library = p_preset->get("package/show_in_app_library"); + if (show_in_app_library) { + manifest_activity_text += " <category android:name=\"android.intent.category.LAUNCHER\" />\n"; + } + + bool uses_leanback_category = p_preset->get("package/show_in_android_tv"); + if (uses_leanback_category) { + manifest_activity_text += " <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n"; + } + + bool uses_home_category = p_preset->get("package/show_as_launcher_app"); + if (uses_home_category) { + manifest_activity_text += " <category android:name=\"android.intent.category.HOME\" />\n"; + } + + manifest_activity_text += " </intent-filter>\n"; + + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(p_export_platform)) { + const String contents = export_plugins[i]->get_android_manifest_activity_element_contents(p_export_platform, p_debug); + if (!contents.is_empty()) { + manifest_activity_text += contents; + manifest_activity_text += "\n"; + } + } } manifest_activity_text += " </activity>\n"; return manifest_activity_text; } -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission) { +String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug) { int app_category_index = (int)(p_preset->get("package/app_category")); bool is_game = app_category_index == APP_CATEGORY_GAME; - int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); - bool uses_xr = xr_mode_index == XR_MODE_OPENXR; - String manifest_application_text = vformat( " <application android:label=\"@string/godot_project_name_string\"\n" " android:allowBackup=\"%s\"\n" @@ -344,18 +323,18 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ bool_to_string(p_preset->get("package/retain_data_on_uninstall")), bool_to_string(p_has_read_write_storage_permission)); - if (uses_xr) { - bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE; - if (hand_tracking_enabled) { - int hand_tracking_frequency_index = p_preset->get("xr_features/hand_tracking_frequency"); - String hand_tracking_frequency = hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"; - manifest_application_text += vformat( - " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", - hand_tracking_frequency); - manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (export_plugins[i]->supports_platform(p_export_platform)) { + const String contents = export_plugins[i]->get_android_manifest_application_element_contents(p_export_platform, p_debug); + if (!contents.is_empty()) { + manifest_application_text += contents; + manifest_application_text += "\n"; + } } } - manifest_application_text += _get_activity_tag(p_preset, uses_xr); + + manifest_application_text += _get_activity_tag(p_export_platform, p_preset, p_debug); manifest_application_text += " </application>\n"; return manifest_application_text; } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 8a885a0d12..2498394add 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -61,20 +61,6 @@ static const int APP_CATEGORY_VIDEO = 8; static const int XR_MODE_REGULAR = 0; static const int XR_MODE_OPENXR = 1; -// Supported XR hand tracking modes. -static const int XR_HAND_TRACKING_NONE = 0; -static const int XR_HAND_TRACKING_OPTIONAL = 1; -static const int XR_HAND_TRACKING_REQUIRED = 2; - -// Supported XR hand tracking frequencies. -static const int XR_HAND_TRACKING_FREQUENCY_LOW = 0; -static const int XR_HAND_TRACKING_FREQUENCY_HIGH = 1; - -// Supported XR passthrough modes. -static const int XR_PASSTHROUGH_NONE = 0; -static const int XR_PASSTHROUGH_OPTIONAL = 1; -static const int XR_PASSTHROUGH_REQUIRED = 2; - struct CustomExportData { String assets_directory; bool debug; @@ -116,10 +102,8 @@ String _get_gles_tag(); String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset); -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan); - -String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr); +String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug); -String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission); +String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug); #endif // ANDROID_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/logo.svg b/platform/android/export/logo.svg index f154e55d11..f154e55d11 100644 --- a/platform/android/logo.svg +++ b/platform/android/export/logo.svg diff --git a/platform/android/run_icon.svg b/platform/android/export/run_icon.svg index 24d930fece..24d930fece 100644 --- a/platform/android/run_icon.svg +++ b/platform/android/export/run_icon.svg diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index b8f45628e5..3aa4ca98fc 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -32,6 +32,7 @@ #define FILE_ACCESS_ANDROID_H #include "core/io/file_access.h" + #include <android/asset_manager.h> #include <android/log.h> #include <stdio.h> @@ -75,8 +76,13 @@ public: virtual bool file_exists(const String &p_path) override; // return true if a file exists virtual uint64_t _get_modified_time(const String &p_file) override { return 0; } - virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + + virtual bool _get_hidden_attribute(const String &p_file) override { return false; } + virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } + virtual bool _get_read_only_attribute(const String &p_file) override { return false; } + virtual Error _set_read_only_attribute(const String &p_file, bool p_ro) override { return ERR_UNAVAILABLE; } virtual void close() override; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index ea8459d1ed..a1865fb1d4 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -30,9 +30,10 @@ #include "file_access_filesystem_jandroid.h" +#include "thread_jandroid.h" + #include "core/os/os.h" #include "core/templates/local_vector.h" -#include "thread_jandroid.h" #include <unistd.h> diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 5e74d9de24..0c3f8d7259 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -31,9 +31,10 @@ #ifndef FILE_ACCESS_FILESYSTEM_JANDROID_H #define FILE_ACCESS_FILESYSTEM_JANDROID_H -#include "core/io/file_access.h" #include "java_godot_lib_jni.h" +#include "core/io/file_access.h" + class FileAccessFilesystemJAndroid : public FileAccess { static jobject file_access_handler; static jclass cls; @@ -90,8 +91,13 @@ public: static void setup(jobject p_file_access_handler); virtual uint64_t _get_modified_time(const String &p_file) override; - virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + + virtual bool _get_hidden_attribute(const String &p_file) override { return false; } + virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } + virtual bool _get_read_only_attribute(const String &p_file) override { return false; } + virtual Error _set_read_only_attribute(const String &p_file, bool p_ro) override { return ERR_UNAVAILABLE; } virtual void close() override; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index ce4a2ecfe4..56d403a263 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -35,7 +35,7 @@ android:name=".GodotApp" android:label="@string/godot_project_name_string" android:theme="@style/GodotAppSplashTheme" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:excludeFromRecents="false" android:exported="true" android:screenOrientation="landscape" @@ -45,6 +45,7 @@ <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 4bac6c814a..e7c06628c8 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -135,14 +135,16 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys -> String statusValue = map["status"] if (statusValue == null) { statusCode = 0 - } else if (statusValue.startsWith("alpha")) { + } else if (statusValue.startsWith("dev")) { statusCode = 1 - } else if (statusValue.startsWith("beta")) { + } else if (statusValue.startsWith("alpha")) { statusCode = 2 - } else if (statusValue.startsWith("rc")) { + } else if (statusValue.startsWith("beta")) { statusCode = 3 - } else if (statusValue.startsWith("stable")) { + } else if (statusValue.startsWith("rc")) { statusCode = 4 + } else if (statusValue.startsWith("stable")) { + statusCode = 5 } else { statusCode = 0 } diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 1d2cc05715..9142d767b4 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -30,7 +30,7 @@ package com.godot.game; -import org.godotengine.godot.FullScreenGodotApp; +import org.godotengine.godot.GodotActivity; import android.os.Bundle; @@ -38,7 +38,7 @@ import android.os.Bundle; * Template activity for Godot Android builds. * Feel free to extend and modify this class for your custom logic. */ -public class GodotApp extends FullScreenGodotApp { +public class GodotApp extends GodotActivity { @Override public void onCreate(Bundle savedInstanceState) { setTheme(R.style.GodotAppMainTheme); diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 10c28a00b2..f94454e2a7 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath libraries.androidGradlePlugin classpath libraries.kotlinGradlePlugin - classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' } } @@ -38,9 +38,7 @@ ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedFlavors = ["editor", "template"] supportedFlavorsBuildTypes = [ - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - "editor": ["dev", "debug"], + "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] ] @@ -54,6 +52,7 @@ ext { def rootDir = "../../.." def binDir = "$rootDir/bin/" +def androidEditorBuildsDir = "$binDir/android_editor_builds/" def getSconsTaskName(String flavor, String buildType, String abi) { return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() @@ -221,18 +220,46 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } -task copyEditorDebugBinaryToBin(type: Copy) { +task copyEditorReleaseApkToBin(type: Copy) { + dependsOn ':editor:assembleRelease' + from('editor/build/outputs/apk/release') + into(androidEditorBuildsDir) + include('android_editor-release*.apk') +} + +task copyEditorReleaseAabToBin(type: Copy) { + dependsOn ':editor:bundleRelease' + from('editor/build/outputs/bundle/release') + into(androidEditorBuildsDir) + include('android_editor-release*.aab') +} + +task copyEditorDebugApkToBin(type: Copy) { dependsOn ':editor:assembleDebug' from('editor/build/outputs/apk/debug') - into(binDir) - include('android_editor.apk') + into(androidEditorBuildsDir) + include('android_editor-debug.apk') } -task copyEditorDevBinaryToBin(type: Copy) { +task copyEditorDebugAabToBin(type: Copy) { + dependsOn ':editor:bundleDebug' + from('editor/build/outputs/bundle/debug') + into(androidEditorBuildsDir) + include('android_editor-debug.aab') +} + +task copyEditorDevApkToBin(type: Copy) { dependsOn ':editor:assembleDev' from('editor/build/outputs/apk/dev') - into(binDir) - include('android_editor_dev.apk') + into(androidEditorBuildsDir) + include('android_editor-dev.apk') +} + +task copyEditorDevAabToBin(type: Copy) { + dependsOn ':editor:bundleDev' + from('editor/build/outputs/bundle/dev') + into(androidEditorBuildsDir) + include('android_editor-dev.aab') } /** @@ -253,7 +280,8 @@ task generateGodotEditor { && targetLibs.isDirectory() && targetLibs.listFiles() != null && targetLibs.listFiles().length > 0) { - tasks += "copyEditor${target.capitalize()}BinaryToBin" + tasks += "copyEditor${target.capitalize()}ApkToBin" + tasks += "copyEditor${target.capitalize()}AabToBin" } } @@ -301,9 +329,11 @@ task cleanGodotEditor(type: Delete) { // Delete the generated binary apks delete("editor/build/outputs/apk") - // Delete the Godot editor apks in the Godot bin directory - delete("$binDir/android_editor.apk") - delete("$binDir/android_editor_dev.apk") + // Delete the generated aab binaries + delete("editor/build/outputs/bundle") + + // Delete the Godot editor apks & aabs in the Godot bin directory + delete(androidEditorBuildsDir) } /** diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 9152492e9d..38034aa47c 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -13,22 +13,67 @@ dependencies { } ext { - // Build number added as a suffix to the version code, and incremented for each build/upload to - // the Google Play store. - // This should be reset on each stable release of Godot. - editorBuildNumber = 0 + // Retrieve the build number from the environment variable; default to 0 if none is specified. + // The build number is added as a suffix to the version code for upload to the Google Play store. + getEditorBuildNumber = { -> + int buildNumber = 0 + String versionStatus = System.getenv("GODOT_VERSION_STATUS") + if (versionStatus != null && !versionStatus.isEmpty()) { + try { + buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")); + } catch (NumberFormatException ignored) { + buildNumber = 0 + } + } + + return buildNumber + } // Value by which the Godot version code should be offset by to make room for the build number editorBuildNumberOffset = 100 + + // Return the keystore file used for signing the release build. + getGodotKeystoreFile = { -> + def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE") + if (keyStore == null) { + return null + } + return file(keyStore) + } + + // Return the key alias used for signing the release build. + getGodotKeyAlias = { -> + def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS") + return kAlias + } + + // Return the password for the key used for signing the release build. + getGodotSigningPassword = { -> + def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD") + return signingPassword + } + + // Returns true if the environment variables contains the configuration for signing the release + // build. + hasReleaseSigningConfigs = { -> + def keystoreFile = getGodotKeystoreFile() + def keyAlias = getGodotKeyAlias() + def signingPassword = getGodotSigningPassword() + + return keystoreFile != null && keystoreFile.isFile() + && keyAlias != null && !keyAlias.isEmpty() + && signingPassword != null && !signingPassword.isEmpty() + } } def generateVersionCode() { int libraryVersionCode = getGodotLibraryVersionCode() - return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber + return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber() } def generateVersionName() { String libraryVersionName = getGodotLibraryVersionName() - return libraryVersionName + ".$editorBuildNumber" + int buildNumber = getEditorBuildNumber() + return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber" } android { @@ -45,6 +90,7 @@ android { targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' + setProperty("archivesBaseName", "android_editor") } compileOptions { @@ -56,6 +102,15 @@ android { jvmTarget = versions.javaVersion } + signingConfigs { + release { + storeFile getGodotKeystoreFile() + storePassword getGodotSigningPassword() + keyAlias getGodotKeyAlias() + keyPassword getGodotSigningPassword() + } + } + buildTypes { dev { initWith debug @@ -64,15 +119,14 @@ android { debug { initWith release - - // Need to swap with the release signing config when this is ready for public release. + applicationIdSuffix ".debug" signingConfig signingConfigs.debug } release { - // This buildtype is disabled below. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. + if (hasReleaseSigningConfigs()) { + signingConfig signingConfigs.release + } } } @@ -82,20 +136,4 @@ android { doNotStrip '**/*.so' } } - - // Disable 'release' buildtype. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.buildType.name == "release") { - setIgnore(true) - } - } - - applicationVariants.all { variant -> - variant.outputs.all { output -> - def suffix = variant.name == "dev" ? "_dev" : "" - output.outputFileName = "android_editor${suffix}.apk" - } - } } diff --git a/platform/android/java/editor/src/debug/res/values/strings.xml b/platform/android/java/editor/src/debug/res/values/strings.xml new file mode 100644 index 0000000000..09ee2d77e1 --- /dev/null +++ b/platform/android/java/editor/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4 (debug)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 80ef10b6a4..cb89d6e1b0 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -21,6 +21,8 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.VIBRATE" /> <application android:allowBackup="false" @@ -33,7 +35,7 @@ <activity android:name=".GodotProjectManager" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:screenOrientation="userLandscape" android:exported="true" android:process=":GodotProjectManager"> @@ -43,6 +45,7 @@ <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @@ -51,7 +54,7 @@ android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:process=":GodotEditor" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:screenOrientation="userLandscape" android:exported="false"> <layout android:defaultHeight="@dimen/editor_default_window_height" @@ -63,7 +66,7 @@ android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:label="@string/godot_project_name_string" android:process=":GodotGame" - android:launchMode="singleTask" + android:launchMode="singleInstance" android:exported="false" android:screenOrientation="userLandscape"> <layout android:defaultHeight="@dimen/editor_default_window_height" diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 42ef1436f3..7cedfa6888 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -39,7 +39,7 @@ import android.os.* import android.util.Log import android.widget.Toast import androidx.window.layout.WindowMetricsCalculator -import org.godotengine.godot.FullScreenGodotApp +import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotLib import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix @@ -55,14 +55,14 @@ import kotlin.math.min * * It also plays the role of the primary editor window. */ -open class GodotEditor : FullScreenGodotApp() { +open class GodotEditor : GodotActivity() { companion object { private val TAG = GodotEditor::class.java.simpleName private const val WAIT_FOR_DEBUGGER = false - private const val COMMAND_LINE_PARAMS = "command_line_params" + private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" private const val EDITOR_ID = 777 private const val EDITOR_ARG = "--editor" @@ -76,14 +76,27 @@ open class GodotEditor : FullScreenGodotApp() { private const val PROJECT_MANAGER_ARG = "--project-manager" private const val PROJECT_MANAGER_ARG_SHORT = "-p" private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager" + + /** + * Sets of constants to specify the window to use to run the project. + * + * Should match the values in 'editor/editor_settings.cpp' for the + * 'run/window_placement/android_window' setting. + */ + private const val ANDROID_WINDOW_AUTO = 0 + private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1 + private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2 } private val commandLineParams = ArrayList<String>() override fun onCreate(savedInstanceState: Bundle?) { - PermissionsUtil.requestManifestPermissions(this) + // 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(COMMAND_LINE_PARAMS) + val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) + Log.d(TAG, "Received parameters ${params.contentToString()}") updateCommandLineParams(params) if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { @@ -98,21 +111,37 @@ open class GodotEditor : FullScreenGodotApp() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() + checkForProjectPermissionsToEnable() + runOnUiThread { // Enable long press, panning and scaling gestures - godotFragment?.renderView?.inputHandler?.apply { + godotFragment?.godot?.renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) } } } + /** + * Check for project permissions to enable + */ + protected open fun checkForProjectPermissionsToEnable() { + // Check for RECORD_AUDIO permission + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")); + if (audioInputEnabled) { + PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) + } + } + private fun updateCommandLineParams(args: Array<String>?) { // Update the list of command line params with the new args commandLineParams.clear() - if (args != null && args.isNotEmpty()) { + if (!args.isNullOrEmpty()) { commandLineParams.addAll(listOf(*args)) } + if (BuildConfig.BUILD_TYPE == "dev") { + commandLineParams.add("--benchmark") + } } override fun getCommandLine() = commandLineParams @@ -124,8 +153,7 @@ open class GodotEditor : FullScreenGodotApp() { // Whether we should launch the new godot instance in an adjacent window // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT - var launchAdjacent = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (isInMultiWindowMode || isLargeScreen) + var launchAdjacent = shouldGameLaunchAdjacent() for (arg in args) { if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) { @@ -146,33 +174,49 @@ open class GodotEditor : FullScreenGodotApp() { // Launch a new activity val newInstance = Intent(this, targetClass) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(COMMAND_LINE_PARAMS, args) + .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) if (launchAdjacent) { newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) } if (targetClass == javaClass) { - Log.d(TAG, "Restarting $targetClass") + Log.d(TAG, "Restarting $targetClass with parameters ${args.contentToString()}") ProcessPhoenix.triggerRebirth(this, newInstance) } else { - Log.d(TAG, "Starting $targetClass") + Log.d(TAG, "Starting $targetClass with parameters ${args.contentToString()}") + newInstance.putExtra(EXTRA_NEW_LAUNCH, true) startActivity(newInstance) } return instanceId } override fun onGodotForceQuit(godotInstanceId: Int): Boolean { - val processNameSuffix = when (godotInstanceId) { + val targetClass: Class<*>? + val processNameSuffix: String + when (godotInstanceId) { GAME_ID -> { - GAME_PROCESS_NAME_SUFFIX + processNameSuffix = GAME_PROCESS_NAME_SUFFIX + targetClass = GodotGame::class.java } EDITOR_ID -> { - EDITOR_PROCESS_NAME_SUFFIX + processNameSuffix = EDITOR_PROCESS_NAME_SUFFIX + targetClass = GodotEditor::class.java } PROJECT_MANAGER_ID -> { - PROJECT_MANAGER_PROCESS_NAME_SUFFIX + processNameSuffix = PROJECT_MANAGER_PROCESS_NAME_SUFFIX + targetClass = GodotProjectManager::class.java + } + else -> { + processNameSuffix = "" + targetClass = null } - else -> "" } + + if (targetClass == javaClass) { + Log.d(TAG, "Force quitting $targetClass") + ProcessPhoenix.forceQuit(this) + return true + } + if (processNameSuffix.isBlank()) { return false } @@ -181,8 +225,16 @@ open class GodotEditor : FullScreenGodotApp() { val runningProcesses = activityManager.runningAppProcesses for (runningProcess in runningProcesses) { if (runningProcess.processName.endsWith(processNameSuffix)) { - Log.v(TAG, "Killing Godot process ${runningProcess.processName}") - Process.killProcess(runningProcess.pid) + if (targetClass == null) { + // Killing process directly + Log.v(TAG, "Killing Godot process ${runningProcess.processName}") + Process.killProcess(runningProcess.pid) + } else { + // Activity is running; sending a request for self termination. + Log.v(TAG, "Sending force quit request to $targetClass running on process ${runningProcess.processName}") + val forceQuitIntent = Intent(this, targetClass).putExtra(EXTRA_FORCE_QUIT, true) + startActivity(forceQuitIntent) + } return true } } @@ -228,6 +280,26 @@ open class GodotEditor : FullScreenGodotApp() { protected open fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) + private fun shouldGameLaunchAdjacent(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { + ANDROID_WINDOW_SAME_AS_EDITOR -> false + ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true + else -> { + // ANDROID_WINDOW_AUTO + isInMultiWindowMode || isLargeScreen + } + } + } catch (e: NumberFormatException) { + // Fall-back to the 'Auto' behavior + isInMultiWindowMode || isLargeScreen + } + } else { + false + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // Check if we got the MANAGE_EXTERNAL_STORAGE permission @@ -246,7 +318,7 @@ open class GodotEditor : FullScreenGodotApp() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array<String?>, + permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 104af0fcff..aa4d02b5b2 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -39,4 +39,9 @@ class GodotGame : GodotEditor() { override fun enableLongPressGestures() = false override fun enablePanAndScaleGestures() = false + + override fun checkForProjectPermissionsToEnable() { + // Nothing to do.. by the time we get here, the project permissions will have already + // been requested by the Editor window. + } } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt index 62a9384f2e..d0e4279eeb 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -37,4 +37,9 @@ package org.godotengine.editor * Upon selection of a project, this activity (via its parent logic) starts the * [GodotEditor] activity. */ -class GodotProjectManager : GodotEditor() +class GodotProjectManager : GodotEditor() { + override fun checkForProjectPermissionsToEnable() { + // Nothing to do here.. we have yet to select a project to load. + } +} + diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 38133ddd51..4340250ad3 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -80,19 +80,11 @@ android { release.jniLibs.srcDirs = ['libs/release'] // Editor jni library + editorRelease.jniLibs.srcDirs = ['libs/tools/release'] editorDebug.jniLibs.srcDirs = ['libs/tools/debug'] editorDev.jniLibs.srcDirs = ['libs/tools/dev'] } - // Disable 'editorRelease'. - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.name == "editorRelease") { - setIgnore(true) - } - } - libraryVariants.all { variant -> def flavorName = variant.getFlavorName() if (flavorName == null || flavorName == "") { @@ -105,9 +97,14 @@ android { } boolean devBuild = buildType == "dev" + boolean runTests = devBuild + boolean productionBuild = !devBuild + boolean storeRelease = buildType == "release" def sconsTarget = flavorName if (sconsTarget == "template") { + // Tests are not supported on template builds + runTests = false switch (buildType) { case "release": sconsTarget += "_release" @@ -135,10 +132,10 @@ android { def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows() ? [".bat", ".cmd", ".ps1", ".exe"] : [""]) - logger.lifecycle("Looking for $sconsName executable path") + logger.debug("Looking for $sconsName executable path") for (ext in sconsExts) { String sconsNameExt = sconsName + ext - logger.lifecycle("Checking $sconsNameExt") + logger.debug("Checking $sconsNameExt") sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! @@ -155,7 +152,7 @@ android { if (sconsExecutableFile == null) { throw new GradleException("Unable to find executable path for the '$sconsName' command.") } else { - logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") + logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") } for (String selectedAbi : selectedAbis) { @@ -167,7 +164,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + 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() } // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index 677c9d8f13..91d272735e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -30,127 +30,10 @@ package org.godotengine.godot; -import org.godotengine.godot.utils.ProcessPhoenix; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; - /** - * Base activity for Android apps intending to use Godot as the primary and only screen. + * Base abstract activity for Android apps intending to use Godot as the primary screen. * - * It's also a reference implementation for how to setup and use the {@link Godot} fragment - * within an Android app. + * @deprecated Use {@link GodotActivity} */ -public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost { - private static final String TAG = FullScreenGodotApp.class.getSimpleName(); - - @Nullable - private Godot godotFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.godot_app_layout); - - Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container); - if (currentFragment instanceof Godot) { - Log.v(TAG, "Reusing existing Godot fragment instance."); - godotFragment = (Godot)currentFragment; - } else { - Log.v(TAG, "Creating new Godot fragment instance."); - godotFragment = initGodotInstance(); - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); - } - } - - @Override - public void onDestroy() { - Log.v(TAG, "Destroying Godot app..."); - super.onDestroy(); - terminateGodotInstance(godotFragment); - } - - @Override - public final void onGodotForceQuit(Godot instance) { - runOnUiThread(() -> { - terminateGodotInstance(instance); - }); - } - - private void terminateGodotInstance(Godot instance) { - if (instance == godotFragment) { - Log.v(TAG, "Force quitting Godot instance"); - ProcessPhoenix.forceQuit(FullScreenGodotApp.this); - } - } - - @Override - public final void onGodotRestartRequested(Godot instance) { - runOnUiThread(() -> { - if (instance == godotFragment) { - // It's very hard to properly de-initialize Godot on Android to restart the game - // from scratch. Therefore, we need to kill the whole app process and relaunch it. - // - // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including - // releasing and reloading native libs or resetting their state somehow and clearing static data). - Log.v(TAG, "Restarting Godot instance..."); - ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this); - } - }); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (godotFragment != null) { - godotFragment.onNewIntent(intent); - } - } - - @CallSuper - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (godotFragment != null) { - godotFragment.onActivityResult(requestCode, resultCode, data); - } - } - - @CallSuper - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (godotFragment != null) { - godotFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - @Override - public void onBackPressed() { - if (godotFragment != null) { - godotFragment.onBackPressed(); - } else { - super.onBackPressed(); - } - } - - /** - * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}. - */ - @NonNull - protected Godot initGodotInstance() { - return new Godot(); - } - - @Nullable - protected final Godot getGodotFragment() { - return godotFragment; - } -} +@Deprecated +public abstract class FullScreenGodotApp extends GodotActivity {} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java deleted file mode 100644 index 99527ccf3a..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ /dev/null @@ -1,1162 +0,0 @@ -/**************************************************************************/ -/* Godot.java */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -package org.godotengine.godot; - -import static android.content.Context.MODE_PRIVATE; -import static android.content.Context.WINDOW_SERVICE; - -import org.godotengine.godot.input.GodotEditText; -import org.godotengine.godot.io.directory.DirectoryAccessHandler; -import org.godotengine.godot.io.file.FileAccessHandler; -import org.godotengine.godot.plugin.GodotPlugin; -import org.godotengine.godot.plugin.GodotPluginRegistry; -import org.godotengine.godot.tts.GodotTTS; -import org.godotengine.godot.utils.GodotNetUtils; -import org.godotengine.godot.utils.PermissionsUtil; -import org.godotengine.godot.xr.XRMode; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.pm.ConfigurationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Messenger; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewTreeObserver; -import android.view.Window; -import android.view.WindowInsets; -import android.view.WindowInsetsAnimation; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.CallSuper; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import com.google.android.vending.expansion.downloader.DownloadProgressInfo; -import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; -import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; -import com.google.android.vending.expansion.downloader.Helpers; -import com.google.android.vending.expansion.downloader.IDownloaderClient; -import com.google.android.vending.expansion.downloader.IDownloaderService; -import com.google.android.vending.expansion.downloader.IStub; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -public class Godot extends Fragment implements SensorEventListener, IDownloaderClient { - private static final String TAG = Godot.class.getSimpleName(); - - private IStub mDownloaderClientStub; - private TextView mStatusText; - private TextView mProgressFraction; - private TextView mProgressPercent; - private TextView mAverageSpeed; - private TextView mTimeRemaining; - private ProgressBar mPB; - private ClipboardManager mClipboard; - - private View mDashboard; - private View mCellMessage; - - private Button mPauseButton; - private Button mWiFiSettingsButton; - - private XRMode xrMode = XRMode.REGULAR; - private boolean use_immersive = false; - private boolean use_debug_opengl = false; - private boolean mStatePaused; - private boolean activityResumed; - private int mState; - - private GodotHost godotHost; - private GodotPluginRegistry pluginRegistry; - - static private Intent mCurrentIntent; - - public void onNewIntent(Intent intent) { - mCurrentIntent = intent; - } - - static public Intent getCurrentIntent() { - return mCurrentIntent; - } - - private void setState(int newState) { - if (mState != newState) { - mState = newState; - mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState)); - } - } - - private void setButtonPausedState(boolean paused) { - mStatePaused = paused; - int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; - mPauseButton.setText(stringResourceID); - } - - private String[] command_line; - private boolean use_apk_expansion; - - private ViewGroup containerLayout; - public GodotRenderView mRenderView; - private boolean godot_initialized = false; - - private SensorManager mSensorManager; - private Sensor mAccelerometer; - private Sensor mGravity; - private Sensor mMagnetometer; - private Sensor mGyroscope; - - public GodotIO io; - public GodotNetUtils netUtils; - public GodotTTS tts; - DirectoryAccessHandler directoryAccessHandler; - - public interface ResultCallback { - void callback(int requestCode, int resultCode, Intent data); - } - public ResultCallback result_callback; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (getParentFragment() instanceof GodotHost) { - godotHost = (GodotHost)getParentFragment(); - } else if (getActivity() instanceof GodotHost) { - godotHost = (GodotHost)getActivity(); - } - } - - @Override - public void onDetach() { - super.onDetach(); - godotHost = null; - } - - @CallSuper - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (result_callback != null) { - result_callback.callback(requestCode, resultCode, data); - result_callback = null; - } - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainActivityResult(requestCode, resultCode, data); - } - } - - @CallSuper - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults); - } - - for (int i = 0; i < permissions.length; i++) { - GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED); - } - } - - /** - * Invoked on the render thread when the Godot setup is complete. - */ - @CallSuper - protected void onGodotSetupCompleted() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onGodotSetupCompleted(); - } - - if (godotHost != null) { - godotHost.onGodotSetupCompleted(); - } - } - - /** - * Invoked on the render thread when the Godot main loop has started. - */ - @CallSuper - protected void onGodotMainLoopStarted() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onGodotMainLoopStarted(); - } - - if (godotHost != null) { - godotHost.onGodotMainLoopStarted(); - } - } - - /** - * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer. - */ - @Keep - private boolean onVideoInit() { - final Activity activity = requireActivity(); - containerLayout = new FrameLayout(activity); - containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - - // GodotEditText layout - GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - (int)getResources().getDimension(R.dimen.text_edit_height))); - // ...add to FrameLayout - containerLayout.addView(editText); - - if (!GodotLib.setup(command_line)) { - Log.e(TAG, "Unable to setup the Godot engine! Aborting..."); - alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit); - return false; - } - - if (usesVulkan()) { - if (!meetsVulkanRequirements(activity.getPackageManager())) { - alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit); - return false; - } - mRenderView = new GodotVulkanRenderView(activity, this); - } else { - // Fallback to openGl - mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl); - } - - View view = mRenderView.getView(); - containerLayout.addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - editText.setView(mRenderView); - io.setEdit(editText); - - // Listeners for keyboard height. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Report the height of virtual keyboard as it changes during the animation. - final View decorView = activity.getWindow().getDecorView(); - decorView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) { - int startBottom, endBottom; - @Override - public void onPrepare(@NonNull WindowInsetsAnimation animation) { - startBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; - } - - @NonNull - @Override - public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) { - endBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom; - return bounds; - } - - @NonNull - @Override - public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) { - // Find the IME animation. - WindowInsetsAnimation imeAnimation = null; - for (WindowInsetsAnimation animation : list) { - if ((animation.getTypeMask() & WindowInsets.Type.ime()) != 0) { - imeAnimation = animation; - break; - } - } - // Update keyboard height based on IME animation. - if (imeAnimation != null) { - float interpolatedFraction = imeAnimation.getInterpolatedFraction(); - // Linear interpolation between start and end values. - float keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction; - GodotLib.setVirtualKeyboardHeight((int)keyboardHeight); - } - return windowInsets; - } - - @Override - public void onEnd(@NonNull WindowInsetsAnimation animation) { - } - }); - } else { - // Infer the virtual keyboard height using visible area. - view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - // Don't allocate a new Rect every time the callback is called. - final Rect visibleSize = new Rect(); - - @Override - public void onGlobalLayout() { - final SurfaceView view = mRenderView.getView(); - view.getWindowVisibleDisplayFrame(visibleSize); - final int keyboardHeight = view.getHeight() - visibleSize.bottom; - GodotLib.setVirtualKeyboardHeight(keyboardHeight); - } - }); - } - - mRenderView.queueOnRenderThread(() -> { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onRegisterPluginWithGodotNative(); - } - setKeepScreenOn(Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))); - }); - - // Include the returned non-null views in the Godot view hierarchy. - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - View pluginView = plugin.onMainCreate(activity); - if (pluginView != null) { - if (plugin.shouldBeOnTop()) { - containerLayout.addView(pluginView); - } else { - containerLayout.addView(pluginView, 0); - } - } - } - return true; - } - - /** - * Returns true if `Vulkan` is used for rendering. - */ - private boolean usesVulkan() { - final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method"); - final String renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver"); - return ("forward_plus".equals(renderer) || "mobile".equals(renderer)) && "vulkan".equals(renderingDevice); - } - - /** - * Returns true if the device meets the base requirements for Vulkan support, false otherwise. - */ - private boolean meetsVulkanRequirements(@Nullable PackageManager packageManager) { - if (packageManager == null) { - return false; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { - // Optional requirements.. log as warning if missing - Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1"); - } - - // Check for api version 1.0 - return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003); - } - - return false; - } - - public void setKeepScreenOn(final boolean p_enabled) { - runOnUiThread(() -> { - if (p_enabled) { - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - }); - } - - /** - * Used by the native code (java_godot_wrapper.h) to vibrate the device. - * @param durationMs - */ - @SuppressLint("MissingPermission") - @Keep - private void vibrate(int durationMs) { - if (durationMs > 0 && requestPermission("VIBRATE")) { - Vibrator v = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)); - } else { - // deprecated in API 26 - v.vibrate(durationMs); - } - } - } - } - - public void restart() { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } - } - - public void alert(final String message, final String title) { - alert(message, title, null); - } - - private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) { - Resources res = getResources(); - alert(res.getString(messageResId), res.getString(titleResId), okCallback); - } - - private void alert(final String message, final String title, @Nullable Runnable okCallback) { - final Activity activity = getActivity(); - runOnUiThread(() -> { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(message).setTitle(title); - builder.setPositiveButton( - "OK", - (dialog, id) -> { - if (okCallback != null) { - okCallback.run(); - } - dialog.cancel(); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - }); - } - - public int getGLESVersionCode() { - ActivityManager am = (ActivityManager)getContext().getSystemService(Context.ACTIVITY_SERVICE); - ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo(); - return deviceInfo.reqGlEsVersion; - } - - @CallSuper - protected String[] getCommandLine() { - String[] original = parseCommandLine(); - String[] updated; - List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null; - if (hostCommandLine == null || hostCommandLine.isEmpty()) { - updated = original; - } else { - updated = Arrays.copyOf(original, original.length + hostCommandLine.size()); - for (int i = 0; i < hostCommandLine.size(); i++) { - updated[original.length + i] = hostCommandLine.get(i); - } - } - return updated; - } - - private String[] parseCommandLine() { - InputStream is; - try { - is = getActivity().getAssets().open("_cl_"); - byte[] len = new byte[4]; - int r = is.read(len); - if (r < 4) { - return new String[0]; - } - int argc = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); - String[] cmdline = new String[argc]; - - for (int i = 0; i < argc; i++) { - r = is.read(len); - if (r < 4) { - return new String[0]; - } - int strlen = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF)); - if (strlen > 65535) { - return new String[0]; - } - byte[] arg = new byte[strlen]; - r = is.read(arg); - if (r == strlen) { - cmdline[i] = new String(arg, "UTF-8"); - } - } - return cmdline; - } catch (Exception e) { - e.printStackTrace(); - return new String[0]; - } - } - - /** - * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused. - */ - @Keep - private boolean isActivityResumed() { - return activityResumed; - } - - /** - * Used by the native code (java_godot_wrapper.h) to access the Android surface. - */ - @Keep - private Surface getSurface() { - return mRenderView.getView().getHolder().getSurface(); - } - - /** - * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. - * @return The input fallback mapping for the current XR mode. - */ - @Keep - private String getInputFallbackMapping() { - return xrMode.inputFallbackMapping; - } - - String expansion_pack_path; - - private void initializeGodot() { - if (expansion_pack_path != null) { - String[] new_cmdline; - int cll = 0; - if (command_line != null) { - new_cmdline = new String[command_line.length + 2]; - cll = command_line.length; - for (int i = 0; i < command_line.length; i++) { - new_cmdline[i] = command_line[i]; - } - } else { - new_cmdline = new String[2]; - } - - new_cmdline[cll] = "--main-pack"; - new_cmdline[cll + 1] = expansion_pack_path; - command_line = new_cmdline; - } - - final Activity activity = getActivity(); - io = new GodotIO(activity); - netUtils = new GodotNetUtils(activity); - tts = new GodotTTS(activity); - Context context = getContext(); - directoryAccessHandler = new DirectoryAccessHandler(context); - FileAccessHandler fileAccessHandler = new FileAccessHandler(context); - mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); - mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); - mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - - godot_initialized = GodotLib.initialize(activity, - this, - activity.getAssets(), - io, - netUtils, - directoryAccessHandler, - fileAccessHandler, - use_apk_expansion, - tts); - - result_callback = null; - } - - @Override - public void onServiceConnected(Messenger m) { - IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); - remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - final Activity activity = getActivity(); - Window window = activity.getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); - pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); - - // check for apk expansion API - boolean md5mismatch = false; - command_line = getCommandLine(); - String main_pack_md5 = null; - String main_pack_key = null; - - List<String> new_args = new LinkedList<>(); - - for (int i = 0; i < command_line.length; i++) { - boolean has_extra = i < command_line.length - 1; - if (command_line[i].equals(XRMode.REGULAR.cmdLineArg)) { - xrMode = XRMode.REGULAR; - } else if (command_line[i].equals(XRMode.OPENXR.cmdLineArg)) { - xrMode = XRMode.OPENXR; - } else if (command_line[i].equals("--debug_opengl")) { - use_debug_opengl = true; - } else if (command_line[i].equals("--use_immersive")) { - use_immersive = true; - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - UiChangeListener(); - } else if (command_line[i].equals("--use_apk_expansion")) { - use_apk_expansion = true; - } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { - main_pack_md5 = command_line[i + 1]; - i++; - } else if (has_extra && command_line[i].equals("--apk_expansion_key")) { - main_pack_key = command_line[i + 1]; - SharedPreferences prefs = activity.getSharedPreferences("app_data_keys", - MODE_PRIVATE); - Editor editor = prefs.edit(); - editor.putString("store_public_key", main_pack_key); - - editor.apply(); - i++; - } else if (command_line[i].trim().length() != 0) { - new_args.add(command_line[i]); - } - } - - if (new_args.isEmpty()) { - command_line = null; - } else { - command_line = new_args.toArray(new String[new_args.size()]); - } - if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) { - // check that environment is ok! - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - // show popup and die - } - - // Build the full path to the app's expansion files - try { - expansion_pack_path = Helpers.getSaveFilePath(getContext()); - expansion_pack_path += "/main." + activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode + "." + activity.getPackageName() + ".obb"; - } catch (Exception e) { - e.printStackTrace(); - } - - File f = new File(expansion_pack_path); - - boolean pack_valid = true; - - if (!f.exists()) { - pack_valid = false; - - } else if (obbIsCorrupted(expansion_pack_path, main_pack_md5)) { - pack_valid = false; - try { - f.delete(); - } catch (Exception e) { - } - } - - if (!pack_valid) { - Intent notifierIntent = new Intent(activity, activity.getClass()); - notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - - PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, - notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - int startResult; - try { - startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired( - getContext(), - pendingIntent, - GodotDownloaderService.class); - - if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { - // This is where you do set up to display the download - // progress (next step in onCreateView) - mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, - GodotDownloaderService.class); - - return; - } - } catch (NameNotFoundException e) { - // TODO Auto-generated catch block - } - } - } - - mCurrentIntent = activity.getIntent(); - - initializeGodot(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { - if (mDownloaderClientStub != null) { - View downloadingExpansionView = - inflater.inflate(R.layout.downloading_expansion, container, false); - mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar); - mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText); - mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction); - mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage); - mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed); - mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining); - mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard); - mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular); - mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton); - mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton); - - return downloadingExpansionView; - } - - return containerLayout; - } - - @Override - public void onDestroy() { - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainDestroy(); - } - - GodotLib.ondestroy(); - - super.onDestroy(); - - forceQuit(); - } - - @Override - public void onPause() { - super.onPause(); - activityResumed = false; - - if (!godot_initialized) { - if (null != mDownloaderClientStub) { - mDownloaderClientStub.disconnect(getActivity()); - } - return; - } - mRenderView.onActivityPaused(); - - mSensorManager.unregisterListener(this); - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainPause(); - } - } - - public boolean hasClipboard() { - return mClipboard.hasPrimaryClip(); - } - - public String getClipboard() { - ClipData clipData = mClipboard.getPrimaryClip(); - if (clipData == null) - return ""; - CharSequence text = clipData.getItemAt(0).getText(); - if (text == null) - return ""; - return text.toString(); - } - - public void setClipboard(String p_text) { - ClipData clip = ClipData.newPlainText("myLabel", p_text); - mClipboard.setPrimaryClip(clip); - } - - @Override - public void onResume() { - super.onResume(); - activityResumed = true; - if (!godot_initialized) { - if (null != mDownloaderClientStub) { - mDownloaderClientStub.connect(getActivity()); - } - return; - } - - mRenderView.onActivityResumed(); - - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - - if (use_immersive) { - Window window = getActivity().getWindow(); - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - plugin.onMainResume(); - } - } - - public void UiChangeListener() { - final View decorView = getActivity().getWindow().getDecorView(); - decorView.setOnSystemUiVisibilityChangeListener(visibility -> { - if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } - }); - } - - public float[] getRotatedValues(float values[]) { - if (values == null || values.length != 3) { - return values; - } - - Display display = - ((WindowManager)getActivity().getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); - int displayRotation = display.getRotation(); - - float[] rotatedValues = new float[3]; - switch (displayRotation) { - case Surface.ROTATION_0: - rotatedValues[0] = values[0]; - rotatedValues[1] = values[1]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_90: - rotatedValues[0] = -values[1]; - rotatedValues[1] = values[0]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_180: - rotatedValues[0] = -values[0]; - rotatedValues[1] = -values[1]; - rotatedValues[2] = values[2]; - break; - case Surface.ROTATION_270: - rotatedValues[0] = values[1]; - rotatedValues[1] = -values[0]; - rotatedValues[2] = values[2]; - break; - } - - return rotatedValues; - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (mRenderView == null) { - return; - } - - final int typeOfSensor = event.sensor.getType(); - switch (typeOfSensor) { - case Sensor.TYPE_ACCELEROMETER: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.accelerometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_GRAVITY: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.gravity(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_MAGNETIC_FIELD: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.magnetometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]); - }); - break; - } - case Sensor.TYPE_GYROSCOPE: { - float[] rotatedValues = getRotatedValues(event.values); - mRenderView.queueOnRenderThread(() -> { - GodotLib.gyroscope(rotatedValues[0], rotatedValues[1], rotatedValues[2]); - }); - break; - } - } - } - - @Override - public final void onAccuracyChanged(Sensor sensor, int accuracy) { - // Do something here if sensor accuracy changes. - } - - /* - @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) { - System.out.printf("** BACK REQUEST!\n"); - - GodotLib.quit(); - return true; - } - System.out.printf("** OTHER KEY!\n"); - - return false; - } - */ - - public void onBackPressed() { - boolean shouldQuit = true; - - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { - if (plugin.onMainBackPressed()) { - shouldQuit = false; - } - } - - if (shouldQuit && mRenderView != null) { - mRenderView.queueOnRenderThread(GodotLib::back); - } - } - - /** - * Queue a runnable to be run on the render thread. - * <p> - * This must be called after the render thread has started. - */ - public final void runOnRenderThread(@NonNull Runnable action) { - if (mRenderView != null) { - mRenderView.queueOnRenderThread(action); - } - } - - public final void runOnUiThread(@NonNull Runnable action) { - if (getActivity() != null) { - getActivity().runOnUiThread(action); - } - } - - private void forceQuit() { - // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each - // native Godot components that is started in Godot#onVideoInit. - forceQuit(0); - } - - @Keep - private boolean forceQuit(int instanceId) { - if (godotHost == null) { - return false; - } - if (instanceId == 0) { - godotHost.onGodotForceQuit(this); - return true; - } else { - return godotHost.onGodotForceQuit(instanceId); - } - } - - private boolean obbIsCorrupted(String f, String main_pack_md5) { - try { - InputStream fis = new FileInputStream(f); - - // Create MD5 Hash - byte[] buffer = new byte[16384]; - - MessageDigest complete = MessageDigest.getInstance("MD5"); - int numRead; - do { - numRead = fis.read(buffer); - if (numRead > 0) { - complete.update(buffer, 0, numRead); - } - } while (numRead != -1); - - fis.close(); - byte[] messageDigest = complete.digest(); - - // Create Hex String - StringBuilder hexString = new StringBuilder(); - for (byte b : messageDigest) { - String s = Integer.toHexString(0xFF & b); - if (s.length() == 1) { - s = "0" + s; - } - hexString.append(s); - } - String md5str = hexString.toString(); - - if (!md5str.equals(main_pack_md5)) { - return true; - } - return false; - } catch (Exception e) { - e.printStackTrace(); - return true; - } - } - - public boolean requestPermission(String p_name) { - return PermissionsUtil.requestPermission(p_name, getActivity()); - } - - public boolean requestPermissions() { - return PermissionsUtil.requestManifestPermissions(getActivity()); - } - - public String[] getGrantedPermissions() { - return PermissionsUtil.getGrantedPermissions(getActivity()); - } - - @Keep - private String getCACertificates() { - return GodotNetUtils.getCACertificates(); - } - - /** - * The download state should trigger changes in the UI --- it may be useful - * to show the state as being indeterminate at times. This sample can be - * considered a guideline. - */ - @Override - public void onDownloadStateChanged(int newState) { - setState(newState); - boolean showDashboard = true; - boolean showCellMessage = false; - boolean paused; - boolean indeterminate; - switch (newState) { - case IDownloaderClient.STATE_IDLE: - // STATE_IDLE means the service is listening, so it's - // safe to start making remote service calls. - paused = false; - indeterminate = true; - break; - case IDownloaderClient.STATE_CONNECTING: - case IDownloaderClient.STATE_FETCHING_URL: - showDashboard = true; - paused = false; - indeterminate = true; - break; - case IDownloaderClient.STATE_DOWNLOADING: - paused = false; - showDashboard = true; - indeterminate = false; - break; - - case IDownloaderClient.STATE_FAILED_CANCELED: - case IDownloaderClient.STATE_FAILED: - case IDownloaderClient.STATE_FAILED_FETCHING_URL: - case IDownloaderClient.STATE_FAILED_UNLICENSED: - paused = true; - showDashboard = false; - indeterminate = false; - break; - case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: - case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: - showDashboard = false; - paused = true; - indeterminate = false; - showCellMessage = true; - break; - - case IDownloaderClient.STATE_PAUSED_BY_REQUEST: - paused = true; - indeterminate = false; - break; - case IDownloaderClient.STATE_PAUSED_ROAMING: - case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: - paused = true; - indeterminate = false; - break; - case IDownloaderClient.STATE_COMPLETED: - showDashboard = false; - paused = false; - indeterminate = false; - initializeGodot(); - return; - default: - paused = true; - indeterminate = true; - showDashboard = true; - } - int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE; - if (mDashboard.getVisibility() != newDashboardVisibility) { - mDashboard.setVisibility(newDashboardVisibility); - } - int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE; - if (mCellMessage.getVisibility() != cellMessageVisibility) { - mCellMessage.setVisibility(cellMessageVisibility); - } - - mPB.setIndeterminate(indeterminate); - setButtonPausedState(paused); - } - - @Override - public void onDownloadProgress(DownloadProgressInfo progress) { - mAverageSpeed.setText(getString(R.string.kilobytes_per_second, - Helpers.getSpeedString(progress.mCurrentSpeed))); - mTimeRemaining.setText(getString(R.string.time_remaining, - Helpers.getTimeRemaining(progress.mTimeRemaining))); - - mPB.setMax((int)(progress.mOverallTotal >> 8)); - mPB.setProgress((int)(progress.mOverallProgress >> 8)); - mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); - mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, - progress.mOverallTotal)); - } - - public void initInputDevices() { - mRenderView.initInputDevices(); - } - - @Keep - public GodotRenderView getRenderView() { // used by native side to get renderView - return mRenderView; - } - - @Keep - private int createNewGodotInstance(String[] args) { - if (godotHost != null) { - return godotHost.onNewGodotInstanceRequested(args); - } - return 0; - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt new file mode 100644 index 0000000000..9c1165bf8a --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -0,0 +1,973 @@ +/**************************************************************************/ +/* Godot.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.content.* +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Rect +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.* +import android.util.Log +import android.view.* +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.FrameLayout +import androidx.annotation.Keep +import androidx.annotation.StringRes +import com.google.android.vending.expansion.downloader.* +import org.godotengine.godot.input.GodotEditText +import org.godotengine.godot.io.directory.DirectoryAccessHandler +import org.godotengine.godot.io.file.FileAccessHandler +import org.godotengine.godot.plugin.GodotPluginRegistry +import org.godotengine.godot.tts.GodotTTS +import org.godotengine.godot.utils.GodotNetUtils +import org.godotengine.godot.utils.PermissionsUtil +import org.godotengine.godot.utils.PermissionsUtil.requestPermission +import org.godotengine.godot.utils.beginBenchmarkMeasure +import org.godotengine.godot.utils.benchmarkFile +import org.godotengine.godot.utils.dumpBenchmark +import org.godotengine.godot.utils.endBenchmarkMeasure +import org.godotengine.godot.utils.useBenchmark +import org.godotengine.godot.xr.XRMode +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.* + +/** + * Core component used to interface with the native layer of the engine. + * + * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its + * lifecycle methods are properly invoked. + */ +class Godot(private val context: Context) : SensorEventListener { + + private companion object { + private val TAG = Godot::class.java.simpleName + } + + private val pluginRegistry: GodotPluginRegistry by lazy { + GodotPluginRegistry.initializePluginRegistry(this) + } + private val mSensorManager: SensorManager by lazy { + requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager + } + private val mAccelerometer: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + } + private val mGravity: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + } + private val mMagnetometer: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + } + private val mGyroscope: Sensor? by lazy { + mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + } + private val mClipboard: ClipboardManager by lazy { + requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } + + private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> + if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { + val decorView = requireActivity().window.decorView + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + }} + + val tts = GodotTTS(context) + val directoryAccessHandler = DirectoryAccessHandler(context) + val fileAccessHandler = FileAccessHandler(context) + val netUtils = GodotNetUtils(context) + + /** + * Tracks whether [onCreate] was completed successfully. + */ + private var initializationStarted = false + + /** + * Tracks whether [GodotLib.initialize] was completed successfully. + */ + private var nativeLayerInitializeCompleted = false + + /** + * Tracks whether [GodotLib.setup] was completed successfully. + */ + private var nativeLayerSetupCompleted = false + + /** + * Tracks whether [onInitRenderView] was completed successfully. + */ + private var renderViewInitialized = false + private var primaryHost: GodotHost? = null + + var io: GodotIO? = null + + private var commandLine : MutableList<String> = ArrayList<String>() + private var xrMode = XRMode.REGULAR + private var expansionPackPath: String = "" + private var useApkExpansion = false + private var useImmersive = false + private var useDebugOpengl = false + + private var containerLayout: FrameLayout? = null + var renderView: GodotRenderView? = null + + /** + * Returns true if the native engine has been initialized through [onInitNativeLayer], false otherwise. + */ + private fun isNativeInitialized() = nativeLayerInitializeCompleted && nativeLayerSetupCompleted + + /** + * Returns true if the engine has been initialized, false otherwise. + */ + fun isInitialized() = initializationStarted && isNativeInitialized() && renderViewInitialized + + /** + * Provides access to the primary host [Activity] + */ + fun getActivity() = primaryHost?.activity + private fun requireActivity() = getActivity() ?: throw IllegalStateException("Host activity must be non-null") + + /** + * Start initialization of the Godot engine. + * + * This must be followed by [onInitNativeLayer] and [onInitRenderView] in that order to complete + * initialization of the engine. + * + * @throws IllegalArgumentException exception if the specified expansion pack (if any) + * is invalid. + */ + fun onCreate(primaryHost: GodotHost) { + if (this.primaryHost != null || initializationStarted) { + Log.d(TAG, "OnCreate already invoked") + return + } + + beginBenchmarkMeasure("Godot::onCreate") + try { + this.primaryHost = primaryHost + val activity = requireActivity() + val window = activity.window + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + GodotPluginRegistry.initializePluginRegistry(this) + if (io == null) { + io = GodotIO(activity) + } + + // check for apk expansion API + commandLine = getCommandLine() + var mainPackMd5: String? = null + var mainPackKey: String? = null + val newArgs: MutableList<String> = ArrayList() + var i = 0 + while (i < commandLine.size) { + val hasExtra: Boolean = i < commandLine.size - 1 + if (commandLine[i] == XRMode.REGULAR.cmdLineArg) { + xrMode = XRMode.REGULAR + } else if (commandLine[i] == XRMode.OPENXR.cmdLineArg) { + xrMode = XRMode.OPENXR + } else if (commandLine[i] == "--debug_opengl") { + useDebugOpengl = true + } else if (commandLine[i] == "--use_immersive") { + useImmersive = true + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + registerUiChangeListener() + } else if (commandLine[i] == "--use_apk_expansion") { + useApkExpansion = true + } else if (hasExtra && commandLine[i] == "--apk_expansion_md5") { + mainPackMd5 = commandLine[i + 1] + i++ + } else if (hasExtra && commandLine[i] == "--apk_expansion_key") { + mainPackKey = commandLine[i + 1] + val prefs = activity.getSharedPreferences( + "app_data_keys", + Context.MODE_PRIVATE + ) + val editor = prefs.edit() + editor.putString("store_public_key", mainPackKey) + editor.apply() + i++ + } else if (commandLine[i] == "--benchmark") { + useBenchmark = true + newArgs.add(commandLine[i]) + } else if (hasExtra && commandLine[i] == "--benchmark-file") { + useBenchmark = true + newArgs.add(commandLine[i]) + + // Retrieve the filepath + benchmarkFile = commandLine[i + 1] + newArgs.add(commandLine[i + 1]) + + i++ + } else if (commandLine[i].trim().isNotEmpty()) { + newArgs.add(commandLine[i]) + } + i++ + } + if (newArgs.isEmpty()) { + commandLine = mutableListOf() + } else { + commandLine = newArgs + } + if (useApkExpansion && mainPackMd5 != null && mainPackKey != null) { + // Build the full path to the app's expansion files + try { + expansionPackPath = Helpers.getSaveFilePath(context) + expansionPackPath += "/main." + activity.packageManager.getPackageInfo( + activity.packageName, + 0 + ).versionCode + "." + activity.packageName + ".obb" + } catch (e: java.lang.Exception) { + Log.e(TAG, "Unable to build full path to the app's expansion files", e) + } + val f = File(expansionPackPath) + var packValid = true + if (!f.exists()) { + packValid = false + } else if (obbIsCorrupted(expansionPackPath, mainPackMd5)) { + packValid = false + try { + f.delete() + } catch (_: java.lang.Exception) { + } + } + if (!packValid) { + // Aborting engine initialization + throw IllegalArgumentException("Invalid expansion pack") + } + } + + initializationStarted = true + } catch (e: java.lang.Exception) { + // Clear the primary host and rethrow + this.primaryHost = null + initializationStarted = false + throw e + } finally { + endBenchmarkMeasure("Godot::onCreate"); + } + } + + /** + * Initializes the native layer of the Godot engine. + * + * This must be preceded by [onCreate] and followed by [onInitRenderView] to complete + * initialization of the engine. + * + * @return false if initialization of the native layer fails, true otherwise. + * + * @throws IllegalStateException if [onCreate] has not been called. + */ + fun onInitNativeLayer(host: GodotHost): Boolean { + if (!initializationStarted) { + throw IllegalStateException("OnCreate must be invoked successfully prior to initializing the native layer") + } + if (isNativeInitialized()) { + Log.d(TAG, "OnInitNativeLayer already invoked") + return true + } + if (host != primaryHost) { + Log.e(TAG, "Native initialization is only supported for the primary host") + return false + } + + if (expansionPackPath.isNotEmpty()) { + commandLine.add("--main-pack") + commandLine.add(expansionPackPath) + } + val activity = requireActivity() + if (!nativeLayerInitializeCompleted) { + nativeLayerInitializeCompleted = GodotLib.initialize( + activity, + this, + activity.assets, + io, + netUtils, + directoryAccessHandler, + fileAccessHandler, + useApkExpansion, + ) + } + + if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { + nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) + if (!nativeLayerSetupCompleted) { + Log.e(TAG, "Unable to setup the Godot engine! Aborting...") + alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit) + } + } + return isNativeInitialized() + } + + /** + * Used to complete initialization of the view used by the engine for rendering. + * + * This must be preceded by [onCreate] and [onInitNativeLayer] in that order to properly + * initialize the engine. + * + * @param host The [GodotHost] that's initializing the render views + * @param providedContainerLayout Optional argument; if provided, this is reused to host the Godot's render views + * + * @return A [FrameLayout] instance containing Godot's render views if initialization is successful, null otherwise. + * + * @throws IllegalStateException if [onInitNativeLayer] has not been called + */ + @JvmOverloads + fun onInitRenderView(host: GodotHost, providedContainerLayout: FrameLayout = FrameLayout(host.activity)): FrameLayout? { + if (!isNativeInitialized()) { + throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view") + } + + try { + val activity: Activity = host.activity + containerLayout = providedContainerLayout + containerLayout?.removeAllViews() + containerLayout?.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // GodotEditText layout + val editText = GodotEditText(activity) + editText.layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + activity.resources.getDimension(R.dimen.text_edit_height).toInt() + ) + // ...add to FrameLayout + containerLayout?.addView(editText) + renderView = if (usesVulkan()) { + if (!meetsVulkanRequirements(activity.packageManager)) { + alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit) + return null + } + GodotVulkanRenderView(host, this) + } else { + // Fallback to openGl + GodotGLRenderView(host, this, xrMode, useDebugOpengl) + } + if (host == primaryHost) { + renderView!!.startRenderer() + } + val view: View = renderView!!.view + containerLayout?.addView( + view, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + editText.setView(renderView) + io?.setEdit(editText) + + // Listeners for keyboard height. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Report the height of virtual keyboard as it changes during the animation. + val decorView = activity.window.decorView + decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + var startBottom = 0 + var endBottom = 0 + override fun onPrepare(animation: WindowInsetsAnimation) { + startBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom + } + + override fun onStart(animation: WindowInsetsAnimation, bounds: WindowInsetsAnimation.Bounds): WindowInsetsAnimation.Bounds { + endBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom + return bounds + } + + override fun onProgress(windowInsets: WindowInsets, list: List<WindowInsetsAnimation>): WindowInsets { + // Find the IME animation. + var imeAnimation: WindowInsetsAnimation? = null + for (animation in list) { + if (animation.typeMask and WindowInsets.Type.ime() != 0) { + imeAnimation = animation + break + } + } + // Update keyboard height based on IME animation. + if (imeAnimation != null) { + val interpolatedFraction = imeAnimation.interpolatedFraction + // Linear interpolation between start and end values. + val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction + GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt()) + } + return windowInsets + } + + override fun onEnd(animation: WindowInsetsAnimation) {} + }) + } else { + // Infer the virtual keyboard height using visible area. + view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + // Don't allocate a new Rect every time the callback is called. + val visibleSize = Rect() + override fun onGlobalLayout() { + val surfaceView = renderView!!.view + surfaceView.getWindowVisibleDisplayFrame(visibleSize) + val keyboardHeight = surfaceView.height - visibleSize.bottom + GodotLib.setVirtualKeyboardHeight(keyboardHeight) + } + }) + } + + if (host == primaryHost) { + renderView!!.queueOnRenderThread { + for (plugin in pluginRegistry.allPlugins) { + plugin.onRegisterPluginWithGodotNative() + } + setKeepScreenOn(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on"))) + } + + // Include the returned non-null views in the Godot view hierarchy. + for (plugin in pluginRegistry.allPlugins) { + val pluginView = plugin.onMainCreate(activity) + if (pluginView != null) { + if (plugin.shouldBeOnTop()) { + containerLayout?.addView(pluginView) + } else { + containerLayout?.addView(pluginView, 0) + } + } + } + } + renderViewInitialized = true + } finally { + if (!renderViewInitialized) { + containerLayout?.removeAllViews() + containerLayout = null + } + } + return containerLayout + } + + fun onResume(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityResumed() + if (mAccelerometer != null) { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) + } + if (mGravity != null) { + mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME) + } + if (mMagnetometer != null) { + mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) + } + if (mGyroscope != null) { + mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME) + } + if (useImmersive) { + val window = requireActivity().window + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + } + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainResume() + } + } + + fun onPause(host: GodotHost) { + if (host != primaryHost) { + return + } + + renderView!!.onActivityPaused() + mSensorManager.unregisterListener(this) + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainPause() + } + } + + fun onDestroy(primaryHost: GodotHost) { + if (this.primaryHost != primaryHost) { + return + } + + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainDestroy() + } + GodotLib.ondestroy() + forceQuit() + } + + /** + * Activity result callback + */ + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainActivityResult(requestCode, resultCode, data) + } + } + + /** + * Permissions request callback + */ + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String?>, + grantResults: IntArray + ) { + for (plugin in pluginRegistry.allPlugins) { + plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults) + } + for (i in permissions.indices) { + GodotLib.requestPermissionResult( + permissions[i], + grantResults[i] == PackageManager.PERMISSION_GRANTED + ) + } + } + + /** + * Invoked on the render thread when the Godot setup is complete. + */ + private fun onGodotSetupCompleted() { + for (plugin in pluginRegistry.allPlugins) { + plugin.onGodotSetupCompleted() + } + primaryHost?.onGodotSetupCompleted() + } + + /** + * Invoked on the render thread when the Godot main loop has started. + */ + private fun onGodotMainLoopStarted() { + for (plugin in pluginRegistry.allPlugins) { + plugin.onGodotMainLoopStarted() + } + primaryHost?.onGodotMainLoopStarted() + } + + private fun restart() { + primaryHost?.onGodotRestartRequested(this) + } + + private fun registerUiChangeListener() { + val decorView = requireActivity().window.decorView + decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) + } + + @Keep + private fun alert(message: String, title: String) { + alert(message, title, null) + } + + private fun alert( + @StringRes messageResId: Int, + @StringRes titleResId: Int, + okCallback: Runnable? + ) { + val res: Resources = getActivity()?.resources ?: return + alert(res.getString(messageResId), res.getString(titleResId), okCallback) + } + + private fun alert(message: String, title: String, okCallback: Runnable?) { + val activity: Activity = getActivity() ?: return + runOnUiThread(Runnable { + val builder = AlertDialog.Builder(activity) + builder.setMessage(message).setTitle(title) + builder.setPositiveButton( + "OK" + ) { dialog: DialogInterface, id: Int -> + okCallback?.run() + dialog.cancel() + } + val dialog = builder.create() + dialog.show() + }) + } + + /** + * Queue a runnable to be run on the render thread. + * + * This must be called after the render thread has started. + */ + fun runOnRenderThread(action: Runnable) { + if (renderView != null) { + renderView!!.queueOnRenderThread(action) + } + } + + /** + * Runs the specified action on the UI thread. + * If the current thread is the UI thread, then the action is executed immediately. + * If the current thread is not the UI thread, the action is posted to the event queue + * of the UI thread. + */ + fun runOnUiThread(action: Runnable) { + val activity: Activity = getActivity() ?: return + activity.runOnUiThread(action) + } + + /** + * Returns true if the call is being made on the Ui thread. + */ + private fun isOnUiThread() = Looper.myLooper() == Looper.getMainLooper() + + /** + * Returns true if `Vulkan` is used for rendering. + */ + private fun usesVulkan(): Boolean { + val renderer = GodotLib.getGlobal("rendering/renderer/rendering_method") + val renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver") + return ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice + } + + /** + * Returns true if the device meets the base requirements for Vulkan support, false otherwise. + */ + private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean { + if (packageManager == null) { + return false + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { + // Optional requirements.. log as warning if missing + Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1") + } + + // Check for api version 1.0 + return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003) + } + return false + } + + private fun setKeepScreenOn(p_enabled: Boolean) { + runOnUiThread { + if (p_enabled) { + getActivity()?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + getActivity()?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + fun hasClipboard(): Boolean { + return mClipboard.hasPrimaryClip() + } + + fun getClipboard(): String? { + val clipData = mClipboard.primaryClip ?: return "" + val text = clipData.getItemAt(0).text ?: return "" + return text.toString() + } + + fun setClipboard(text: String?) { + val clip = ClipData.newPlainText("myLabel", text) + mClipboard.setPrimaryClip(clip) + } + + private fun forceQuit() { + forceQuit(0) + } + + @Keep + private fun forceQuit(instanceId: Int): Boolean { + if (primaryHost == null) { + return false + } + return if (instanceId == 0) { + primaryHost!!.onGodotForceQuit(this) + true + } else { + primaryHost!!.onGodotForceQuit(instanceId) + } + } + + fun onBackPressed(host: GodotHost) { + if (host != primaryHost) { + return + } + + var shouldQuit = true + for (plugin in pluginRegistry.allPlugins) { + if (plugin.onMainBackPressed()) { + shouldQuit = false + } + } + if (shouldQuit && renderView != null) { + renderView!!.queueOnRenderThread { GodotLib.back() } + } + } + + private fun getRotatedValues(values: FloatArray?): FloatArray? { + if (values == null || values.size != 3) { + return values + } + val display = + (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + val displayRotation = display.rotation + val rotatedValues = FloatArray(3) + when (displayRotation) { + Surface.ROTATION_0 -> { + rotatedValues[0] = values[0] + rotatedValues[1] = values[1] + rotatedValues[2] = values[2] + } + Surface.ROTATION_90 -> { + rotatedValues[0] = -values[1] + rotatedValues[1] = values[0] + rotatedValues[2] = values[2] + } + Surface.ROTATION_180 -> { + rotatedValues[0] = -values[0] + rotatedValues[1] = -values[1] + rotatedValues[2] = values[2] + } + Surface.ROTATION_270 -> { + rotatedValues[0] = values[1] + rotatedValues[1] = -values[0] + rotatedValues[2] = values[2] + } + } + return rotatedValues + } + + override fun onSensorChanged(event: SensorEvent) { + if (renderView == null) { + return + } + when (event.sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.accelerometer( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_GRAVITY -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.gravity( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_MAGNETIC_FIELD -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.magnetometer( + -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] + ) + } + } + Sensor.TYPE_GYROSCOPE -> { + val rotatedValues = getRotatedValues(event.values) + renderView!!.queueOnRenderThread { + GodotLib.gyroscope( + rotatedValues!![0], rotatedValues[1], rotatedValues[2] + ) + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Do something here if sensor accuracy changes. + } + + /** + * Used by the native code (java_godot_wrapper.h) to vibrate the device. + * @param durationMs + */ + @SuppressLint("MissingPermission") + @Keep + private fun vibrate(durationMs: Int) { + if (durationMs > 0 && requestPermission("VIBRATE")) { + val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + } else { + // deprecated in API 26 + vibratorService.vibrate(durationMs.toLong()) + } + } + } + + private fun getCommandLine(): MutableList<String> { + val original: MutableList<String> = parseCommandLine() + val hostCommandLine = primaryHost?.commandLine + if (hostCommandLine != null && hostCommandLine.isNotEmpty()) { + original.addAll(hostCommandLine) + } + return original + } + + private fun parseCommandLine(): MutableList<String> { + val inputStream: InputStream + return try { + inputStream = requireActivity().assets.open("_cl_") + val len = ByteArray(4) + var r = inputStream.read(len) + if (r < 4) { + return mutableListOf() + } + val argc = + (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) + val cmdline = ArrayList<String>(argc) + for (i in 0 until argc) { + r = inputStream.read(len) + if (r < 4) { + return mutableListOf() + } + val strlen = + (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) + if (strlen > 65535) { + return mutableListOf() + } + val arg = ByteArray(strlen) + r = inputStream.read(arg) + if (r == strlen) { + cmdline[i] = String(arg, StandardCharsets.UTF_8) + } + } + cmdline + } catch (e: Exception) { + // The _cl_ file can be missing with no adverse effect + mutableListOf() + } + } + + /** + * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping. + * @return The input fallback mapping for the current XR mode. + */ + @Keep + private fun getInputFallbackMapping(): String? { + return xrMode.inputFallbackMapping + } + + fun requestPermission(name: String?): Boolean { + return requestPermission(name, getActivity()) + } + + fun requestPermissions(): Boolean { + return PermissionsUtil.requestManifestPermissions(getActivity()) + } + + fun getGrantedPermissions(): Array<String?>? { + return PermissionsUtil.getGrantedPermissions(getActivity()) + } + + @Keep + private fun getCACertificates(): String { + return GodotNetUtils.getCACertificates() + } + + private fun obbIsCorrupted(f: String, mainPackMd5: String): Boolean { + return try { + val fis: InputStream = FileInputStream(f) + + // Create MD5 Hash + val buffer = ByteArray(16384) + val complete = MessageDigest.getInstance("MD5") + var numRead: Int + do { + numRead = fis.read(buffer) + if (numRead > 0) { + complete.update(buffer, 0, numRead) + } + } while (numRead != -1) + fis.close() + val messageDigest = complete.digest() + + // Create Hex String + val hexString = StringBuilder() + for (b in messageDigest) { + var s = Integer.toHexString(0xFF and b.toInt()) + if (s.length == 1) { + s = "0$s" + } + hexString.append(s) + } + val md5str = hexString.toString() + md5str != mainPackMd5 + } catch (e: java.lang.Exception) { + e.printStackTrace() + true + } + } + + @Keep + private fun initInputDevices() { + renderView!!.initInputDevices() + } + + @Keep + private fun createNewGodotInstance(args: Array<String>): Int { + return primaryHost?.onNewGodotInstanceRequested(args) ?: 0 + } + + @Keep + private fun nativeBeginBenchmarkMeasure(label: String) { + beginBenchmarkMeasure(label) + } + + @Keep + private fun nativeEndBenchmarkMeasure(label: String) { + endBenchmarkMeasure(label) + } + + @Keep + private fun nativeDumpBenchmark(benchmarkFile: String) { + dumpBenchmark(fileAccessHandler, benchmarkFile) + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt new file mode 100644 index 0000000000..4636f753af --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -0,0 +1,167 @@ +/**************************************************************************/ +/* GodotActivity.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.annotation.CallSuper +import androidx.fragment.app.FragmentActivity +import org.godotengine.godot.utils.ProcessPhoenix + +/** + * Base abstract activity for Android apps intending to use Godot as the primary screen. + * + * Also a reference implementation for how to setup and use the [GodotFragment] fragment + * within an Android app. + */ +abstract class GodotActivity : FragmentActivity(), GodotHost { + + companion object { + private val TAG = GodotActivity::class.java.simpleName + + @JvmStatic + protected val EXTRA_FORCE_QUIT = "force_quit_requested" + @JvmStatic + protected val EXTRA_NEW_LAUNCH = "new_launch_requested" + } + + /** + * Interaction with the [Godot] object is delegated to the [GodotFragment] class. + */ + protected var godotFragment: GodotFragment? = null + private set + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.godot_app_layout) + + handleStartIntent(intent, true) + + val currentFragment = supportFragmentManager.findFragmentById(R.id.godot_fragment_container) + if (currentFragment is GodotFragment) { + Log.v(TAG, "Reusing existing Godot fragment instance.") + godotFragment = currentFragment + } else { + Log.v(TAG, "Creating new Godot fragment instance.") + godotFragment = initGodotInstance() + supportFragmentManager.beginTransaction().replace(R.id.godot_fragment_container, godotFragment!!).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss() + } + } + + override fun onDestroy() { + Log.v(TAG, "Destroying Godot app...") + super.onDestroy() + if (godotFragment != null) { + terminateGodotInstance(godotFragment!!.godot) + } + } + + override fun onGodotForceQuit(instance: Godot) { + runOnUiThread { terminateGodotInstance(instance) } + } + + private fun terminateGodotInstance(instance: Godot) { + if (godotFragment != null && instance === godotFragment!!.godot) { + Log.v(TAG, "Force quitting Godot instance") + ProcessPhoenix.forceQuit(this) + } + } + + override fun onGodotRestartRequested(instance: Godot) { + runOnUiThread { + if (godotFragment != null && instance === godotFragment!!.godot) { + // It's very hard to properly de-initialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing static data). + Log.v(TAG, "Restarting Godot instance...") + ProcessPhoenix.triggerRebirth(this) + } + } + } + + override fun onNewIntent(newIntent: Intent) { + super.onNewIntent(newIntent) + intent = newIntent + + handleStartIntent(newIntent, false) + + godotFragment?.onNewIntent(newIntent) + } + + private fun handleStartIntent(intent: Intent, newLaunch: Boolean) { + val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false) + if (forceQuitRequested) { + Log.d(TAG, "Force quit requested, terminating..") + ProcessPhoenix.forceQuit(this) + return + } + if (!newLaunch) { + val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false) + if (newLaunchRequested) { + Log.d(TAG, "New launch requested, restarting..") + val restartIntent = Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false) + ProcessPhoenix.triggerRebirth(this, restartIntent) + return + } + } + } + + @CallSuper + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + godotFragment?.onActivityResult(requestCode, resultCode, data) + } + + @CallSuper + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onBackPressed() { + godotFragment?.onBackPressed() ?: super.onBackPressed() + } + + override fun getActivity(): Activity? { + return this + } + + /** + * Used to initialize the Godot fragment instance in [onCreate]. + */ + protected open fun initGodotInstance(): GodotFragment { + return GodotFragment() + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java new file mode 100644 index 0000000000..9a8b10ea3e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -0,0 +1,429 @@ +/**************************************************************************/ +/* GodotFragment.java */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot; + +import org.godotengine.godot.utils.BenchmarkUtils; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Messenger; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.vending.expansion.downloader.DownloadProgressInfo; +import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; +import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; +import com.google.android.vending.expansion.downloader.Helpers; +import com.google.android.vending.expansion.downloader.IDownloaderClient; +import com.google.android.vending.expansion.downloader.IDownloaderService; +import com.google.android.vending.expansion.downloader.IStub; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Base fragment for Android apps intending to use Godot for part of the app's UI. + */ +public class GodotFragment extends Fragment implements IDownloaderClient, GodotHost { + private static final String TAG = GodotFragment.class.getSimpleName(); + + private IStub mDownloaderClientStub; + private TextView mStatusText; + private TextView mProgressFraction; + private TextView mProgressPercent; + private TextView mAverageSpeed; + private TextView mTimeRemaining; + private ProgressBar mPB; + + private View mDashboard; + private View mCellMessage; + + private Button mPauseButton; + private Button mWiFiSettingsButton; + + private FrameLayout godotContainerLayout; + private boolean mStatePaused; + private int mState; + + @Nullable + private GodotHost parentHost; + private Godot godot; + + static private Intent mCurrentIntent; + + public void onNewIntent(Intent intent) { + mCurrentIntent = intent; + } + + static public Intent getCurrentIntent() { + return mCurrentIntent; + } + + private void setState(int newState) { + if (mState != newState) { + mState = newState; + mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState)); + } + } + + private void setButtonPausedState(boolean paused) { + mStatePaused = paused; + int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; + mPauseButton.setText(stringResourceID); + } + + public interface ResultCallback { + void callback(int requestCode, int resultCode, Intent data); + } + public ResultCallback resultCallback; + + public Godot getGodot() { + return godot; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof GodotHost) { + parentHost = (GodotHost)getParentFragment(); + } else if (getActivity() instanceof GodotHost) { + parentHost = (GodotHost)getActivity(); + } + } + + @Override + public void onDetach() { + super.onDetach(); + parentHost = null; + } + + @CallSuper + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCallback != null) { + resultCallback.callback(requestCode, resultCode, data); + resultCallback = null; + } + + godot.onActivityResult(requestCode, resultCode, data); + } + + @CallSuper + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + godot.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onServiceConnected(Messenger m) { + IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m); + remoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); + } + + @Override + public void onCreate(Bundle icicle) { + BenchmarkUtils.beginBenchmarkMeasure("GodotFragment::onCreate"); + super.onCreate(icicle); + + final Activity activity = getActivity(); + mCurrentIntent = activity.getIntent(); + + godot = new Godot(requireContext()); + performEngineInitialization(); + BenchmarkUtils.endBenchmarkMeasure("GodotFragment::onCreate"); + } + + private void performEngineInitialization() { + try { + godot.onCreate(this); + + if (!godot.onInitNativeLayer(this)) { + throw new IllegalStateException("Unable to initialize engine native layer"); + } + + godotContainerLayout = godot.onInitRenderView(this); + if (godotContainerLayout == null) { + throw new IllegalStateException("Unable to initialize engine render view"); + } + } catch (IllegalArgumentException ignored) { + final Activity activity = getActivity(); + Intent notifierIntent = new Intent(activity, activity.getClass()); + notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = PendingIntent.getActivity(activity, 0, + notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + int startResult; + try { + startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(getContext(), pendingIntent, GodotDownloaderService.class); + + if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { + // This is where you do set up to display the download + // progress (next step in onCreateView) + mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, GodotDownloaderService.class); + return; + } + + // Restart engine initialization + performEngineInitialization(); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to start download service", e); + } + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle icicle) { + if (mDownloaderClientStub != null) { + View downloadingExpansionView = + inflater.inflate(R.layout.downloading_expansion, container, false); + mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar); + mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText); + mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction); + mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage); + mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed); + mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining); + mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard); + mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular); + mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton); + mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton); + + return downloadingExpansionView; + } + + return godotContainerLayout; + } + + @Override + public void onDestroy() { + godot.onDestroy(this); + super.onDestroy(); + } + + @Override + public void onPause() { + super.onPause(); + + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.disconnect(getActivity()); + } + return; + } + + godot.onPause(this); + } + + @Override + public void onResume() { + super.onResume(); + if (!godot.isInitialized()) { + if (null != mDownloaderClientStub) { + mDownloaderClientStub.connect(getActivity()); + } + return; + } + + godot.onResume(this); + } + + public void onBackPressed() { + godot.onBackPressed(this); + } + + /** + * The download state should trigger changes in the UI --- it may be useful + * to show the state as being indeterminate at times. This sample can be + * considered a guideline. + */ + @Override + public void onDownloadStateChanged(int newState) { + setState(newState); + boolean showDashboard = true; + boolean showCellMessage = false; + boolean paused; + boolean indeterminate; + switch (newState) { + case IDownloaderClient.STATE_IDLE: + // STATE_IDLE means the service is listening, so it's + // safe to start making remote service calls. + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_CONNECTING: + case IDownloaderClient.STATE_FETCHING_URL: + showDashboard = true; + paused = false; + indeterminate = true; + break; + case IDownloaderClient.STATE_DOWNLOADING: + paused = false; + showDashboard = true; + indeterminate = false; + break; + + case IDownloaderClient.STATE_FAILED_CANCELED: + case IDownloaderClient.STATE_FAILED: + case IDownloaderClient.STATE_FAILED_FETCHING_URL: + case IDownloaderClient.STATE_FAILED_UNLICENSED: + paused = true; + showDashboard = false; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: + case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: + showDashboard = false; + paused = true; + indeterminate = false; + showCellMessage = true; + break; + + case IDownloaderClient.STATE_PAUSED_BY_REQUEST: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_PAUSED_ROAMING: + case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: + paused = true; + indeterminate = false; + break; + case IDownloaderClient.STATE_COMPLETED: + showDashboard = false; + paused = false; + indeterminate = false; + performEngineInitialization(); + return; + default: + paused = true; + indeterminate = true; + showDashboard = true; + } + int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE; + if (mDashboard.getVisibility() != newDashboardVisibility) { + mDashboard.setVisibility(newDashboardVisibility); + } + int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE; + if (mCellMessage.getVisibility() != cellMessageVisibility) { + mCellMessage.setVisibility(cellMessageVisibility); + } + + mPB.setIndeterminate(indeterminate); + setButtonPausedState(paused); + } + + @Override + public void onDownloadProgress(DownloadProgressInfo progress) { + mAverageSpeed.setText(getString(R.string.kilobytes_per_second, + Helpers.getSpeedString(progress.mCurrentSpeed))); + mTimeRemaining.setText(getString(R.string.time_remaining, + Helpers.getTimeRemaining(progress.mTimeRemaining))); + + mPB.setMax((int)(progress.mOverallTotal >> 8)); + mPB.setProgress((int)(progress.mOverallProgress >> 8)); + mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); + mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, + progress.mOverallTotal)); + } + + @CallSuper + @Override + public List<String> getCommandLine() { + return parentHost != null ? parentHost.getCommandLine() : Collections.emptyList(); + } + + @CallSuper + @Override + public void onGodotSetupCompleted() { + if (parentHost != null) { + parentHost.onGodotSetupCompleted(); + } + } + + @CallSuper + @Override + public void onGodotMainLoopStarted() { + if (parentHost != null) { + parentHost.onGodotMainLoopStarted(); + } + } + + @Override + public void onGodotForceQuit(Godot instance) { + if (parentHost != null) { + parentHost.onGodotForceQuit(instance); + } + } + + @Override + public boolean onGodotForceQuit(int godotInstanceId) { + return parentHost != null && parentHost.onGodotForceQuit(godotInstanceId); + } + + @Override + public void onGodotRestartRequested(Godot instance) { + if (parentHost != null) { + parentHost.onGodotRestartRequested(instance); + } + } + + @Override + public int onNewGodotInstanceRequested(String[] args) { + if (parentHost != null) { + return parentHost.onNewGodotInstanceRequested(args); + } + return 0; + } +} 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 330e2ede76..52350c12a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,10 +29,10 @@ /**************************************************************************/ package org.godotengine.godot; + import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.gl.GodotRenderer; import org.godotengine.godot.input.GodotInputHandler; -import org.godotengine.godot.utils.GLUtils; import org.godotengine.godot.xr.XRMode; import org.godotengine.godot.xr.ovr.OvrConfigChooser; import org.godotengine.godot.xr.ovr.OvrContextFactory; @@ -78,22 +78,23 @@ import java.io.InputStream; * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. */ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { + private final GodotHost host; private final Godot godot; private final GodotInputHandler inputHandler; private final GodotRenderer godotRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) { - super(context); - GLUtils.use_debug_opengl = p_use_debug_opengl; + public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) { + super(host.getActivity()); + this.host = host; this.godot = godot; this.inputHandler = new GodotInputHandler(this); this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } - init(xrMode, false); + init(xrMode, false, useDebugOpengl); } @Override @@ -123,7 +124,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Override public void onBackPressed() { - godot.onBackPressed(); + godot.onBackPressed(host); } @Override @@ -166,8 +167,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Override public void requestPointerCapture() { - super.requestPointerCapture(); - inputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + inputHandler.onPointerCaptureChange(true); + } } @Override @@ -188,10 +191,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); @@ -231,7 +234,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView return super.onResolvePointerIcon(me, pointerIndex); } - private void init(XRMode xrMode, boolean translucent) { + private void init(XRMode xrMode, boolean translucent, boolean useDebugOpengl) { setPreserveEGLContextOnPause(true); setFocusableInTouchMode(true); switch (xrMode) { @@ -260,7 +263,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView /* Setup the context factory for 2.0 rendering. * See ContextFactory class definition below */ - setEGLContextFactory(new RegularContextFactory()); + setEGLContextFactory(new RegularContextFactory(useDebugOpengl)); /* We need to choose an EGLConfig that matches the format of * our surface exactly. This is going to be done in our @@ -273,7 +276,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView new RegularConfigChooser(8, 8, 8, 8, 16, 0))); break; } + } + @Override + public void startRenderer() { /* Set the renderer responsible for frame rendering */ setRenderer(godotRenderer); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 7700b9b628..e5333085dd 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -30,11 +30,13 @@ package org.godotengine.godot; +import android.app.Activity; + import java.util.Collections; import java.util.List; /** - * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment. + * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} engine. */ public interface GodotHost { /** @@ -86,4 +88,9 @@ public interface GodotHost { default int onNewGodotInstanceRequested(String[] args) { return 0; } + + /** + * Provide access to the Activity hosting the Godot engine. + */ + Activity getActivity(); } 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 d9aab950df..b9ecd6971d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -61,8 +61,7 @@ public class GodotLib { GodotNetUtils netUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, - boolean use_apk_expansion, - GodotTTS tts); + boolean use_apk_expansion); /** * Invoked on the main thread to clean up Godot native layer. @@ -74,7 +73,7 @@ public class GodotLib { * Invoked on the GL thread to complete setup for the Godot native layer logic. * @param p_cmdline Command line arguments used to configure Godot native layer components. */ - public static native boolean setup(String[] p_cmdline); + public static native boolean setup(String[] p_cmdline, GodotTTS tts); /** * Invoked on the GL thread when the underlying Android surface has changed size. @@ -148,7 +147,7 @@ public class GodotLib { /** * Forward regular key events. */ - public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed); + public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed, boolean p_echo); /** * Forward game device's key events. 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 02c0d67fff..ebf3a6b2fb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -39,6 +39,11 @@ public interface GodotRenderView { void initInputDevices(); + /** + * Starts the thread that will drive Godot's rendering. + */ + void startRenderer(); + void queueOnRenderThread(Runnable event); void onActivityPaused(); @@ -51,4 +56,8 @@ public interface GodotRenderView { void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY); void setPointerIcon(int pointerType); + + default boolean canCapturePointer() { + return getInputHandler().canCapturePointer(); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt new file mode 100644 index 0000000000..68cd2c1358 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotService.kt @@ -0,0 +1,54 @@ +package org.godotengine.godot + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import android.util.Log + +/** + * Godot service responsible for hosting the Godot engine instance. + */ +class GodotService : Service() { + + companion object { + private val TAG = GodotService::class.java.simpleName + } + + private var boundIntent: Intent? = null + private val godot by lazy { + Godot(applicationContext) + } + + override fun onCreate() { + super.onCreate() + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? { + if (boundIntent != null) { + Log.d(TAG, "GodotService already bound") + return null + } + + boundIntent = intent + return GodotHandle(godot) + } + + override fun onRebind(intent: Intent?) { + super.onRebind(intent) + } + + override fun onUnbind(intent: Intent?): Boolean { + return super.onUnbind(intent) + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + } + + class GodotHandle(val godot: Godot) : Binder() +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 34490d4625..48708152be 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -35,7 +35,6 @@ import org.godotengine.godot.vulkan.VkRenderer; import org.godotengine.godot.vulkan.VkSurfaceView; import android.annotation.SuppressLint; -import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -52,14 +51,16 @@ import androidx.annotation.Keep; import java.io.InputStream; public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { + private final GodotHost host; private final Godot godot; private final GodotInputHandler mInputHandler; private final VkRenderer mRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotVulkanRenderView(Context context, Godot godot) { - super(context); + public GodotVulkanRenderView(GodotHost host, Godot godot) { + super(host.getActivity()); + this.host = host; this.godot = godot; mInputHandler = new GodotInputHandler(this); mRenderer = new VkRenderer(); @@ -67,6 +68,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); + } + + @Override + public void startRenderer() { startRenderer(mRenderer); } @@ -97,7 +102,7 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Override public void onBackPressed() { - godot.onBackPressed(); + godot.onBackPressed(host); } @Override @@ -134,8 +139,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Override public void requestPointerCapture() { - super.requestPointerCapture(); - mInputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + mInputHandler.onPointerCaptureChange(true); + } } @Override @@ -162,10 +169,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index 8449c08b88..56397bb2c2 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 @@ -867,7 +867,7 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback */ public interface EGLConfigChooser { /** - * Choose a configuration from the list. Implementors typically + * Choose a configuration from the list. Implementers typically * implement this method by calling * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 7dc5fb6f83..e26c9d39c2 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 @@ -227,35 +227,36 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi ) dragInProgress = false } - return true } - dragInProgress = true - val x = terminusEvent.x val y = terminusEvent.y - if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress) { + if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) - } else { + } else if (!scaleInProgress){ + dragInProgress = true GodotInputHandler.handleMotionEvent(terminusEvent) } return true } override fun onScale(detector: ScaleGestureDetector): Boolean { - if (!panningAndScalingEnabled || pointerCaptureInProgress) { + if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) { return false } - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + + if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { + GodotLib.magnify( + detector.focusX, + detector.focusY, + detector.scaleFactor + ) + } return true } override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { - if (!panningAndScalingEnabled || pointerCaptureInProgress) { + if (!panningAndScalingEnabled || pointerCaptureInProgress || dragInProgress) { return false } scaleInProgress = true 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 cedbbfb7c3..1a25be0460 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 @@ -66,6 +66,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; + /** + * Used to decide whether mouse capture can be enabled. + */ + private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; + public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); mRenderView = godotView; @@ -105,6 +110,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; } + public boolean canCapturePointer() { + return lastSeenToolType == MotionEvent.TOOL_TYPE_MOUSE; + } + public void onPointerCaptureChange(boolean hasCapture) { godotGestureHandler.onPointerCaptureChange(hasCapture); } @@ -132,7 +141,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, false); + GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); }; return true; @@ -167,13 +176,15 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, true); + GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); } return true; } public boolean onTouchEvent(final MotionEvent event) { + lastSeenToolType = event.getToolType(0); + this.scaleGestureDetector.onTouchEvent(event); if (this.gestureDetector.onTouchEvent(event)) { // The gesture detector has handled the event. @@ -198,6 +209,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onGenericMotionEvent(MotionEvent event) { + lastSeenToolType = event.getToolType(0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { // The gesture detector has handled the event. return true; @@ -293,7 +306,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return; } - // Assign first available number. Re-use numbers where possible. + // Assign first available number. Reuse numbers where possible. final int id = assignJoystickIdNumber(deviceId); final Joystick joystick = new Joystick(); @@ -471,15 +484,27 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { + // Fix the buttonsMask + switch (eventAction) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // Zero-up the button state + buttonsMask = 0; + break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + if (buttonsMask == 0) { + buttonsMask = MotionEvent.BUTTON_PRIMARY; + } + break; + } + // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. switch (eventAction) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - // Zero-up the button state - buttonsMask = 0; - // FALL THROUGH case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_EXIT: diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index f48dba56df..06b565c30f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false); if (mHasSelection) { mHasSelection = false; @@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene // Return keys are handled through action events continue; } - GodotLib.key(0, character, 0, true); - GodotLib.key(0, character, 0, false); + GodotLib.key(0, character, 0, true, false); + GodotLib.key(0, character, 0, false, false); } } @@ -127,8 +127,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene if (characters != null) { for (int i = 0; i < characters.length(); i++) { final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true); - GodotLib.key(0, character, 0, false); + GodotLib.key(0, character, 0, true, false); + GodotLib.key(0, character, 0, false, false); } } } @@ -136,8 +136,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed mRenderView.queueOnRenderThread(() -> { - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true); - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); }); mRenderView.getView().requestFocus(); return true; diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 357008ca66..984bf607d0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -46,7 +46,7 @@ class FileAccessHandler(val context: Context) { private val TAG = FileAccessHandler::class.java.simpleName private const val FILE_NOT_FOUND_ERROR_ID = -1 - private const val INVALID_FILE_ID = 0 + internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { @@ -96,13 +96,17 @@ class FileAccessHandler(val context: Context) { private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 fun fileOpen(path: String?, modeFlags: Int): Int { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + return fileOpen(path, accessFlag) + } + + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { return INVALID_FILE_ID } try { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID files.put(++lastFileId, dataAccess) diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java index ebab8398de..dce6753b7a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -33,6 +33,7 @@ package org.godotengine.godot.tts; import org.godotengine.godot.GodotLib; import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; @@ -62,19 +63,17 @@ public class GodotTTS extends UtteranceProgressListener { final private static int EVENT_CANCEL = 2; final private static int EVENT_BOUNDARY = 3; - final private TextToSpeech synth; - final private LinkedList<GodotUtterance> queue; + private final Context context; + private TextToSpeech synth; + private LinkedList<GodotUtterance> queue; final private Object lock = new Object(); private GodotUtterance lastUtterance; private boolean speaking; private boolean paused; - public GodotTTS(Activity p_activity) { - synth = new TextToSpeech(p_activity, null); - queue = new LinkedList<GodotUtterance>(); - - synth.setOnUtteranceProgressListener(this); + public GodotTTS(Context context) { + this.context = context; } private void updateTTS() { @@ -187,6 +186,16 @@ public class GodotTTS extends UtteranceProgressListener { } /** + * Initialize synth and query. + */ + public void init() { + synth = new TextToSpeech(context, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + /** * Adds an utterance to the queue. */ public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { 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 new file mode 100644 index 0000000000..1552c8f082 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -0,0 +1,122 @@ +/**************************************************************************/ +/* BenchmarkUtils.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +@file:JvmName("BenchmarkUtils") + +package org.godotengine.godot.utils + +import android.os.Build +import android.os.SystemClock +import android.os.Trace +import android.util.Log +import org.godotengine.godot.BuildConfig +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 + +/** + * Contains benchmark related utilities methods + */ +private const val TAG = "GodotBenchmark" + +var useBenchmark = false +var benchmarkFile = "" + +private val startBenchmarkFrom = ConcurrentSkipListMap<String, Long>() +private val benchmarkTracker = ConcurrentSkipListMap<String, Double>() + +/** + * Start measuring and tracing the execution of a given section of code using the given label. + * + * Must be followed by a call to [endBenchmarkMeasure]. + * + * Note: Only enabled on 'editorDev' build variant. + */ +fun beginBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + startBenchmarkFrom[label] = SystemClock.elapsedRealtime() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.beginAsyncSection(label, 0) + } +} + +/** + * End measuring and tracing of the section of code with the given label. + * + * Must be preceded by a call [beginBenchmarkMeasure] + * + * * Note: Only enabled on 'editorDev' build variant. + */ +fun endBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + val startTime = startBenchmarkFrom[label] ?: return + val total = SystemClock.elapsedRealtime() - startTime + benchmarkTracker[label] = total / 1000.0 + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.endAsyncSection(label, 0) + } +} + +/** + * Dump the benchmark measurements. + * If [filepath] is valid, the data is also written in json format to the specified file. + * + * * Note: Only enabled on 'editorDev' build variant. + */ +@JvmOverloads +fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + if (!useBenchmark) { + return + } + + val printOut = + benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n") + Log.i(TAG, "BENCHMARK:\n$printOut") + + if (fileAccessHandler != null && !filepath.isNullOrBlank()) { + val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) + fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) + fileAccessHandler.fileClose(fileId) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java index 7db02968bb..2c7b73ae4d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java @@ -44,8 +44,6 @@ public class GLUtils { public static final boolean DEBUG = false; - public static boolean use_debug_opengl = false; - private static final String[] ATTRIBUTES_NAMES = new String[] { "EGL_BUFFER_SIZE", "EGL_ALPHA_SIZE", diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java index c31d56a3e1..dca190a2fc 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java @@ -36,7 +36,8 @@ import android.net.wifi.WifiManager; import android.util.Base64; import android.util.Log; -import java.io.StringWriter; +import androidx.annotation.NonNull; + import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.Enumeration; @@ -50,9 +51,9 @@ public class GodotNetUtils { /* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */ private WifiManager.MulticastLock multicastLock; - public GodotNetUtils(Activity p_activity) { - if (PermissionsUtil.hasManifestPermission(p_activity, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) { - WifiManager wifi = (WifiManager)p_activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + public GodotNetUtils(Context context) { + if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) { + WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); multicastLock = wifi.createMulticastLock("GodotMulticastLock"); multicastLock.setReferenceCounted(true); } @@ -91,7 +92,7 @@ public class GodotNetUtils { * @see https://developer.android.com/reference/java/security/KeyStore . * @return A string of concatenated X509 certificates in PEM format. */ - public static String getCACertificates() { + public static @NonNull String getCACertificates() { try { KeyStore ks = KeyStore.getInstance("AndroidCAStore"); StringBuilder writer = new StringBuilder(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index e34c94975b..8353fc8dc6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -32,6 +32,7 @@ package org.godotengine.godot.utils; import android.Manifest; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -42,15 +43,16 @@ import android.os.Environment; import android.provider.Settings; import android.util.Log; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * This class includes utility functions for Android permissions related operations. */ - public final class PermissionsUtil { private static final String TAG = PermissionsUtil.class.getSimpleName(); @@ -58,6 +60,7 @@ public final class PermissionsUtil { static final int REQUEST_CAMERA_PERMISSION = 2; 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; private PermissionsUtil() { @@ -65,31 +68,57 @@ 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 name the name of the requested permission. + * @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". */ - public static boolean requestPermission(String name, Activity activity) { + public static boolean requestPermission(String permissionName, Activity activity) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already return true; } - if (name.equals("RECORD_AUDIO") && ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); - return false; - } + switch (permissionName) { + case "RECORD_AUDIO": + case Manifest.permission.RECORD_AUDIO: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); + return false; + } + return true; - if (name.equals("CAMERA") && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); - return false; - } + case "CAMERA": + case Manifest.permission.CAMERA: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); + return false; + } + return true; - if (name.equals("VIBRATE") && ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); - return false; + case "VIBRATE": + case Manifest.permission.VIBRATE: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); + return false; + } + return true; + + default: + // Check if the given permission is a dangerous permission + try { + PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + // Unknown permission - return false as it can't be granted. + Log.w(TAG, "Unable to identify permission " + permissionName, e); + return false; + } + return true; } - return true; } /** @@ -98,6 +127,16 @@ public final class PermissionsUtil { * @return true/false. "true" if all permissions were granted otherwise returns "false". */ public static boolean requestManifestPermissions(Activity activity) { + return requestManifestPermissions(activity, null); + } + + /** + * 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". + */ + public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } @@ -115,6 +154,9 @@ public final class PermissionsUtil { List<String> requestedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { + if (excludes != null && excludes.contains(manifestPermission)) { + continue; + } try { if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { @@ -151,13 +193,13 @@ public final class PermissionsUtil { /** * With this function you can get the list of dangerous permissions that have been granted to the Android application. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @return granted permissions list */ - public static String[] getGrantedPermissions(Activity activity) { + public static String[] getGrantedPermissions(Context context) { String[] manifestPermissions; try { - manifestPermissions = getManifestPermissions(activity); + manifestPermissions = getManifestPermissions(context); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return new String[0]; @@ -173,9 +215,9 @@ public final class PermissionsUtil { grantedPermissions.add(manifestPermission); } } else { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission); int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) { grantedPermissions.add(manifestPermission); } } @@ -190,13 +232,13 @@ public final class PermissionsUtil { /** * Check if the given permission is in the AndroidManifest.xml file. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @param permission the permession to look for in the manifest file. * @return "true" if the permission is in the manifest file of the activity, "false" otherwise. */ - public static boolean hasManifestPermission(Activity activity, String permission) { + public static boolean hasManifestPermission(Context context, String permission) { try { - for (String p : getManifestPermissions(activity)) { + for (String p : getManifestPermissions(context)) { if (permission.equals(p)) return true; } @@ -208,13 +250,13 @@ public final class PermissionsUtil { /** * Returns the permissions defined in the AndroidManifest.xml file. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @return manifest permissions list * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - private static String[] getManifestPermissions(Activity activity) throws PackageManager.NameNotFoundException { - PackageManager packageManager = activity.getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS); + private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); if (packageInfo.requestedPermissions == null) return new String[0]; return packageInfo.requestedPermissions; @@ -222,13 +264,13 @@ public final class PermissionsUtil { /** * Returns the information of the desired permission. - * @param activity the caller activity for this method. + * @param context the caller context for this method. * @param permission the name of the permission. * @return permission info object * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - private static PermissionInfo getPermissionInfo(Activity activity, String permission) throws PackageManager.NameNotFoundException { - PackageManager packageManager = activity.getPackageManager(); + private static PermissionInfo getPermissionInfo(Context context, String permission) throws PackageManager.NameNotFoundException { + PackageManager packageManager = context.getPackageManager(); return packageManager.getPermissionInfo(permission, 0); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java index 3ee3478fcb..b1bce45fbb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -90,7 +90,7 @@ public final class ProcessPhoenix extends Activity { */ public static void forceQuit(Activity activity, int pid) { Process.killProcess(pid); // Kill original main process - activity.finish(); + activity.finishAndRemoveTask(); Runtime.getRuntime().exit(0); // Kill kill kill! } diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index 1a126ff765..01ee41e30b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -51,12 +51,22 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory { private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private final boolean mUseDebugOpengl; + + public RegularContextFactory() { + this(false); + } + + public RegularContextFactory(boolean useDebugOpengl) { + this.mUseDebugOpengl = useDebugOpengl; + } + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { Log.w(TAG, "creating OpenGL ES 3.0 context :"); GLUtils.checkEglError(TAG, "Before eglCreateContext", egl); EGLContext context; - if (GLUtils.use_debug_opengl) { + if (mUseDebugOpengl) { int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); } else { diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 99e29bb53d..c113a13040 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -31,12 +31,13 @@ #ifndef JAVA_GODOT_IO_WRAPPER_H #define JAVA_GODOT_IO_WRAPPER_H -#include <android/log.h> -#include <jni.h> +#include "string_android.h" #include "core/math/rect2i.h" #include "core/variant/typed_array.h" -#include "string_android.h" + +#include <android/log.h> +#include <jni.h> // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ class GodotIOJavaWrapper { diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 18091649e3..74605e3377 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -30,37 +30,36 @@ #include "java_godot_lib_jni.h" -#include "java_godot_io_wrapper.h" -#include "java_godot_wrapper.h" - -#include "android/asset_manager_jni.h" #include "android_input_handler.h" #include "api/java_class_wrapper.h" #include "api/jni_singleton.h" -#include "core/config/engine.h" -#include "core/config/project_settings.h" -#include "core/input/input.h" #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" #include "file_access_filesystem_jandroid.h" +#include "java_godot_io_wrapper.h" +#include "java_godot_wrapper.h" #include "jni_utils.h" -#include "main/main.h" #include "net_socket_android.h" #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" #include "tts_android.h" -#include <android/input.h> -#include <unistd.h> - -#include <android/native_window_jni.h> +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/input/input.h" +#include "main/main.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" #endif +#include <android/asset_manager_jni.h> +#include <android/input.h> +#include <android/native_window_jni.h> +#include <unistd.h> + static JavaClassWrapper *java_class_wrapper = nullptr; static OS_Android *os_android = nullptr; static AndroidInputHandler *input_handler = nullptr; @@ -116,7 +115,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) { JavaVM *jvm; env->GetJavaVM(&jvm); @@ -133,18 +132,17 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv DirAccessJAndroid::setup(p_directory_access_handler); FileAccessFilesystemJAndroid::setup(p_file_access_handler); NetSocketAndroid::setup(p_net_utils); - TTS_Android::setup(p_godot_tts); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); - return godot_java->on_video_init(env); + return true; } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) { _terminate(env, false); } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts) { setup_android_thread(); const char **cmdline = nullptr; @@ -185,6 +183,8 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env return false; } + TTS_Android::setup(p_godot_tts); + java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); GDREGISTER_CLASS(JNISingleton); return true; @@ -385,11 +385,11 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged( } // Called on the UI thread -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo) { if (step.get() <= 0) { return; } - input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed); + input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed, p_echo); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 59ab2448d7..ee6a19034c 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,9 +37,9 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz); -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); @@ -49,7 +49,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jboolean p_double_tap); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y); diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index b50d4870bd..a95f762e01 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -49,6 +49,8 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { _request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V"); _release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V"); } + + _can_capture_pointer = env->GetMethodID(_cls, "canCapturePointer", "()Z"); } bool GodotJavaViewWrapper::can_update_pointer_icon() const { @@ -56,7 +58,16 @@ bool GodotJavaViewWrapper::can_update_pointer_icon() const { } bool GodotJavaViewWrapper::can_capture_pointer() const { - return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr; + // We can capture the pointer if the other jni capture method ids are initialized, + // and GodotView#canCapturePointer() returns true. + if (_request_pointer_capture != nullptr && _release_pointer_capture != nullptr && _can_capture_pointer != nullptr) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + + return env->CallBooleanMethod(_godot_view, _can_capture_pointer); + } + + return false; } void GodotJavaViewWrapper::request_pointer_capture() { diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index 9b64ded29c..e5b04e4866 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -31,12 +31,13 @@ #ifndef JAVA_GODOT_VIEW_WRAPPER_H #define JAVA_GODOT_VIEW_WRAPPER_H +#include "string_android.h" + #include "core/math/vector2.h" + #include <android/log.h> #include <jni.h> -#include "string_android.h" - // Class that makes functions in java/src/org/godotengine/godot/GodotView.java callable from C++ class GodotJavaViewWrapper { private: @@ -44,6 +45,7 @@ private: jobject _godot_view; + jmethodID _can_capture_pointer = 0; jmethodID _request_pointer_capture = 0; jmethodID _release_pointer_capture = 0; diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 2b504ad69b..79ba2528ba 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -58,12 +58,10 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ } // get some Godot method pointers... - _on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()Z"); _restart = p_env->GetMethodID(godot_class, "restart", "()V"); _finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z"); _set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V"); _alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V"); - _get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I"); _get_clipboard = p_env->GetMethodID(godot_class, "getClipboard", "()Ljava/lang/String;"); _set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V"); _has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z"); @@ -72,17 +70,15 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); - _get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;"); - _is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z"); _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V"); _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); - - // get some Activity method pointers... - _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); + _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V"); + _end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V"); + _dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V"); } GodotJavaWrapper::~GodotJavaWrapper() { @@ -102,29 +98,6 @@ jobject GodotJavaWrapper::get_activity() { return activity; } -jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env) { - if (godot_class) { - if (p_env == nullptr) { - p_env = get_jni_env(); - } - ERR_FAIL_NULL_V(p_env, nullptr); - jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class); - return p_env->GetStaticObjectField(godot_class, fid); - } else { - return nullptr; - } -} - -jobject GodotJavaWrapper::get_class_loader() { - if (_get_class_loader) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, nullptr); - return env->CallObjectMethod(activity, _get_class_loader); - } else { - return nullptr; - } -} - GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { if (godot_view != nullptr) { return godot_view; @@ -140,17 +113,6 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { return godot_view; } -bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) { - if (_on_video_init) { - if (p_env == nullptr) { - p_env = get_jni_env(); - } - ERR_FAIL_NULL_V(p_env, false); - return p_env->CallBooleanMethod(godot_instance, _on_video_init); - } - return false; -} - void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) { if (_on_godot_setup_completed) { if (p_env == nullptr) { @@ -209,15 +171,6 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { } } -int GodotJavaWrapper::get_gles_version_code() { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, 0); - if (_get_GLES_version_code) { - return env->CallIntMethod(godot_instance, _get_GLES_version_code); - } - return 0; -} - bool GodotJavaWrapper::has_get_clipboard() { return _get_clipboard != nullptr; } @@ -330,26 +283,6 @@ void GodotJavaWrapper::init_input_devices() { } } -jobject GodotJavaWrapper::get_surface() { - if (_get_surface) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, nullptr); - return env->CallObjectMethod(godot_instance, _get_surface); - } else { - return nullptr; - } -} - -bool GodotJavaWrapper::is_activity_resumed() { - if (_is_activity_resumed) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, false); - return env->CallBooleanMethod(godot_instance, _is_activity_resumed); - } else { - return false; - } -} - void GodotJavaWrapper::vibrate(int p_duration_ms) { if (_vibrate) { JNIEnv *env = get_jni_env(); @@ -371,3 +304,30 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> args) { return 0; } } + +void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) { + if (_begin_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::end_benchmark_measure(const String &p_label) { + if (_end_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) { + if (_dump_benchmark) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data()); + env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 05144380e6..ba42d5dccd 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -31,13 +31,14 @@ #ifndef JAVA_GODOT_WRAPPER_H #define JAVA_GODOT_WRAPPER_H -#include <android/log.h> -#include <jni.h> - -#include "core/templates/list.h" #include "java_godot_view_wrapper.h" #include "string_android.h" +#include "core/templates/list.h" + +#include <android/log.h> +#include <jni.h> + // Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++ class GodotJavaWrapper { private: @@ -48,12 +49,10 @@ private: GodotJavaViewWrapper *godot_view = nullptr; - jmethodID _on_video_init = nullptr; jmethodID _restart = nullptr; jmethodID _finish = nullptr; jmethodID _set_keep_screen_on = nullptr; jmethodID _alert = nullptr; - jmethodID _get_GLES_version_code = nullptr; jmethodID _get_clipboard = nullptr; jmethodID _set_clipboard = nullptr; jmethodID _has_clipboard = nullptr; @@ -62,34 +61,30 @@ private: jmethodID _get_granted_permissions = nullptr; jmethodID _get_ca_certificates = nullptr; jmethodID _init_input_devices = nullptr; - jmethodID _get_surface = nullptr; - jmethodID _is_activity_resumed = nullptr; jmethodID _vibrate = nullptr; jmethodID _get_input_fallback_mapping = nullptr; jmethodID _on_godot_setup_completed = nullptr; jmethodID _on_godot_main_loop_started = nullptr; - jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; jmethodID _get_render_view = nullptr; + jmethodID _begin_benchmark_measure = nullptr; + jmethodID _end_benchmark_measure = nullptr; + jmethodID _dump_benchmark = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); ~GodotJavaWrapper(); jobject get_activity(); - jobject get_member_object(const char *p_name, const char *p_class, JNIEnv *p_env = nullptr); - jobject get_class_loader(); GodotJavaViewWrapper *get_godot_view(); - bool on_video_init(JNIEnv *p_env = nullptr); void on_godot_setup_completed(JNIEnv *p_env = nullptr); void on_godot_main_loop_started(JNIEnv *p_env = nullptr); void restart(JNIEnv *p_env = nullptr); bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0); void set_keep_screen_on(bool p_enabled); void alert(const String &p_message, const String &p_title); - int get_gles_version_code(); bool has_get_clipboard(); String get_clipboard(); bool has_set_clipboard(); @@ -101,11 +96,12 @@ public: Vector<String> get_granted_permissions() const; String get_ca_certificates() const; void init_input_devices(); - jobject get_surface(); - bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); int create_new_godot_instance(List<String> args); + void begin_benchmark_measure(const String &p_label); + void end_benchmark_measure(const String &p_label); + void dump_benchmark(const String &benchmark_file); }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/jni_utils.h b/platform/android/jni_utils.h index d1a4082ae5..c608f9ebaa 100644 --- a/platform/android/jni_utils.h +++ b/platform/android/jni_utils.h @@ -32,8 +32,10 @@ #define JNI_UTILS_H #include "string_android.h" -#include <core/config/engine.h> -#include <core/variant/variant.h> + +#include "core/config/engine.h" +#include "core/variant/variant.h" + #include <jni.h> struct jvalret { diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 73081e35e7..c040d8c4c6 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -30,25 +30,24 @@ #include "os_android.h" +#include "dir_access_jandroid.h" +#include "display_server_android.h" +#include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" +#include "java_godot_io_wrapper.h" +#include "java_godot_wrapper.h" +#include "net_socket_android.h" + #include "core/config/project_settings.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" -#include "platform/android/display_server_android.h" #include "scene/main/scene_tree.h" #include "servers/rendering_server.h" -#include "dir_access_jandroid.h" -#include "file_access_android.h" -#include "file_access_filesystem_jandroid.h" -#include "net_socket_android.h" - #include <dlfcn.h> #include <sys/system_properties.h> -#include "java_godot_io_wrapper.h" -#include "java_godot_wrapper.h" - const char *OS_Android::ANDROID_EXEC_PATH = "apk"; String _remove_symlink(const String &dir) { @@ -168,7 +167,7 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han } p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); - ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); if (r_resolved_path != nullptr) { *r_resolved_path = path; @@ -182,7 +181,7 @@ String OS_Android::get_name() const { } String OS_Android::get_system_property(const char *key) const { - static String value; + String value; char value_str[PROP_VALUE_MAX]; if (__system_property_get(key, value_str)) { value = String(value_str); @@ -230,20 +229,20 @@ String OS_Android::get_version() const { "ro.potato.version", "ro.xtended.version", "org.evolution.version", "ro.corvus.version", "ro.pa.version", "ro.crdroid.version", "ro.syberia.version", "ro.arrow.version", "ro.lineage.version" }; for (int i = 0; i < roms.size(); i++) { - static String rom_version = get_system_property(roms[i]); + String rom_version = get_system_property(roms[i]); if (!rom_version.is_empty()) { return rom_version; } } - static String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. + String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. if (!mod_version.is_empty()) { return mod_version; } // Handles stock Android. - static String sdk_version = get_system_property("ro.build.version.sdk_int"); - static String build = get_system_property("ro.build.version.incremental"); + String sdk_version = get_system_property("ro.build.version.sdk_int"); + String build = get_system_property("ro.build.version.incremental"); if (!sdk_version.is_empty()) { if (!build.is_empty()) { return vformat("%s.%s", sdk_version, build); @@ -675,6 +674,27 @@ 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) { +#ifdef TOOLS_ENABLED + godot_java->begin_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_end_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + godot_java->end_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_dump() { +#ifdef TOOLS_ENABLED + if (!is_use_benchmark_set()) { + return; + } + godot_java->dump_benchmark(get_benchmark_file()); +#endif +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "system_fonts") { return true; diff --git a/platform/android/os_android.h b/platform/android/os_android.h index f1d08b7cfe..abcc412588 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -32,6 +32,7 @@ #define OS_ANDROID_H #include "audio_driver_opensl.h" + #include "core/os/main_loop.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" @@ -164,6 +165,10 @@ 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_dump() override; + virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); ~OS_Android(); diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 843c015d49..5d48c4e248 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -30,12 +30,13 @@ #include "godot_plugin_jni.h" -#include <core/config/engine.h> -#include <core/config/project_settings.h> -#include <core/error/error_macros.h> -#include <platform/android/api/jni_singleton.h> -#include <platform/android/jni_utils.h> -#include <platform/android/string_android.h> +#include "api/jni_singleton.h" +#include "jni_utils.h" +#include "string_android.h" + +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/error/error_macros.h" static HashMap<String, JNISingleton *> jni_singletons; diff --git a/platform/android/string_android.h b/platform/android/string_android.h index fe2f2e20a7..3f30b8ec3d 100644 --- a/platform/android/string_android.h +++ b/platform/android/string_android.h @@ -31,8 +31,10 @@ #ifndef STRING_ANDROID_H #define STRING_ANDROID_H -#include "core/string/ustring.h" #include "thread_jandroid.h" + +#include "core/string/ustring.h" + #include <jni.h> /** diff --git a/platform/android/thread_jandroid.cpp b/platform/android/thread_jandroid.cpp index 5051c179ed..9f4140f70f 100644 --- a/platform/android/thread_jandroid.cpp +++ b/platform/android/thread_jandroid.cpp @@ -30,10 +30,10 @@ #include "thread_jandroid.h" -#include <android/log.h> - #include "core/os/thread.h" +#include <android/log.h> + static JavaVM *java_vm = nullptr; static thread_local JNIEnv *env = nullptr; diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp index c08c1d4941..aef59c2584 100644 --- a/platform/android/tts_android.cpp +++ b/platform/android/tts_android.cpp @@ -35,9 +35,11 @@ #include "string_android.h" #include "thread_jandroid.h" +bool TTS_Android::initialized = false; jobject TTS_Android::tts = nullptr; jclass TTS_Android::cls = nullptr; +jmethodID TTS_Android::_init = nullptr; jmethodID TTS_Android::_is_speaking = nullptr; jmethodID TTS_Android::_is_paused = nullptr; jmethodID TTS_Android::_get_voices = nullptr; @@ -49,23 +51,34 @@ jmethodID TTS_Android::_stop_speaking = nullptr; HashMap<int, Char16String> TTS_Android::ids; void TTS_Android::setup(jobject p_tts) { - JNIEnv *env = get_jni_env(); + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + tts = env->NewGlobalRef(p_tts); - tts = env->NewGlobalRef(p_tts); + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); - jclass c = env->GetObjectClass(tts); - cls = (jclass)env->NewGlobalRef(c); + _init = env->GetMethodID(cls, "init", "()V"); + _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); + _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); + _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); + _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); + _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); + _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); + _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); - _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); - _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); - _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); - _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); - _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); - _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); - _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); + if (_init) { + env->CallVoidMethod(tts, _init); + initialized = true; + } + } } void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (ids.has(p_id)) { int pos = 0; if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { @@ -86,6 +99,7 @@ void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { } bool TTS_Android::is_speaking() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_speaking) { JNIEnv *env = get_jni_env(); @@ -97,6 +111,7 @@ bool TTS_Android::is_speaking() { } bool TTS_Android::is_paused() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_paused) { JNIEnv *env = get_jni_env(); @@ -108,6 +123,7 @@ bool TTS_Android::is_paused() { } Array TTS_Android::get_voices() { + ERR_FAIL_COND_V_MSG(!initialized, Array(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); Array list; if (_get_voices) { JNIEnv *env = get_jni_env(); @@ -135,6 +151,7 @@ Array TTS_Android::get_voices() { } void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (p_interrupt) { stop(); } @@ -157,6 +174,7 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum } void TTS_Android::pause() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_pause_speaking) { JNIEnv *env = get_jni_env(); @@ -166,6 +184,7 @@ void TTS_Android::pause() { } void TTS_Android::resume() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_resume_speaking) { JNIEnv *env = get_jni_env(); @@ -175,6 +194,7 @@ void TTS_Android::resume() { } void TTS_Android::stop() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); for (const KeyValue<int, Char16String> &E : ids) { DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); } diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h index 8e00ac5000..39efef6ed1 100644 --- a/platform/android/tts_android.h +++ b/platform/android/tts_android.h @@ -31,6 +31,7 @@ #ifndef TTS_ANDROID_H #define TTS_ANDROID_H +#include "core/config/project_settings.h" #include "core/string/ustring.h" #include "core/templates/hash_map.h" #include "core/variant/array.h" @@ -39,9 +40,11 @@ #include <jni.h> class TTS_Android { + static bool initialized; static jobject tts; static jclass cls; + static jmethodID _init; static jmethodID _is_speaking; static jmethodID _is_paused; static jmethodID _get_voices; diff --git a/platform/android/vulkan/vulkan_context_android.cpp b/platform/android/vulkan_context_android.cpp index ce4b1b7967..01e6d14438 100644 --- a/platform/android/vulkan/vulkan_context_android.cpp +++ b/platform/android/vulkan_context_android.cpp @@ -28,10 +28,10 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifdef VULKAN_ENABLED - #include "vulkan_context_android.h" +#ifdef VULKAN_ENABLED + #ifdef USE_VOLK #include <volk.h> #else diff --git a/platform/android/vulkan/vulkan_context_android.h b/platform/android/vulkan_context_android.h index f253149ef6..f253149ef6 100644 --- a/platform/android/vulkan/vulkan_context_android.h +++ b/platform/android/vulkan_context_android.h |
