summaryrefslogtreecommitdiffstats
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/android/android_input_handler.cpp5
-rw-r--r--platform/android/android_input_handler.h2
-rw-r--r--platform/android/doc_classes/EditorExportPlatformAndroid.xml12
-rw-r--r--platform/android/export/export_plugin.cpp217
-rw-r--r--platform/android/export/export_plugin.h36
-rw-r--r--platform/android/export/godot_plugin_config.cpp36
-rw-r--r--platform/android/export/godot_plugin_config.h12
-rw-r--r--platform/android/export/gradle_export_util.cpp101
-rw-r--r--platform/android/export/gradle_export_util.h20
-rw-r--r--platform/android/java/app/src/com/godot/game/GodotApp.java4
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java154
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java1195
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt973
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt167
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java429
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java20
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotHost.java9
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotService.kt54
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java13
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java9
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java11
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java32
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java12
-rw-r--r--platform/android/java_godot_lib_jni.cpp6
-rw-r--r--platform/android/java_godot_lib_jni.h2
-rw-r--r--platform/android/java_godot_wrapper.cpp76
-rw-r--r--platform/android/java_godot_wrapper.h11
-rw-r--r--platform/android/os_android.cpp2
-rw-r--r--platform/ios/doc_classes/EditorExportPlatformIOS.xml3
-rw-r--r--platform/ios/export/export.cpp5
-rw-r--r--platform/ios/export/export_plugin.cpp486
-rw-r--r--platform/ios/export/export_plugin.h98
-rw-r--r--platform/ios/export/run_icon.svg1
-rw-r--r--platform/ios/os_ios.mm2
-rw-r--r--platform/linuxbsd/detect.py3
-rw-r--r--platform/linuxbsd/export/export_plugin.cpp5
-rw-r--r--platform/linuxbsd/export/export_plugin.h2
-rw-r--r--platform/linuxbsd/export/run_icon.svg2
-rw-r--r--platform/linuxbsd/joypad_linux.cpp34
-rw-r--r--platform/linuxbsd/os_linuxbsd.cpp34
-rw-r--r--platform/linuxbsd/x11/display_server_x11.cpp40
-rw-r--r--platform/linuxbsd/x11/display_server_x11.h1
-rw-r--r--platform/macos/display_server_macos.h6
-rw-r--r--platform/macos/display_server_macos.mm286
-rw-r--r--platform/macos/doc_classes/EditorExportPlatformMacOS.xml3
-rw-r--r--platform/macos/export/export_plugin.cpp17
-rw-r--r--platform/macos/export/run_icon.svg2
-rw-r--r--platform/macos/os_macos.h4
-rw-r--r--platform/macos/os_macos.mm61
-rw-r--r--platform/uwp/export/export_plugin.cpp20
-rw-r--r--platform/uwp/export/export_plugin.h1
-rw-r--r--platform/uwp/os_uwp.cpp2
-rw-r--r--platform/web/display_server_web.cpp52
-rw-r--r--platform/web/export/export_plugin.cpp22
-rw-r--r--platform/web/export/run_icon.svg2
-rw-r--r--platform/web/http_client_web.cpp10
-rw-r--r--platform/web/http_client_web.h1
-rw-r--r--platform/web/js/libs/library_godot_display.js25
-rw-r--r--platform/web/js/libs/library_godot_fetch.js15
-rw-r--r--platform/web/os_web.cpp2
-rw-r--r--platform/windows/display_server_windows.cpp333
-rw-r--r--platform/windows/display_server_windows.h6
-rw-r--r--platform/windows/export/export_plugin.cpp5
-rw-r--r--platform/windows/export/run_icon.svg2
-rw-r--r--platform/windows/gl_manager_windows.cpp1
-rw-r--r--platform/windows/os_windows.cpp6
72 files changed, 3252 insertions, 2005 deletions
diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp
index 37a019eaa4..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);
diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h
index 42d1c228a8..c74c5020e3 100644
--- a/platform/android/android_input_handler.h
+++ b/platform/android/android_input_handler.h
@@ -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/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
index 6d3affed15..d61d63d242 100644
--- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml
+++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
@@ -104,6 +104,12 @@
<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/signed" type="bool" setter="" getter="">
If [code]true[/code], package signing is enabled.
</member>
@@ -578,12 +584,6 @@
<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="">
- </member>
<member name="xr_features/xr_mode" type="int" setter="" getter="">
</member>
</members>
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index c94119e13d..4eb516fb63 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -48,6 +48,7 @@
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "main/splash.gen.h"
+#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For mono and svg.
#ifdef MODULE_SVG_ENABLED
@@ -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"));
@@ -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.
@@ -1841,6 +1837,8 @@ 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_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 +1847,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 +1884,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() {
@@ -2228,17 +2225,16 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_
}
bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
- String err;
- bool valid = false;
- const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
-
#ifdef MODULE_MONO_ENABLED
- err += TTR("Exporting to Android is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Android with C#/Mono instead.") + "\n";
- err += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
// Don't check for additional errors, as this particular error cannot be resolved.
- r_error = err;
+ r_error += TTR("Exporting to Android is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Android with C#/Mono instead.") + "\n";
+ r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
return false;
-#endif
+#else
+
+ String err;
+ bool valid = false;
+ const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
// Look for export templates (first official, and if defined custom templates).
@@ -2369,6 +2365,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
}
return valid;
+#endif // !MODULE_MONO_ENABLED
}
bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
@@ -2694,6 +2691,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");
@@ -2851,11 +2906,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) {
@@ -2879,9 +2963,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));
@@ -3299,16 +3383,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 0ac0fbb10b..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);
@@ -224,28 +231,11 @@ public:
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 b64cca3254..cdec5f55b7 100644
--- a/platform/android/export/godot_plugin_config.cpp
+++ b/platform/android/export/godot_plugin_config.cpp
@@ -30,6 +30,8 @@
#include "godot_plugin_config.h"
+#ifndef DISABLE_DEPRECATED
+
/*
* Set of prebuilt plugins.
* Currently unused, this is just for future reference:
@@ -145,10 +147,8 @@ PluginConfigAndroid PluginConfigAndroid::load_plugin_config(Ref<ConfigFile> conf
return plugin_config;
}
-String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs) {
- String plugins_binaries;
+void PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) {
if (!plugins_configs.is_empty()) {
- Vector<String> binaries;
for (int i = 0; i < plugins_configs.size(); i++) {
PluginConfigAndroid config = plugins_configs[i];
if (!config.valid_config) {
@@ -156,56 +156,44 @@ String PluginConfigAndroid::get_plugins_binaries(String binary_type, Vector<Plug
}
if (config.binary_type == binary_type) {
- binaries.push_back(config.binary);
+ r_result.push_back(config.binary);
}
if (binary_type == PluginConfigAndroid::BINARY_TYPE_LOCAL) {
- binaries.append_array(config.local_dependencies);
+ r_result.append_array(config.local_dependencies);
}
if (binary_type == PluginConfigAndroid::BINARY_TYPE_REMOTE) {
- binaries.append_array(config.remote_dependencies);
+ r_result.append_array(config.remote_dependencies);
}
}
-
- plugins_binaries = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(binaries);
}
-
- return plugins_binaries;
}
-String PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs) {
- String custom_maven_repos;
+void PluginConfigAndroid::get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) {
if (!plugins_configs.is_empty()) {
- Vector<String> repos_urls;
for (int i = 0; i < plugins_configs.size(); i++) {
PluginConfigAndroid config = plugins_configs[i];
if (!config.valid_config) {
continue;
}
- repos_urls.append_array(config.custom_maven_repos);
+ r_result.append_array(config.custom_maven_repos);
}
-
- custom_maven_repos = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(repos_urls);
}
- return custom_maven_repos;
}
-String PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs) {
- String plugins_names;
+void PluginConfigAndroid::get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result) {
if (!plugins_configs.is_empty()) {
- Vector<String> names;
for (int i = 0; i < plugins_configs.size(); i++) {
PluginConfigAndroid config = plugins_configs[i];
if (!config.valid_config) {
continue;
}
- names.push_back(config.name);
+ r_result.push_back(config.name);
}
- plugins_names = String(PluginConfigAndroid::PLUGIN_VALUE_SEPARATOR).join(names);
}
-
- return plugins_names;
}
+
+#endif // DISABLE_DEPRECATED
diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h
index bef00979a9..8c56d00187 100644
--- a/platform/android/export/godot_plugin_config.h
+++ b/platform/android/export/godot_plugin_config.h
@@ -31,6 +31,8 @@
#ifndef ANDROID_GODOT_PLUGIN_CONFIG_H
#define ANDROID_GODOT_PLUGIN_CONFIG_H
+#ifndef DISABLE_DEPRECATED
+
#include "core/config/project_settings.h"
#include "core/error/error_list.h"
#include "core/io/config_file.h"
@@ -67,8 +69,6 @@ struct PluginConfigAndroid {
inline static const char *BINARY_TYPE_LOCAL = "local";
inline static const char *BINARY_TYPE_REMOTE = "remote";
- inline static const char *PLUGIN_VALUE_SEPARATOR = "|";
-
// Set to true when the config file is properly loaded.
bool valid_config = false;
// Unix timestamp of last change to this plugin.
@@ -96,11 +96,13 @@ struct PluginConfigAndroid {
static PluginConfigAndroid load_plugin_config(Ref<ConfigFile> config_file, const String &path);
- static String get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs);
+ static void get_plugins_binaries(String binary_type, Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result);
- static String get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs);
+ static void get_plugins_custom_maven_repos(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result);
- static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs);
+ static void get_plugins_names(Vector<PluginConfigAndroid> plugins_configs, Vector<String> &r_result);
};
+#endif // DISABLE_DEPRECATED
+
#endif // ANDROID_GODOT_PLUGIN_CONFIG_H
diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp
index ba4487cc4d..d0d0c34bb4 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,42 @@ 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.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 += " <category android:name=\"android.intent.category.DEFAULT\" />\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 +319,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/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/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 64d3d4eca1..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,7 +55,7 @@ import kotlin.math.min
*
* It also plays the role of the primary editor window.
*/
-open class GodotEditor : FullScreenGodotApp() {
+open class GodotEditor : GodotActivity() {
companion object {
private val TAG = GodotEditor::class.java.simpleName
@@ -115,7 +115,7 @@ open class GodotEditor : FullScreenGodotApp() {
runOnUiThread {
// Enable long press, panning and scaling gestures
- godotFragment?.renderView?.inputHandler?.apply {
+ godotFragment?.godot?.renderView?.inputHandler?.apply {
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
}
@@ -318,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/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
index 3e975449d8..91d272735e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
@@ -30,156 +30,10 @@
package org.godotengine.godot;
-import org.godotengine.godot.utils.ProcessPhoenix;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-
/**
- * Base activity for Android apps intending to use Godot as the primary and only screen.
+ * Base abstract activity for Android apps intending to use Godot as the primary screen.
*
- * It's also a reference implementation for how to setup and use the {@link Godot} fragment
- * within an Android app.
+ * @deprecated Use {@link GodotActivity}
*/
-public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost {
- private static final String TAG = FullScreenGodotApp.class.getSimpleName();
-
- protected static final String EXTRA_FORCE_QUIT = "force_quit_requested";
- protected static final String EXTRA_NEW_LAUNCH = "new_launch_requested";
-
- @Nullable
- private Godot godotFragment;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.godot_app_layout);
-
- handleStartIntent(getIntent(), true);
-
- Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.godot_fragment_container);
- if (currentFragment instanceof Godot) {
- Log.v(TAG, "Reusing existing Godot fragment instance.");
- godotFragment = (Godot)currentFragment;
- } else {
- Log.v(TAG, "Creating new Godot fragment instance.");
- godotFragment = initGodotInstance();
- getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss();
- }
- }
-
- @Override
- public void onDestroy() {
- Log.v(TAG, "Destroying Godot app...");
- super.onDestroy();
- terminateGodotInstance(godotFragment);
- }
-
- @Override
- public final void onGodotForceQuit(Godot instance) {
- runOnUiThread(() -> {
- terminateGodotInstance(instance);
- });
- }
-
- private void terminateGodotInstance(Godot instance) {
- if (instance == godotFragment) {
- Log.v(TAG, "Force quitting Godot instance");
- ProcessPhoenix.forceQuit(FullScreenGodotApp.this);
- }
- }
-
- @Override
- public final void onGodotRestartRequested(Godot instance) {
- runOnUiThread(() -> {
- if (instance == godotFragment) {
- // It's very hard to properly de-initialize Godot on Android to restart the game
- // from scratch. Therefore, we need to kill the whole app process and relaunch it.
- //
- // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including
- // releasing and reloading native libs or resetting their state somehow and clearing static data).
- Log.v(TAG, "Restarting Godot instance...");
- ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this);
- }
- });
- }
-
- @Override
- public void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- setIntent(intent);
-
- handleStartIntent(intent, false);
-
- if (godotFragment != null) {
- godotFragment.onNewIntent(intent);
- }
- }
-
- private void handleStartIntent(Intent intent, boolean newLaunch) {
- boolean forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false);
- if (forceQuitRequested) {
- Log.d(TAG, "Force quit requested, terminating..");
- ProcessPhoenix.forceQuit(this);
- return;
- }
-
- if (!newLaunch) {
- boolean newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false);
- if (newLaunchRequested) {
- Log.d(TAG, "New launch requested, restarting..");
-
- Intent restartIntent = new Intent(intent).putExtra(EXTRA_NEW_LAUNCH, false);
- ProcessPhoenix.triggerRebirth(this, restartIntent);
- return;
- }
- }
- }
-
- @CallSuper
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (godotFragment != null) {
- godotFragment.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- @CallSuper
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (godotFragment != null) {
- godotFragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
- }
- }
-
- @Override
- public void onBackPressed() {
- if (godotFragment != null) {
- godotFragment.onBackPressed();
- } else {
- super.onBackPressed();
- }
- }
-
- /**
- * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}.
- */
- @NonNull
- protected Godot initGodotInstance() {
- return new Godot();
- }
-
- @Nullable
- protected final Godot getGodotFragment() {
- return godotFragment;
- }
-}
+@Deprecated
+public abstract class FullScreenGodotApp extends GodotActivity {}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
deleted file mode 100644
index 9f2dec7317..0000000000
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java
+++ /dev/null
@@ -1,1195 +0,0 @@
-/**************************************************************************/
-/* Godot.java */
-/**************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/**************************************************************************/
-/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
-/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
-/* */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the */
-/* "Software"), to deal in the Software without restriction, including */
-/* without limitation the rights to use, copy, modify, merge, publish, */
-/* distribute, sublicense, and/or sell copies of the Software, and to */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions: */
-/* */
-/* The above copyright notice and this permission notice shall be */
-/* included in all copies or substantial portions of the Software. */
-/* */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
-/**************************************************************************/
-
-package org.godotengine.godot;
-
-import static android.content.Context.MODE_PRIVATE;
-import static android.content.Context.WINDOW_SERVICE;
-
-import org.godotengine.godot.input.GodotEditText;
-import org.godotengine.godot.io.directory.DirectoryAccessHandler;
-import org.godotengine.godot.io.file.FileAccessHandler;
-import org.godotengine.godot.plugin.GodotPlugin;
-import org.godotengine.godot.plugin.GodotPluginRegistry;
-import org.godotengine.godot.tts.GodotTTS;
-import org.godotengine.godot.utils.BenchmarkUtils;
-import org.godotengine.godot.utils.GodotNetUtils;
-import org.godotengine.godot.utils.PermissionsUtil;
-import org.godotengine.godot.xr.XRMode;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.AlertDialog;
-import android.app.PendingIntent;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.content.pm.ConfigurationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Messenger;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.Surface;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewTreeObserver;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.view.WindowInsetsAnimation;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.vending.expansion.downloader.DownloadProgressInfo;
-import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
-import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller;
-import com.google.android.vending.expansion.downloader.Helpers;
-import com.google.android.vending.expansion.downloader.IDownloaderClient;
-import com.google.android.vending.expansion.downloader.IDownloaderService;
-import com.google.android.vending.expansion.downloader.IStub;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-
-public class Godot extends Fragment implements SensorEventListener, IDownloaderClient {
- private static final String TAG = Godot.class.getSimpleName();
-
- private IStub mDownloaderClientStub;
- private TextView mStatusText;
- private TextView mProgressFraction;
- private TextView mProgressPercent;
- private TextView mAverageSpeed;
- private TextView mTimeRemaining;
- private ProgressBar mPB;
- private ClipboardManager mClipboard;
-
- private View mDashboard;
- private View mCellMessage;
-
- private Button mPauseButton;
- private Button mWiFiSettingsButton;
-
- private XRMode xrMode = XRMode.REGULAR;
- private boolean use_immersive = false;
- private boolean use_debug_opengl = false;
- private boolean mStatePaused;
- private boolean activityResumed;
- private int mState;
-
- private GodotHost godotHost;
- private GodotPluginRegistry pluginRegistry;
-
- static private Intent mCurrentIntent;
-
- public void onNewIntent(Intent intent) {
- mCurrentIntent = intent;
- }
-
- static public Intent getCurrentIntent() {
- return mCurrentIntent;
- }
-
- private void setState(int newState) {
- if (mState != newState) {
- mState = newState;
- mStatusText.setText(Helpers.getDownloaderStringResourceIDFromState(newState));
- }
- }
-
- private void setButtonPausedState(boolean paused) {
- mStatePaused = paused;
- int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause;
- mPauseButton.setText(stringResourceID);
- }
-
- private String[] command_line;
- private boolean use_apk_expansion;
-
- private ViewGroup containerLayout;
- public GodotRenderView mRenderView;
- private boolean godot_initialized = false;
-
- private SensorManager mSensorManager;
- private Sensor mAccelerometer;
- private Sensor mGravity;
- private Sensor mMagnetometer;
- private Sensor mGyroscope;
-
- public GodotIO io;
- public GodotNetUtils netUtils;
- public GodotTTS tts;
- private DirectoryAccessHandler directoryAccessHandler;
- private FileAccessHandler fileAccessHandler;
-
- public interface ResultCallback {
- void callback(int requestCode, int resultCode, Intent data);
- }
- public ResultCallback result_callback;
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- if (getParentFragment() instanceof GodotHost) {
- godotHost = (GodotHost)getParentFragment();
- } else if (getActivity() instanceof GodotHost) {
- godotHost = (GodotHost)getActivity();
- }
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- godotHost = null;
- }
-
- @CallSuper
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (result_callback != null) {
- result_callback.callback(requestCode, resultCode, data);
- result_callback = null;
- }
-
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onMainActivityResult(requestCode, resultCode, data);
- }
- }
-
- @CallSuper
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults);
- }
-
- for (int i = 0; i < permissions.length; i++) {
- GodotLib.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED);
- }
- }
-
- /**
- * Invoked on the render thread when the Godot setup is complete.
- */
- @CallSuper
- protected void onGodotSetupCompleted() {
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onGodotSetupCompleted();
- }
-
- if (godotHost != null) {
- godotHost.onGodotSetupCompleted();
- }
- }
-
- /**
- * Invoked on the render thread when the Godot main loop has started.
- */
- @CallSuper
- protected void onGodotMainLoopStarted() {
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onGodotMainLoopStarted();
- }
-
- if (godotHost != null) {
- godotHost.onGodotMainLoopStarted();
- }
- }
-
- /**
- * Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.
- */
- @Keep
- private boolean onVideoInit() {
- final Activity activity = requireActivity();
- containerLayout = new FrameLayout(activity);
- containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
-
- // GodotEditText layout
- GodotEditText editText = new GodotEditText(activity);
- editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
- (int)getResources().getDimension(R.dimen.text_edit_height)));
- // ...add to FrameLayout
- containerLayout.addView(editText);
-
- tts = new GodotTTS(activity);
-
- if (!GodotLib.setup(command_line, tts)) {
- Log.e(TAG, "Unable to setup the Godot engine! Aborting...");
- alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit);
- return false;
- }
-
- if (usesVulkan()) {
- if (!meetsVulkanRequirements(activity.getPackageManager())) {
- alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit);
- return false;
- }
- mRenderView = new GodotVulkanRenderView(activity, this);
- } else {
- // Fallback to openGl
- mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl);
- }
-
- View view = mRenderView.getView();
- containerLayout.addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
- editText.setView(mRenderView);
- io.setEdit(editText);
-
- // Listeners for keyboard height.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- // Report the height of virtual keyboard as it changes during the animation.
- final View decorView = activity.getWindow().getDecorView();
- decorView.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP) {
- int startBottom, endBottom;
- @Override
- public void onPrepare(@NonNull WindowInsetsAnimation animation) {
- startBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
- }
-
- @NonNull
- @Override
- public WindowInsetsAnimation.Bounds onStart(@NonNull WindowInsetsAnimation animation, @NonNull WindowInsetsAnimation.Bounds bounds) {
- endBottom = decorView.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
- return bounds;
- }
-
- @NonNull
- @Override
- public WindowInsets onProgress(@NonNull WindowInsets windowInsets, @NonNull List<WindowInsetsAnimation> list) {
- // Find the IME animation.
- WindowInsetsAnimation imeAnimation = null;
- for (WindowInsetsAnimation animation : list) {
- if ((animation.getTypeMask() & WindowInsets.Type.ime()) != 0) {
- imeAnimation = animation;
- break;
- }
- }
- // Update keyboard height based on IME animation.
- if (imeAnimation != null) {
- float interpolatedFraction = imeAnimation.getInterpolatedFraction();
- // Linear interpolation between start and end values.
- float keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction;
- GodotLib.setVirtualKeyboardHeight((int)keyboardHeight);
- }
- return windowInsets;
- }
-
- @Override
- public void onEnd(@NonNull WindowInsetsAnimation animation) {
- }
- });
- } else {
- // Infer the virtual keyboard height using visible area.
- view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- // Don't allocate a new Rect every time the callback is called.
- final Rect visibleSize = new Rect();
-
- @Override
- public void onGlobalLayout() {
- final SurfaceView view = mRenderView.getView();
- view.getWindowVisibleDisplayFrame(visibleSize);
- final int keyboardHeight = view.getHeight() - visibleSize.bottom;
- GodotLib.setVirtualKeyboardHeight(keyboardHeight);
- }
- });
- }
-
- mRenderView.queueOnRenderThread(() -> {
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onRegisterPluginWithGodotNative();
- }
- setKeepScreenOn(Boolean.parseBoolean(GodotLib.getGlobal("display/window/energy_saving/keep_screen_on")));
- });
-
- // Include the returned non-null views in the Godot view hierarchy.
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- View pluginView = plugin.onMainCreate(activity);
- if (pluginView != null) {
- if (plugin.shouldBeOnTop()) {
- containerLayout.addView(pluginView);
- } else {
- containerLayout.addView(pluginView, 0);
- }
- }
- }
- return true;
- }
-
- /**
- * Returns true if `Vulkan` is used for rendering.
- */
- private boolean usesVulkan() {
- final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method");
- final String renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver");
- return ("forward_plus".equals(renderer) || "mobile".equals(renderer)) && "vulkan".equals(renderingDevice);
- }
-
- /**
- * Returns true if the device meets the base requirements for Vulkan support, false otherwise.
- */
- private boolean meetsVulkanRequirements(@Nullable PackageManager packageManager) {
- if (packageManager == null) {
- return false;
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) {
- // Optional requirements.. log as warning if missing
- Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1");
- }
-
- // Check for api version 1.0
- return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003);
- }
-
- return false;
- }
-
- public void setKeepScreenOn(final boolean p_enabled) {
- runOnUiThread(() -> {
- if (p_enabled) {
- getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- } else {
- getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
- });
- }
-
- /**
- * Used by the native code (java_godot_wrapper.h) to vibrate the device.
- * @param durationMs
- */
- @SuppressLint("MissingPermission")
- @Keep
- private void vibrate(int durationMs) {
- if (durationMs > 0 && requestPermission("VIBRATE")) {
- Vibrator v = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
- if (v != null) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE));
- } else {
- // deprecated in API 26
- v.vibrate(durationMs);
- }
- }
- }
- }
-
- public void restart() {
- if (godotHost != null) {
- godotHost.onGodotRestartRequested(this);
- }
- }
-
- public void alert(final String message, final String title) {
- alert(message, title, null);
- }
-
- private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) {
- Resources res = getResources();
- alert(res.getString(messageResId), res.getString(titleResId), okCallback);
- }
-
- private void alert(final String message, final String title, @Nullable Runnable okCallback) {
- final Activity activity = getActivity();
- runOnUiThread(() -> {
- AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- builder.setMessage(message).setTitle(title);
- builder.setPositiveButton(
- "OK",
- (dialog, id) -> {
- if (okCallback != null) {
- okCallback.run();
- }
- dialog.cancel();
- });
- AlertDialog dialog = builder.create();
- dialog.show();
- });
- }
-
- public int getGLESVersionCode() {
- ActivityManager am = (ActivityManager)getContext().getSystemService(Context.ACTIVITY_SERVICE);
- ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo();
- return deviceInfo.reqGlEsVersion;
- }
-
- @CallSuper
- protected String[] getCommandLine() {
- String[] original = parseCommandLine();
- String[] updated;
- List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null;
- if (hostCommandLine == null || hostCommandLine.isEmpty()) {
- updated = original;
- } else {
- updated = Arrays.copyOf(original, original.length + hostCommandLine.size());
- for (int i = 0; i < hostCommandLine.size(); i++) {
- updated[original.length + i] = hostCommandLine.get(i);
- }
- }
- return updated;
- }
-
- private String[] parseCommandLine() {
- InputStream is;
- try {
- is = getActivity().getAssets().open("_cl_");
- byte[] len = new byte[4];
- int r = is.read(len);
- if (r < 4) {
- return new String[0];
- }
- int argc = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF));
- String[] cmdline = new String[argc];
-
- for (int i = 0; i < argc; i++) {
- r = is.read(len);
- if (r < 4) {
- return new String[0];
- }
- int strlen = ((int)(len[3] & 0xFF) << 24) | ((int)(len[2] & 0xFF) << 16) | ((int)(len[1] & 0xFF) << 8) | ((int)(len[0] & 0xFF));
- if (strlen > 65535) {
- return new String[0];
- }
- byte[] arg = new byte[strlen];
- r = is.read(arg);
- if (r == strlen) {
- cmdline[i] = new String(arg, "UTF-8");
- }
- }
- return cmdline;
- } catch (Exception e) {
- // The _cl_ file can be missing with no adverse effect
- return new String[0];
- }
- }
-
- /**
- * Used by the native code (java_godot_wrapper.h) to check whether the activity is resumed or paused.
- */
- @Keep
- private boolean isActivityResumed() {
- return activityResumed;
- }
-
- /**
- * Used by the native code (java_godot_wrapper.h) to access the Android surface.
- */
- @Keep
- private Surface getSurface() {
- return mRenderView.getView().getHolder().getSurface();
- }
-
- /**
- * Used by the native code (java_godot_wrapper.h) to access the input fallback mapping.
- * @return The input fallback mapping for the current XR mode.
- */
- @Keep
- private String getInputFallbackMapping() {
- return xrMode.inputFallbackMapping;
- }
-
- String expansion_pack_path;
-
- private void initializeGodot() {
- if (expansion_pack_path != null) {
- String[] new_cmdline;
- int cll = 0;
- if (command_line != null) {
- new_cmdline = new String[command_line.length + 2];
- cll = command_line.length;
- for (int i = 0; i < command_line.length; i++) {
- new_cmdline[i] = command_line[i];
- }
- } else {
- new_cmdline = new String[2];
- }
-
- new_cmdline[cll] = "--main-pack";
- new_cmdline[cll + 1] = expansion_pack_path;
- command_line = new_cmdline;
- }
-
- final Activity activity = getActivity();
- io = new GodotIO(activity);
- netUtils = new GodotNetUtils(activity);
- Context context = getContext();
- directoryAccessHandler = new DirectoryAccessHandler(context);
- fileAccessHandler = new FileAccessHandler(context);
- mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
- mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
- mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
- mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
-
- godot_initialized = GodotLib.initialize(activity,
- this,
- activity.getAssets(),
- io,
- netUtils,
- directoryAccessHandler,
- fileAccessHandler,
- use_apk_expansion);
-
- result_callback = null;
- }
-
- @Override
- public void onServiceConnected(Messenger m) {
- IDownloaderService remoteService = DownloaderServiceMarshaller.CreateProxy(m);
- remoteService.onClientUpdated(mDownloaderClientStub.getMessenger());
- }
-
- @Override
- public void onCreate(Bundle icicle) {
- BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate");
- super.onCreate(icicle);
-
- final Activity activity = getActivity();
- Window window = activity.getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
- mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE);
- pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this);
-
- // check for apk expansion API
- boolean md5mismatch = false;
- command_line = getCommandLine();
- String main_pack_md5 = null;
- String main_pack_key = null;
-
- List<String> new_args = new LinkedList<>();
-
- for (int i = 0; i < command_line.length; i++) {
- boolean has_extra = i < command_line.length - 1;
- if (command_line[i].equals(XRMode.REGULAR.cmdLineArg)) {
- xrMode = XRMode.REGULAR;
- } else if (command_line[i].equals(XRMode.OPENXR.cmdLineArg)) {
- xrMode = XRMode.OPENXR;
- } else if (command_line[i].equals("--debug_opengl")) {
- use_debug_opengl = true;
- } else if (command_line[i].equals("--use_immersive")) {
- use_immersive = true;
- window.getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
- View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- UiChangeListener();
- } else if (command_line[i].equals("--use_apk_expansion")) {
- use_apk_expansion = true;
- } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) {
- main_pack_md5 = command_line[i + 1];
- i++;
- } else if (has_extra && command_line[i].equals("--apk_expansion_key")) {
- main_pack_key = command_line[i + 1];
- SharedPreferences prefs = activity.getSharedPreferences("app_data_keys",
- MODE_PRIVATE);
- Editor editor = prefs.edit();
- editor.putString("store_public_key", main_pack_key);
-
- editor.apply();
- i++;
- } else if (command_line[i].equals("--benchmark")) {
- BenchmarkUtils.setUseBenchmark(true);
- new_args.add(command_line[i]);
- } else if (has_extra && command_line[i].equals("--benchmark-file")) {
- BenchmarkUtils.setUseBenchmark(true);
- new_args.add(command_line[i]);
-
- // Retrieve the filepath
- BenchmarkUtils.setBenchmarkFile(command_line[i + 1]);
- new_args.add(command_line[i + 1]);
-
- i++;
- } else if (command_line[i].trim().length() != 0) {
- new_args.add(command_line[i]);
- }
- }
-
- if (new_args.isEmpty()) {
- command_line = null;
- } else {
- command_line = new_args.toArray(new String[new_args.size()]);
- }
- if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) {
- // check that environment is ok!
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- // show popup and die
- }
-
- // Build the full path to the app's expansion files
- try {
- expansion_pack_path = Helpers.getSaveFilePath(getContext());
- expansion_pack_path += "/main." + activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode + "." + activity.getPackageName() + ".obb";
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- File f = new File(expansion_pack_path);
-
- boolean pack_valid = true;
-
- if (!f.exists()) {
- pack_valid = false;
-
- } else if (obbIsCorrupted(expansion_pack_path, main_pack_md5)) {
- pack_valid = false;
- try {
- f.delete();
- } catch (Exception e) {
- }
- }
-
- if (!pack_valid) {
- Intent notifierIntent = new Intent(activity, activity.getClass());
- notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- PendingIntent pendingIntent;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- pendingIntent = PendingIntent.getActivity(activity, 0,
- notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
- } else {
- pendingIntent = PendingIntent.getActivity(activity, 0,
- notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- int startResult;
- try {
- startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(
- getContext(),
- pendingIntent,
- GodotDownloaderService.class);
-
- if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) {
- // This is where you do set up to display the download
- // progress (next step in onCreateView)
- mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this,
- GodotDownloaderService.class);
-
- return;
- }
- } catch (NameNotFoundException e) {
- // TODO Auto-generated catch block
- }
- }
- }
-
- mCurrentIntent = activity.getIntent();
-
- initializeGodot();
- BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate");
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
- if (mDownloaderClientStub != null) {
- View downloadingExpansionView =
- inflater.inflate(R.layout.downloading_expansion, container, false);
- mPB = (ProgressBar)downloadingExpansionView.findViewById(R.id.progressBar);
- mStatusText = (TextView)downloadingExpansionView.findViewById(R.id.statusText);
- mProgressFraction = (TextView)downloadingExpansionView.findViewById(R.id.progressAsFraction);
- mProgressPercent = (TextView)downloadingExpansionView.findViewById(R.id.progressAsPercentage);
- mAverageSpeed = (TextView)downloadingExpansionView.findViewById(R.id.progressAverageSpeed);
- mTimeRemaining = (TextView)downloadingExpansionView.findViewById(R.id.progressTimeRemaining);
- mDashboard = downloadingExpansionView.findViewById(R.id.downloaderDashboard);
- mCellMessage = downloadingExpansionView.findViewById(R.id.approveCellular);
- mPauseButton = (Button)downloadingExpansionView.findViewById(R.id.pauseButton);
- mWiFiSettingsButton = (Button)downloadingExpansionView.findViewById(R.id.wifiSettingsButton);
-
- return downloadingExpansionView;
- }
-
- return containerLayout;
- }
-
- @Override
- public void onDestroy() {
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onMainDestroy();
- }
-
- GodotLib.ondestroy();
-
- super.onDestroy();
-
- forceQuit();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- activityResumed = false;
-
- if (!godot_initialized) {
- if (null != mDownloaderClientStub) {
- mDownloaderClientStub.disconnect(getActivity());
- }
- return;
- }
- mRenderView.onActivityPaused();
-
- mSensorManager.unregisterListener(this);
-
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onMainPause();
- }
- }
-
- public boolean hasClipboard() {
- return mClipboard.hasPrimaryClip();
- }
-
- public String getClipboard() {
- ClipData clipData = mClipboard.getPrimaryClip();
- if (clipData == null)
- return "";
- CharSequence text = clipData.getItemAt(0).getText();
- if (text == null)
- return "";
- return text.toString();
- }
-
- public void setClipboard(String p_text) {
- ClipData clip = ClipData.newPlainText("myLabel", p_text);
- mClipboard.setPrimaryClip(clip);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- activityResumed = true;
- if (!godot_initialized) {
- if (null != mDownloaderClientStub) {
- mDownloaderClientStub.connect(getActivity());
- }
- return;
- }
-
- mRenderView.onActivityResumed();
-
- mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
- mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME);
- mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
- mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME);
-
- if (use_immersive) {
- Window window = getActivity().getWindow();
- window.getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar
- View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
-
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- plugin.onMainResume();
- }
- }
-
- public void UiChangeListener() {
- final View decorView = getActivity().getWindow().getDecorView();
- decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
- if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
- decorView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
- View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
- });
- }
-
- public float[] getRotatedValues(float values[]) {
- if (values == null || values.length != 3) {
- return values;
- }
-
- Display display =
- ((WindowManager)getActivity().getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
- int displayRotation = display.getRotation();
-
- float[] rotatedValues = new float[3];
- switch (displayRotation) {
- case Surface.ROTATION_0:
- rotatedValues[0] = values[0];
- rotatedValues[1] = values[1];
- rotatedValues[2] = values[2];
- break;
- case Surface.ROTATION_90:
- rotatedValues[0] = -values[1];
- rotatedValues[1] = values[0];
- rotatedValues[2] = values[2];
- break;
- case Surface.ROTATION_180:
- rotatedValues[0] = -values[0];
- rotatedValues[1] = -values[1];
- rotatedValues[2] = values[2];
- break;
- case Surface.ROTATION_270:
- rotatedValues[0] = values[1];
- rotatedValues[1] = -values[0];
- rotatedValues[2] = values[2];
- break;
- }
-
- return rotatedValues;
- }
-
- @Override
- public void onSensorChanged(SensorEvent event) {
- if (mRenderView == null) {
- return;
- }
-
- final int typeOfSensor = event.sensor.getType();
- switch (typeOfSensor) {
- case Sensor.TYPE_ACCELEROMETER: {
- float[] rotatedValues = getRotatedValues(event.values);
- mRenderView.queueOnRenderThread(() -> {
- GodotLib.accelerometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
- });
- break;
- }
- case Sensor.TYPE_GRAVITY: {
- float[] rotatedValues = getRotatedValues(event.values);
- mRenderView.queueOnRenderThread(() -> {
- GodotLib.gravity(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
- });
- break;
- }
- case Sensor.TYPE_MAGNETIC_FIELD: {
- float[] rotatedValues = getRotatedValues(event.values);
- mRenderView.queueOnRenderThread(() -> {
- GodotLib.magnetometer(-rotatedValues[0], -rotatedValues[1], -rotatedValues[2]);
- });
- break;
- }
- case Sensor.TYPE_GYROSCOPE: {
- float[] rotatedValues = getRotatedValues(event.values);
- mRenderView.queueOnRenderThread(() -> {
- GodotLib.gyroscope(rotatedValues[0], rotatedValues[1], rotatedValues[2]);
- });
- break;
- }
- }
- }
-
- @Override
- public final void onAccuracyChanged(Sensor sensor, int accuracy) {
- // Do something here if sensor accuracy changes.
- }
-
- public void onBackPressed() {
- boolean shouldQuit = true;
-
- for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
- if (plugin.onMainBackPressed()) {
- shouldQuit = false;
- }
- }
-
- if (shouldQuit && mRenderView != null) {
- mRenderView.queueOnRenderThread(GodotLib::back);
- }
- }
-
- /**
- * Queue a runnable to be run on the render thread.
- * <p>
- * This must be called after the render thread has started.
- */
- public final void runOnRenderThread(@NonNull Runnable action) {
- if (mRenderView != null) {
- mRenderView.queueOnRenderThread(action);
- }
- }
-
- public final void runOnUiThread(@NonNull Runnable action) {
- if (getActivity() != null) {
- getActivity().runOnUiThread(action);
- }
- }
-
- private void forceQuit() {
- // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each
- // native Godot components that is started in Godot#onVideoInit.
- forceQuit(0);
- }
-
- @Keep
- private boolean forceQuit(int instanceId) {
- if (godotHost == null) {
- return false;
- }
- if (instanceId == 0) {
- godotHost.onGodotForceQuit(this);
- return true;
- } else {
- return godotHost.onGodotForceQuit(instanceId);
- }
- }
-
- private boolean obbIsCorrupted(String f, String main_pack_md5) {
- try {
- InputStream fis = new FileInputStream(f);
-
- // Create MD5 Hash
- byte[] buffer = new byte[16384];
-
- MessageDigest complete = MessageDigest.getInstance("MD5");
- int numRead;
- do {
- numRead = fis.read(buffer);
- if (numRead > 0) {
- complete.update(buffer, 0, numRead);
- }
- } while (numRead != -1);
-
- fis.close();
- byte[] messageDigest = complete.digest();
-
- // Create Hex String
- StringBuilder hexString = new StringBuilder();
- for (byte b : messageDigest) {
- String s = Integer.toHexString(0xFF & b);
- if (s.length() == 1) {
- s = "0" + s;
- }
- hexString.append(s);
- }
- String md5str = hexString.toString();
-
- if (!md5str.equals(main_pack_md5)) {
- return true;
- }
- return false;
- } catch (Exception e) {
- e.printStackTrace();
- return true;
- }
- }
-
- public boolean requestPermission(String p_name) {
- return PermissionsUtil.requestPermission(p_name, getActivity());
- }
-
- public boolean requestPermissions() {
- return PermissionsUtil.requestManifestPermissions(getActivity());
- }
-
- public String[] getGrantedPermissions() {
- return PermissionsUtil.getGrantedPermissions(getActivity());
- }
-
- @Keep
- private String getCACertificates() {
- return GodotNetUtils.getCACertificates();
- }
-
- /**
- * The download state should trigger changes in the UI --- it may be useful
- * to show the state as being indeterminate at times. This sample can be
- * considered a guideline.
- */
- @Override
- public void onDownloadStateChanged(int newState) {
- setState(newState);
- boolean showDashboard = true;
- boolean showCellMessage = false;
- boolean paused;
- boolean indeterminate;
- switch (newState) {
- case IDownloaderClient.STATE_IDLE:
- // STATE_IDLE means the service is listening, so it's
- // safe to start making remote service calls.
- paused = false;
- indeterminate = true;
- break;
- case IDownloaderClient.STATE_CONNECTING:
- case IDownloaderClient.STATE_FETCHING_URL:
- showDashboard = true;
- paused = false;
- indeterminate = true;
- break;
- case IDownloaderClient.STATE_DOWNLOADING:
- paused = false;
- showDashboard = true;
- indeterminate = false;
- break;
-
- case IDownloaderClient.STATE_FAILED_CANCELED:
- case IDownloaderClient.STATE_FAILED:
- case IDownloaderClient.STATE_FAILED_FETCHING_URL:
- case IDownloaderClient.STATE_FAILED_UNLICENSED:
- paused = true;
- showDashboard = false;
- indeterminate = false;
- break;
- case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION:
- case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION:
- showDashboard = false;
- paused = true;
- indeterminate = false;
- showCellMessage = true;
- break;
-
- case IDownloaderClient.STATE_PAUSED_BY_REQUEST:
- paused = true;
- indeterminate = false;
- break;
- case IDownloaderClient.STATE_PAUSED_ROAMING:
- case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE:
- paused = true;
- indeterminate = false;
- break;
- case IDownloaderClient.STATE_COMPLETED:
- showDashboard = false;
- paused = false;
- indeterminate = false;
- initializeGodot();
- return;
- default:
- paused = true;
- indeterminate = true;
- showDashboard = true;
- }
- int newDashboardVisibility = showDashboard ? View.VISIBLE : View.GONE;
- if (mDashboard.getVisibility() != newDashboardVisibility) {
- mDashboard.setVisibility(newDashboardVisibility);
- }
- int cellMessageVisibility = showCellMessage ? View.VISIBLE : View.GONE;
- if (mCellMessage.getVisibility() != cellMessageVisibility) {
- mCellMessage.setVisibility(cellMessageVisibility);
- }
-
- mPB.setIndeterminate(indeterminate);
- setButtonPausedState(paused);
- }
-
- @Override
- public void onDownloadProgress(DownloadProgressInfo progress) {
- mAverageSpeed.setText(getString(R.string.kilobytes_per_second,
- Helpers.getSpeedString(progress.mCurrentSpeed)));
- mTimeRemaining.setText(getString(R.string.time_remaining,
- Helpers.getTimeRemaining(progress.mTimeRemaining)));
-
- mPB.setMax((int)(progress.mOverallTotal >> 8));
- mPB.setProgress((int)(progress.mOverallProgress >> 8));
- mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal));
- mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress,
- progress.mOverallTotal));
- }
-
- public void initInputDevices() {
- mRenderView.initInputDevices();
- }
-
- @Keep
- public GodotRenderView getRenderView() { // used by native side to get renderView
- return mRenderView;
- }
-
- @Keep
- public DirectoryAccessHandler getDirectoryAccessHandler() {
- return directoryAccessHandler;
- }
-
- @Keep
- public FileAccessHandler getFileAccessHandler() {
- return fileAccessHandler;
- }
-
- @Keep
- private int createNewGodotInstance(String[] args) {
- if (godotHost != null) {
- return godotHost.onNewGodotInstanceRequested(args);
- }
- return 0;
- }
-
- @Keep
- private void beginBenchmarkMeasure(String label) {
- BenchmarkUtils.beginBenchmarkMeasure(label);
- }
-
- @Keep
- private void endBenchmarkMeasure(String label) {
- BenchmarkUtils.endBenchmarkMeasure(label);
- }
-
- @Keep
- private void dumpBenchmark(String benchmarkFile) {
- BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile);
- }
-}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
new file mode 100644
index 0000000000..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 b465377743..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
@@ -233,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) {
@@ -262,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
@@ -275,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 c725b1a7c9..b9ecd6971d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -147,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 00243dab2a..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();
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 681e182adb..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
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 317344f2a5..185d03fe39 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
@@ -141,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;
@@ -176,7 +176,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, true);
+ GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0);
}
return true;
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/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
index edace53e7f..dce6753b7a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java
@@ -33,6 +33,7 @@ package org.godotengine.godot.tts;
import org.godotengine.godot.GodotLib;
import android.app.Activity;
+import android.content.Context;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
@@ -62,7 +63,7 @@ public class GodotTTS extends UtteranceProgressListener {
final private static int EVENT_CANCEL = 2;
final private static int EVENT_BOUNDARY = 3;
- final private Activity activity;
+ private final Context context;
private TextToSpeech synth;
private LinkedList<GodotUtterance> queue;
final private Object lock = new Object();
@@ -71,8 +72,8 @@ public class GodotTTS extends UtteranceProgressListener {
private boolean speaking;
private boolean paused;
- public GodotTTS(Activity p_activity) {
- activity = p_activity;
+ public GodotTTS(Context context) {
+ this.context = context;
}
private void updateTTS() {
@@ -188,7 +189,7 @@ public class GodotTTS extends UtteranceProgressListener {
* Initialize synth and query.
*/
public void init() {
- synth = new TextToSpeech(activity, null);
+ synth = new TextToSpeech(context, null);
queue = new LinkedList<GodotUtterance>();
synth.setOnUtteranceProgressListener(this);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java
index 7db02968bb..2c7b73ae4d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GLUtils.java
@@ -44,8 +44,6 @@ public class GLUtils {
public static final boolean DEBUG = false;
- public static boolean use_debug_opengl = false;
-
private static final String[] ATTRIBUTES_NAMES = new String[] {
"EGL_BUFFER_SIZE",
"EGL_ALPHA_SIZE",
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java
index c31d56a3e1..dca190a2fc 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java
@@ -36,7 +36,8 @@ import android.net.wifi.WifiManager;
import android.util.Base64;
import android.util.Log;
-import java.io.StringWriter;
+import androidx.annotation.NonNull;
+
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
@@ -50,9 +51,9 @@ public class GodotNetUtils {
/* A single, reference counted, multicast lock, or null if permission CHANGE_WIFI_MULTICAST_STATE is missing */
private WifiManager.MulticastLock multicastLock;
- public GodotNetUtils(Activity p_activity) {
- if (PermissionsUtil.hasManifestPermission(p_activity, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
- WifiManager wifi = (WifiManager)p_activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ public GodotNetUtils(Context context) {
+ if (PermissionsUtil.hasManifestPermission(context, "android.permission.CHANGE_WIFI_MULTICAST_STATE")) {
+ WifiManager wifi = (WifiManager)context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifi.createMulticastLock("GodotMulticastLock");
multicastLock.setReferenceCounted(true);
}
@@ -91,7 +92,7 @@ public class GodotNetUtils {
* @see https://developer.android.com/reference/java/security/KeyStore .
* @return A string of concatenated X509 certificates in PEM format.
*/
- public static String getCACertificates() {
+ public static @NonNull String getCACertificates() {
try {
KeyStore ks = KeyStore.getInstance("AndroidCAStore");
StringBuilder writer = new StringBuilder();
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
index a94188c405..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;
@@ -52,7 +53,6 @@ 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();
@@ -193,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];
@@ -215,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);
}
}
@@ -232,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;
}
@@ -250,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;
@@ -264,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/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_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 7c1b6023c7..74605e3377 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -135,7 +135,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv
os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion);
- return godot_java->on_video_init(env);
+ return true;
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {
@@ -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 9158e89c13..ee6a19034c 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -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_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 862d9f0436..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,20 +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;");
- _begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V");
- _end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V");
- _dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V");
-
- // get some Activity method pointers...
- _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
+ _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)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() {
@@ -105,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;
@@ -143,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) {
@@ -212,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;
}
@@ -333,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();
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 1efdffd71b..ba42d5dccd 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -49,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;
@@ -63,13 +61,10 @@ 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;
@@ -81,19 +76,15 @@ public:
~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();
@@ -105,8 +96,6 @@ 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);
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index c3a7d70034..c040d8c4c6 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -167,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;
diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
index 346cc9bf35..84bc0e1277 100644
--- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml
+++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
@@ -27,6 +27,9 @@
<member name="application/export_method_release" type="int" setter="" getter="">
Application distribution target (release export).
</member>
+ <member name="application/export_project_only" type="bool" setter="" getter="">
+ If [code]true[/code], exports iOS project files without building an XCArchive or [code].ipa[/code] file. If [code]false[/code], exports iOS project files and builds an XCArchive and [code].ipa[/code] file at the same time. When combining Godot with Fastlane or other build pipelines, you may want to set this to [code]true[/code].
+ </member>
<member name="application/icon_interpolation" type="int" setter="" getter="">
Interpolation method used to resize application icon.
</member>
diff --git a/platform/ios/export/export.cpp b/platform/ios/export/export.cpp
index e07a135861..98cc80e4a0 100644
--- a/platform/ios/export/export.cpp
+++ b/platform/ios/export/export.cpp
@@ -39,6 +39,11 @@ void register_ios_exporter_types() {
}
void register_ios_exporter() {
+#ifdef MACOS_ENABLED
+ EDITOR_DEF("export/ios/ios_deploy", "");
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/ios/ios_deploy", PROPERTY_HINT_GLOBAL_FILE, "*"));
+#endif
+
Ref<EditorExportPlatformIOS> platform;
platform.instantiate();
diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp
index edc850e74f..544bfb71e0 100644
--- a/platform/ios/export/export_plugin.cpp
+++ b/platform/ios/export/export_plugin.cpp
@@ -31,11 +31,15 @@
#include "export_plugin.h"
#include "logo_svg.gen.h"
+#include "run_icon_svg.gen.h"
+#include "core/io/json.h"
#include "core/string/translation.h"
#include "editor/editor_node.h"
+#include "editor/editor_paths.h"
#include "editor/editor_scale.h"
#include "editor/export/editor_export.h"
+#include "editor/plugins/script_editor_plugin.h"
#include "modules/modules_enabled.gen.h" // For mono and svg.
#ifdef MODULE_SVG_ENABLED
@@ -178,6 +182,8 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/launch_screens_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false));
+
Vector<PluginConfigIOS> found_plugins = get_plugins();
for (int i = 0; i < found_plugins.size(); i++) {
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), found_plugins[i].name)), false));
@@ -1475,13 +1481,19 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset>
}
Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
+ return _export_project_helper(p_preset, p_debug, p_path, p_flags, false, false);
+}
+
+Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags, bool p_simulator, bool p_skip_ipa) {
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
String src_pkg_name;
String dest_dir = p_path.get_base_dir() + "/";
String binary_name = p_path.get_file().get_basename();
- EditorProgress ep("export", "Exporting for iOS", 5, true);
+ bool export_project_only = p_preset->get("application/export_project_only");
+
+ EditorProgress ep("export", export_project_only ? TTR("Exporting for iOS (Project Files Only)") : TTR("Exporting for iOS"), export_project_only ? 2 : 5, true);
String team_id = p_preset->get("application/app_store_team_id");
ERR_FAIL_COND_V_MSG(team_id.length() == 0, ERR_CANT_OPEN, "App Store Team ID not specified - cannot configure the project.");
@@ -1843,6 +1855,10 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
}
}
+ if (export_project_only) {
+ return OK;
+ }
+
if (ep.step("Making .xcarchive", 3)) {
return ERR_SKIP;
}
@@ -1853,11 +1869,19 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
archive_args.push_back("-scheme");
archive_args.push_back(binary_name);
archive_args.push_back("-sdk");
- archive_args.push_back("iphoneos");
+ if (p_simulator) {
+ archive_args.push_back("iphonesimulator");
+ } else {
+ archive_args.push_back("iphoneos");
+ }
archive_args.push_back("-configuration");
archive_args.push_back(p_debug ? "Debug" : "Release");
archive_args.push_back("-destination");
- archive_args.push_back("generic/platform=iOS");
+ if (p_simulator) {
+ archive_args.push_back("generic/platform=iOS Simulator");
+ } else {
+ archive_args.push_back("generic/platform=iOS");
+ }
archive_args.push_back("archive");
archive_args.push_back("-allowProvisioningUpdates");
archive_args.push_back("-archivePath");
@@ -1871,26 +1895,27 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
return FAILED;
}
- if (ep.step("Making .ipa", 4)) {
- return ERR_SKIP;
- }
- List<String> export_args;
- export_args.push_back("-exportArchive");
- export_args.push_back("-archivePath");
- export_args.push_back(archive_path);
- export_args.push_back("-exportOptionsPlist");
- export_args.push_back(dest_dir + binary_name + "/export_options.plist");
- export_args.push_back("-allowProvisioningUpdates");
- export_args.push_back("-exportPath");
- export_args.push_back(dest_dir);
- String export_str;
- err = OS::get_singleton()->execute("xcodebuild", export_args, &export_str, nullptr, true);
- ERR_FAIL_COND_V(err, err);
-
- print_line("xcodebuild (.ipa):\n" + export_str);
- if (!export_str.contains("** EXPORT SUCCEEDED **")) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Xcode Build"), TTR(".ipa export failed, see editor log for details."));
- return FAILED;
+ if (!p_skip_ipa) {
+ if (ep.step("Making .ipa", 4)) {
+ return ERR_SKIP;
+ }
+ List<String> export_args;
+ export_args.push_back("-exportArchive");
+ export_args.push_back("-archivePath");
+ export_args.push_back(archive_path);
+ export_args.push_back("-exportOptionsPlist");
+ export_args.push_back(dest_dir + binary_name + "/export_options.plist");
+ export_args.push_back("-allowProvisioningUpdates");
+ export_args.push_back("-exportPath");
+ export_args.push_back(dest_dir);
+ String export_str;
+ err = OS::get_singleton()->execute("xcodebuild", export_args, &export_str, nullptr, true);
+ ERR_FAIL_COND_V(err, err);
+ print_line("xcodebuild (.ipa):\n" + export_str);
+ if (!export_str.contains("** EXPORT SUCCEEDED **")) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Xcode Build"), TTR(".ipa export failed, see editor log for details."));
+ return FAILED;
+ }
}
#else
add_message(EXPORT_MESSAGE_WARNING, TTR("Xcode Build"), TTR(".ipa can only be built on macOS. Leaving Xcode project without building the package."));
@@ -1900,16 +1925,15 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p
}
bool EditorExportPlatformIOS::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;
-
#ifdef MODULE_MONO_ENABLED
- err += TTR("Exporting to iOS is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target iOS with C#/Mono instead.") + "\n";
- err += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
// Don't check for additional errors, as this particular error cannot be resolved.
- r_error = err;
+ r_error += TTR("Exporting to iOS is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target iOS with C#/Mono instead.") + "\n";
+ r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
return false;
-#endif
+#else
+
+ String err;
+ bool valid = false;
// Look for export templates (first official, and if defined custom templates).
@@ -1937,6 +1961,7 @@ bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExp
}
return valid;
+#endif // !MODULE_MONO_ENABLED
}
bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
@@ -1972,26 +1997,419 @@ bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorEx
return valid;
}
+int EditorExportPlatformIOS::get_options_count() const {
+ MutexLock lock(device_lock);
+ return devices.size();
+}
+
+String EditorExportPlatformIOS::get_options_tooltip() const {
+ return TTR("Select device from the list");
+}
+
+Ref<ImageTexture> EditorExportPlatformIOS::get_option_icon(int p_index) const {
+ MutexLock lock(device_lock);
+
+ Ref<ImageTexture> icon;
+ if (p_index >= 0 || p_index < devices.size()) {
+ Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+ if (theme.is_valid()) {
+ if (devices[p_index].simulator) {
+ icon = theme->get_icon("IOSSimulator", "EditorIcons");
+ } else if (devices[p_index].wifi) {
+ icon = theme->get_icon("IOSDeviceWireless", "EditorIcons");
+ } else {
+ icon = theme->get_icon("IOSDeviceWired", "EditorIcons");
+ }
+ }
+ }
+ return icon;
+}
+
+String EditorExportPlatformIOS::get_option_label(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, devices.size(), "");
+ MutexLock lock(device_lock);
+ return devices[p_index].name;
+}
+
+String EditorExportPlatformIOS::get_option_tooltip(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, devices.size(), "");
+ MutexLock lock(device_lock);
+ return "UUID: " + devices[p_index].id;
+}
+
+bool EditorExportPlatformIOS::is_package_name_valid(const String &p_package, String *r_error) const {
+ String pname = p_package;
+
+ if (pname.length() == 0) {
+ if (r_error) {
+ *r_error = TTR("Identifier is missing.");
+ }
+ return false;
+ }
+
+ for (int i = 0; i < pname.length(); i++) {
+ char32_t c = pname[i];
+ if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) {
+ if (r_error) {
+ *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c));
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+#ifdef MACOS_ENABLED
+void EditorExportPlatformIOS::_check_for_changes_poll_thread(void *ud) {
+ EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud);
+
+ while (!ea->quit_request.is_set()) {
+ // Nothing to do if we already know the plugins have changed.
+ if (!ea->plugins_changed.is_set()) {
+ MutexLock lock(ea->plugins_lock);
+
+ Vector<PluginConfigIOS> loaded_plugins = get_plugins();
+
+ if (ea->plugins.size() != loaded_plugins.size()) {
+ ea->plugins_changed.set();
+ } else {
+ for (int i = 0; i < ea->plugins.size(); i++) {
+ if (ea->plugins[i].name != loaded_plugins[i].name || ea->plugins[i].last_updated != loaded_plugins[i].last_updated) {
+ ea->plugins_changed.set();
+ break;
+ }
+ }
+ }
+ }
+
+ // Check for devices updates.
+ Vector<Device> ldevices;
+
+ // Enum real devices.
+ String idepl = EDITOR_GET("export/ios/ios_deploy");
+ if (idepl.is_empty()) {
+ idepl = "ios-deploy";
+ }
+ {
+ String devices;
+ List<String> args;
+ args.push_back("-c");
+ args.push_back("-timeout");
+ args.push_back("1");
+ args.push_back("-j");
+ args.push_back("-u");
+ args.push_back("-I");
+
+ int ec = 0;
+ Error err = OS::get_singleton()->execute(idepl, args, &devices, &ec, true);
+ if (err == OK && ec == 0) {
+ Ref<JSON> json;
+ json.instantiate();
+ devices = "{ \"devices\":[" + devices.replace("}{", "},{") + "]}";
+ err = json->parse(devices);
+ if (err == OK) {
+ Dictionary data = json->get_data();
+ Array devices = data["devices"];
+ for (int i = 0; i < devices.size(); i++) {
+ Dictionary device_event = devices[i];
+ if (device_event["Event"] == "DeviceDetected") {
+ Dictionary device_info = device_event["Device"];
+ Device nd;
+ nd.id = device_info["DeviceIdentifier"];
+ nd.name = device_info["DeviceName"].operator String() + " (connected through " + device_event["Interface"].operator String() + ")";
+ nd.wifi = device_event["Interface"] == "WIFI";
+ nd.simulator = false;
+ ldevices.push_back(nd);
+ }
+ }
+ }
+ }
+ }
+
+ // Enum simulators
+ if (FileAccess::exists("/usr/bin/xcrun") || FileAccess::exists("/bin/xcrun")) {
+ String devices;
+ List<String> args;
+ args.push_back("simctl");
+ args.push_back("list");
+ args.push_back("devices");
+ args.push_back("-j");
+
+ int ec = 0;
+ Error err = OS::get_singleton()->execute("xcrun", args, &devices, &ec, true);
+ if (err == OK && ec == 0) {
+ Ref<JSON> json;
+ json.instantiate();
+ err = json->parse(devices);
+ if (err == OK) {
+ Dictionary data = json->get_data();
+ Dictionary devices = data["devices"];
+ for (const Variant *key = devices.next(nullptr); key; key = devices.next(key)) {
+ Array os_devices = devices[*key];
+ for (int i = 0; i < os_devices.size(); i++) {
+ Dictionary device_info = os_devices[i];
+ if (device_info["isAvailable"].operator bool() && device_info["state"] == "Booted") {
+ Device nd;
+ nd.id = device_info["udid"];
+ nd.name = device_info["name"].operator String() + " (simulator)";
+ nd.simulator = true;
+ ldevices.push_back(nd);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Update device list.
+ {
+ MutexLock lock(ea->device_lock);
+
+ bool different = false;
+
+ if (ea->devices.size() != ldevices.size()) {
+ different = true;
+ } else {
+ for (int i = 0; i < ea->devices.size(); i++) {
+ if (ea->devices[i].id != ldevices[i].id) {
+ different = true;
+ break;
+ }
+ }
+ }
+
+ if (different) {
+ ea->devices = ldevices;
+ ea->devices_changed.set();
+ }
+ }
+
+ uint64_t sleep = 200;
+ uint64_t wait = 3000000;
+ uint64_t time = OS::get_singleton()->get_ticks_usec();
+ while (OS::get_singleton()->get_ticks_usec() - time < wait) {
+ OS::get_singleton()->delay_usec(1000 * sleep);
+ if (ea->quit_request.is_set()) {
+ break;
+ }
+ }
+ }
+}
+#endif
+
+Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
+#ifdef MACOS_ENABLED
+ ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
+
+ String can_export_error;
+ bool can_export_missing_templates;
+ if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);
+ return ERR_UNCONFIGURED;
+ }
+
+ MutexLock lock(device_lock);
+
+ EditorProgress ep("run", vformat(TTR("Running on %s"), devices[p_device].name), 3);
+
+ String id = "tmpexport." + uitos(OS::get_singleton()->get_unix_time());
+
+ Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + EditorPaths::get_singleton()->get_cache_dir() + "'.");
+ filesystem_da->make_dir_recursive(EditorPaths::get_singleton()->get_cache_dir().path_join(id));
+ String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.ipa");
+
+#define CLEANUP_AND_RETURN(m_err) \
+ { \
+ if (filesystem_da->change_dir(EditorPaths::get_singleton()->get_cache_dir().path_join(id)) == OK) { \
+ filesystem_da->erase_contents_recursive(); \
+ filesystem_da->change_dir(".."); \
+ filesystem_da->remove(id); \
+ } \
+ return m_err; \
+ } \
+ ((void)0)
+
+ Device dev = devices[p_device];
+
+ // Export before sending to device.
+ Error err = _export_project_helper(p_preset, true, tmp_export_path, p_debug_flags, dev.simulator, true);
+
+ if (err != OK) {
+ CLEANUP_AND_RETURN(err);
+ }
+
+ Vector<String> cmd_args_list;
+ String host = EDITOR_GET("network/debug/remote_host");
+ int remote_port = (int)EDITOR_GET("network/debug/remote_port");
+
+ if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) {
+ host = "localhost";
+ }
+
+ if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) {
+ int port = EDITOR_GET("filesystem/file_server/port");
+ String passwd = EDITOR_GET("filesystem/file_server/password");
+ cmd_args_list.push_back("--remote-fs");
+ cmd_args_list.push_back(host + ":" + itos(port));
+ if (!passwd.is_empty()) {
+ cmd_args_list.push_back("--remote-fs-password");
+ cmd_args_list.push_back(passwd);
+ }
+ }
+
+ if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) {
+ cmd_args_list.push_back("--remote-debug");
+
+ cmd_args_list.push_back(get_debug_protocol() + host + ":" + String::num(remote_port));
+
+ List<String> breakpoints;
+ ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
+
+ if (breakpoints.size()) {
+ cmd_args_list.push_back("--breakpoints");
+ String bpoints;
+ for (const List<String>::Element *E = breakpoints.front(); E; E = E->next()) {
+ bpoints += E->get().replace(" ", "%20");
+ if (E->next()) {
+ bpoints += ",";
+ }
+ }
+
+ cmd_args_list.push_back(bpoints);
+ }
+ }
+
+ if (p_debug_flags & DEBUG_FLAG_VIEW_COLLISIONS) {
+ cmd_args_list.push_back("--debug-collisions");
+ }
+
+ if (p_debug_flags & DEBUG_FLAG_VIEW_NAVIGATION) {
+ cmd_args_list.push_back("--debug-navigation");
+ }
+
+ if (dev.simulator) {
+ // Deploy and run on simulator.
+ if (ep.step("Installing to simulator...", 3)) {
+ CLEANUP_AND_RETURN(ERR_SKIP);
+ } else {
+ List<String> args;
+ args.push_back("simctl");
+ args.push_back("install");
+ args.push_back(dev.id);
+ args.push_back(EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.xcarchive/Products/Applications/export.app"));
+
+ String log;
+ int ec;
+ err = OS::get_singleton()->execute("xcrun", args, &log, &ec, true);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start simctl executable."));
+ CLEANUP_AND_RETURN(err);
+ }
+ if (ec != 0) {
+ print_line("simctl install:\n" + log);
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation failed, see editor log for details."));
+ CLEANUP_AND_RETURN(ERR_UNCONFIGURED);
+ }
+ }
+
+ if (ep.step("Running on simulator...", 4)) {
+ CLEANUP_AND_RETURN(ERR_SKIP);
+ } else {
+ List<String> args;
+ args.push_back("simctl");
+ args.push_back("launch");
+ args.push_back(dev.id);
+ args.push_back(p_preset->get("application/bundle_identifier"));
+ for (const String &E : cmd_args_list) {
+ args.push_back(E);
+ }
+
+ String log;
+ int ec;
+ err = OS::get_singleton()->execute("xcrun", args, &log, &ec, true);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start simctl executable."));
+ CLEANUP_AND_RETURN(err);
+ }
+ if (ec != 0) {
+ print_line("simctl launch:\n" + log);
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Running failed, see editor log for details."));
+ }
+ }
+ } else {
+ // Deploy and run on real device.
+ if (ep.step("Installing and running on device...", 4)) {
+ CLEANUP_AND_RETURN(ERR_SKIP);
+ } else {
+ List<String> args;
+ args.push_back("-u");
+ args.push_back("-I");
+ args.push_back("--id");
+ args.push_back(dev.id);
+ args.push_back("--justlaunch");
+ args.push_back("--bundle");
+ args.push_back(EditorPaths::get_singleton()->get_cache_dir().path_join(id).path_join("export.xcarchive/Products/Applications/export.app"));
+ String app_args;
+ for (const String &E : cmd_args_list) {
+ app_args += E + " ";
+ }
+ if (!app_args.is_empty()) {
+ args.push_back("--args");
+ args.push_back(app_args);
+ }
+
+ String idepl = EDITOR_GET("export/ios/ios_deploy");
+ if (idepl.is_empty()) {
+ idepl = "ios-deploy";
+ }
+ String log;
+ int ec;
+ err = OS::get_singleton()->execute(idepl, args, &log, &ec, true);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Run"), TTR("Could not start ios-deploy executable."));
+ CLEANUP_AND_RETURN(err);
+ }
+ if (ec != 0) {
+ print_line("ios-deploy:\n" + log);
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Installation/running failed, see editor log for details."));
+ CLEANUP_AND_RETURN(ERR_UNCONFIGURED);
+ }
+ }
+ }
+
+ CLEANUP_AND_RETURN(OK);
+
+#undef CLEANUP_AND_RETURN
+#else
+ return ERR_UNCONFIGURED;
+#endif
+}
+
EditorExportPlatformIOS::EditorExportPlatformIOS() {
if (EditorNode::get_singleton()) {
#ifdef MODULE_SVG_ENABLED
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, _ios_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _ios_logo_svg, EDSCALE, upsample, false);
logo = ImageTexture::create_from_image(img);
+
+ ImageLoaderSVG::create_image_from_string(img, _ios_run_icon_svg, EDSCALE, upsample, false);
+ run_icon = ImageTexture::create_from_image(img);
#endif
plugins_changed.set();
-#ifndef ANDROID_ENABLED
+ devices_changed.set();
+#ifdef MACOS_ENABLED
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
#endif
}
}
EditorExportPlatformIOS::~EditorExportPlatformIOS() {
-#ifndef ANDROID_ENABLED
+#ifdef MACOS_ENABLED
quit_request.set();
if (check_for_changes_thread.is_started()) {
check_for_changes_thread.wait_to_finish();
diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h
index 6616bbd714..7de4c0b69d 100644
--- a/platform/ios/export/export_plugin.h
+++ b/platform/ios/export/export_plugin.h
@@ -45,6 +45,7 @@
#include "editor/editor_settings.h"
#include "editor/export/editor_export_platform.h"
#include "main/splash.gen.h"
+#include "scene/resources/image_texture.h"
#include <string.h>
#include <sys/stat.h>
@@ -58,15 +59,30 @@ class EditorExportPlatformIOS : public EditorExportPlatform {
GDCLASS(EditorExportPlatformIOS, EditorExportPlatform);
Ref<ImageTexture> logo;
+ Ref<ImageTexture> run_icon;
// Plugins
mutable SafeFlag plugins_changed;
-#ifndef ANDROID_ENABLED
+ SafeFlag devices_changed;
+
+ struct Device {
+ String id;
+ String name;
+ bool simulator = false;
+ bool wifi = false;
+ };
+
+ Vector<Device> devices;
+ Mutex device_lock;
+
+ Mutex plugins_lock;
+ mutable Vector<PluginConfigIOS> plugins;
+#ifdef MACOS_ENABLED
Thread check_for_changes_thread;
SafeFlag quit_request;
+
+ static void _check_for_changes_poll_thread(void *ud);
#endif
- Mutex plugins_lock;
- mutable Vector<PluginConfigIOS> plugins;
typedef Error (*FileHandler)(String p_file, void *p_userdata);
static Error _walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata);
@@ -122,64 +138,9 @@ class EditorExportPlatformIOS : public EditorExportPlatform {
Error _export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets);
Error _export_ios_plugins(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &dest_dir, Vector<IOSExportAsset> &r_exported_assets, bool p_debug);
- bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
- String pname = p_package;
-
- if (pname.length() == 0) {
- if (r_error) {
- *r_error = TTR("Identifier is missing.");
- }
- return false;
- }
-
- for (int i = 0; i < pname.length(); i++) {
- char32_t c = pname[i];
- if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) {
- if (r_error) {
- *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c));
- }
- return false;
- }
- }
-
- return true;
- }
+ Error _export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags, bool p_simulator, bool p_skip_ipa);
-#ifndef ANDROID_ENABLED
- static void _check_for_changes_poll_thread(void *ud) {
- EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud);
-
- while (!ea->quit_request.is_set()) {
- // Nothing to do if we already know the plugins have changed.
- if (!ea->plugins_changed.is_set()) {
- MutexLock lock(ea->plugins_lock);
-
- Vector<PluginConfigIOS> loaded_plugins = get_plugins();
-
- if (ea->plugins.size() != loaded_plugins.size()) {
- ea->plugins_changed.set();
- } else {
- for (int i = 0; i < ea->plugins.size(); i++) {
- if (ea->plugins[i].name != loaded_plugins[i].name || ea->plugins[i].last_updated != loaded_plugins[i].last_updated) {
- ea->plugins_changed.set();
- break;
- }
- }
- }
- }
-
- uint64_t wait = 3000000;
- uint64_t time = OS::get_singleton()->get_ticks_usec();
- while (OS::get_singleton()->get_ticks_usec() - time < wait) {
- OS::get_singleton()->delay_usec(300000);
-
- if (ea->quit_request.is_set()) {
- break;
- }
- }
- }
- }
-#endif
+ bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const;
protected:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
@@ -191,6 +152,23 @@ public:
virtual String get_name() const override { return "iOS"; }
virtual String get_os_name() const override { return "iOS"; }
virtual Ref<Texture2D> get_logo() const override { return logo; }
+ virtual Ref<Texture2D> get_run_icon() const override { return run_icon; }
+
+ virtual int get_options_count() const override;
+ virtual String get_options_tooltip() const override;
+ virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
+ virtual String get_option_label(int p_index) const override;
+ virtual String get_option_tooltip(int p_index) const override;
+ virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
+
+ virtual bool poll_export() override {
+ bool dc = devices_changed.is_set();
+ if (dc) {
+ // don't clear unless we're reporting true, to avoid race
+ devices_changed.clear();
+ }
+ return dc;
+ }
virtual bool should_update_export_options() override {
bool export_options_changed = plugins_changed.is_set();
diff --git a/platform/ios/export/run_icon.svg b/platform/ios/export/run_icon.svg
new file mode 100644
index 0000000000..859c58409e
--- /dev/null
+++ b/platform/ios/export/run_icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#bfbfbf" d="M.462 11.653H1.72V6.296H.462Zm.627-6.059a.687.687 0 0 0 .702-.682.688.688 0 0 0-.702-.687.687.687 0 0 0-.698.687c0 .38.309.682.698.682zM5.91 4.24c-2.127 0-3.461 1.45-3.461 3.77 0 2.32 1.333 3.765 3.461 3.765 2.123 0 3.457-1.445 3.457-3.765 0-2.32-1.334-3.77-3.457-3.77zm0 1.112c1.299 0 2.128 1.03 2.128 2.658 0 1.622-.829 2.653-2.128 2.653-1.304 0-2.127-1.03-2.127-2.653 0-1.627.823-2.658 2.127-2.658zm3.988 4.25c.055 1.344 1.157 2.173 2.835 2.173 1.764 0 2.876-.87 2.876-2.254 0-1.086-.627-1.698-2.108-2.037l-.839-.192c-.895-.212-1.263-.495-1.263-.98 0-.607.556-1.01 1.38-1.01.834 0 1.405.408 1.465 1.09h1.244c-.03-1.283-1.092-2.152-2.699-2.152-1.587 0-2.714.874-2.714 2.168 0 1.041.637 1.688 1.981 1.997l.945.222c.92.217 1.294.52 1.294 1.046 0 .606-.611 1.041-1.49 1.041-.89 0-1.562-.44-1.643-1.112H9.899Z" style="stroke-width:.502532;fill:#fff"/></svg>
diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm
index 461c226070..50102e02cc 100644
--- a/platform/ios/os_ios.mm
+++ b/platform/ios/os_ios.mm
@@ -257,7 +257,7 @@ Error OS_IOS::open_dynamic_library(const String p_path, void *&p_library_handle,
}
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + ".");
+ ERR_FAIL_COND_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;
diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py
index ce743fbf8a..a723bb5d58 100644
--- a/platform/linuxbsd/detect.py
+++ b/platform/linuxbsd/detect.py
@@ -291,6 +291,9 @@ def configure(env: "Environment"):
# No pkgconfig file so far, hardcode expected lib name.
env.Append(LIBS=["embree3"])
+ if not env["builtin_openxr"]:
+ env.ParseConfig("pkg-config openxr --cflags --libs")
+
if env["fontconfig"]:
if not env["use_sowrap"]:
if os.system("pkg-config --exists fontconfig") == 0: # 0 means found
diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp
index f74bdf3516..40151b1a02 100644
--- a/platform/linuxbsd/export/export_plugin.cpp
+++ b/platform/linuxbsd/export/export_plugin.cpp
@@ -521,11 +521,10 @@ EditorExportPlatformLinuxBSD::EditorExportPlatformLinuxBSD() {
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, _linuxbsd_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _linuxbsd_logo_svg, EDSCALE, upsample, false);
set_logo(ImageTexture::create_from_image(img));
- img_loader.create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false);
run_icon = ImageTexture::create_from_image(img);
#endif
diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h
index cef714e86e..21bd81ed2f 100644
--- a/platform/linuxbsd/export/export_plugin.h
+++ b/platform/linuxbsd/export/export_plugin.h
@@ -34,7 +34,7 @@
#include "core/io/file_access.h"
#include "editor/editor_settings.h"
#include "editor/export/editor_export_platform_pc.h"
-#include "scene/resources/texture.h"
+#include "scene/resources/image_texture.h"
class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC {
GDCLASS(EditorExportPlatformLinuxBSD, EditorExportPlatformPC);
diff --git a/platform/linuxbsd/export/run_icon.svg b/platform/linuxbsd/export/run_icon.svg
index 56465a0df3..ad58bcd5c7 100644
--- a/platform/linuxbsd/export/run_icon.svg
+++ b/platform/linuxbsd/export/run_icon.svg
@@ -1 +1 @@
-<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M7.941 13.966a3.62 3.62 0 0 1-.096.444 2.129 2.129 0 0 1-.31.668c.15.01.305.01.464.003.16.008.314.007.465-.003a2.129 2.129 0 0 1-.31-.668 3.62 3.62 0 0 1-.097-.444l-.058.001-.058-.001z" fill="#333" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/><path d="M10.688 10.793c-.297.005-.441.299-.707.33-.328.038-.533-.34-.996-.058-.463.283-.862 3.528.212 3.97 1.074.442 3.072-2.146 2.942-2.673-.13-.527-.542-.403-.747-.66-.206-.258 0-.666-.47-.86a.588.588 0 0 0-.234-.05zm-5.411 0a.62.62 0 0 0-.199.05c-.47.193-.264.601-.47.859-.205.257-.618.133-.748.66s1.867 3.115 2.942 2.673c1.074-.442.674-3.687.211-3.97-.463-.283-.668.096-.995.058-.277-.032-.42-.349-.741-.33z" fill="#f4bb37" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/><path d="M8 .914c-1.386 0-2.2.845-2.353 1.985-.153 1.14.094 1.348-.29 2.515s-2.103 3.168-2.063 5.013c.012.575.078 1.072.194 1.507a1.25 1.25 0 0 1 .503-.47 4.37 4.37 0 0 1 .204-.09c.004-.03.019-.098.046-.23.038-.182.183-.467.43-.654a4.773 4.773 0 0 1-.006-.172c-.029-1.431 1.45-2.982 1.723-3.888.272-.905.154-.998.199-1.223.045-.225.218-.487.468-.696a.11.11 0 0 1 .07-.028c.228-.003.456.826.897.827.44 0 .67-1.01.923-.799.25.21.423.471.468.696.045.225-.073.318.199 1.223.272.906 1.75 2.457 1.722 3.888-.001.058-.004.114-.007.17a1.2 1.2 0 0 1 .432.656c.027.132.042.2.046.23.027.01.085.037.204.092.153.07.36.236.502.47.115-.435.183-.933.195-1.509.04-1.845-1.681-3.846-2.065-5.013-.383-1.167-.135-1.376-.288-2.515C10.2 1.759 9.385.914 7.999.914Z" fill="#333" style="stroke-width:.472092;fill:#e0e0e0;fill-opacity:1"/></svg>
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M7.941 13.966a3.62 3.62 0 0 1-.096.444 2.129 2.129 0 0 1-.31.668c.15.01.305.01.464.003.16.008.314.007.465-.003a2.129 2.129 0 0 1-.31-.668 3.62 3.62 0 0 1-.097-.444zM8 .914c-1.386 0-2.2.845-2.353 1.985-.153 1.14.094 1.348-.29 2.515s-2.103 3.168-2.063 5.013c.012.575.078 1.072.194 1.507a1.25 1.25 0 0 1 .503-.47 4.37 4.37 0 0 1 .204-.09c.004-.03.019-.098.046-.23.038-.182.183-.467.43-.654a4.773 4.773 0 0 1-.006-.172c-.029-1.431 1.45-2.982 1.723-3.888.272-.905.154-.998.199-1.223.045-.225.218-.487.468-.696.253-.211.483.798.945.799.462 0 .692-1.01.945-.799.25.21.423.471.468.696.045.225-.073.318.199 1.223.272.906 1.75 2.457 1.722 3.888a4.773 4.773 0 0 0-.007.17 1.2 1.2 0 0 1 .432.656c.027.132.042.2.046.23.027.01.085.037.204.092.153.07.36.236.502.47.115-.435.183-.933.195-1.509.04-1.845-1.681-3.846-2.065-5.013-.383-1.167-.135-1.376-.288-2.515C10.2 1.759 9.385.914 7.999.914z" fill="#333" style="fill:#e0e0e0;fill-opacity:1"/><path d="M10.688 10.793c-.297.005-.441.299-.707.33-.328.038-.533-.34-.996-.058-.463.283-.862 3.528.212 3.97 1.074.442 3.072-2.146 2.942-2.673-.13-.527-.542-.403-.747-.66-.206-.258 0-.666-.47-.86a.588.588 0 0 0-.234-.05zm-5.411 0a.62.62 0 0 0-.199.05c-.47.193-.264.601-.47.859-.205.257-.618.133-.748.66s1.867 3.115 2.942 2.673c1.074-.442.674-3.687.211-3.97-.463-.283-.668.096-.995.058-.277-.032-.42-.349-.741-.33z" fill="#f4bb37" style="fill:#e0e0e0;fill-opacity:1"/></svg>
diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp
index ab79885fb4..342cff82e9 100644
--- a/platform/linuxbsd/joypad_linux.cpp
+++ b/platform/linuxbsd/joypad_linux.cpp
@@ -98,19 +98,20 @@ static bool detect_sandbox() {
JoypadLinux::JoypadLinux(Input *in) {
#ifdef UDEV_ENABLED
-#ifdef SOWRAP_ENABLED
-#ifdef DEBUG_ENABLED
- int dylibloader_verbose = 1;
-#else
- int dylibloader_verbose = 0;
-#endif
if (detect_sandbox()) {
// Linux binaries in sandboxes / containers need special handling because
// libudev doesn't work there. So we need to fallback to manual parsing
// of /dev/input in such case.
use_udev = false;
print_verbose("JoypadLinux: udev enabled, but detected incompatible sandboxed mode. Falling back to /dev/input to detect joypads.");
- } else {
+ }
+#ifdef SOWRAP_ENABLED
+ else {
+#ifdef DEBUG_ENABLED
+ int dylibloader_verbose = 1;
+#else
+ int dylibloader_verbose = 0;
+#endif
use_udev = initialize_libudev(dylibloader_verbose) == 0;
if (use_udev) {
if (!udev_new || !udev_unref || !udev_enumerate_new || !udev_enumerate_add_match_subsystem || !udev_enumerate_scan_devices || !udev_enumerate_get_list_entry || !udev_list_entry_get_next || !udev_list_entry_get_name || !udev_device_new_from_syspath || !udev_device_get_devnode || !udev_device_get_action || !udev_device_unref || !udev_enumerate_unref || !udev_monitor_new_from_netlink || !udev_monitor_filter_add_match_subsystem_devtype || !udev_monitor_enable_receiving || !udev_monitor_get_fd || !udev_monitor_receive_device || !udev_monitor_unref) {
@@ -124,10 +125,11 @@ JoypadLinux::JoypadLinux(Input *in) {
print_verbose("JoypadLinux: udev enabled, but couldn't be loaded. Falling back to /dev/input to detect joypads.");
}
}
-#endif
+#endif // SOWRAP_ENABLED
#else
print_verbose("JoypadLinux: udev disabled, parsing /dev/input to detect joypads.");
-#endif
+#endif // UDEV_ENABLED
+
input = in;
monitor_joypads_thread.start(monitor_joypads_thread_func, this);
joypad_events_thread.start(joypad_events_thread_func, this);
@@ -391,6 +393,16 @@ void JoypadLinux::open_joypad(const char *p_path) {
return;
}
+ uint16_t vendor = BSWAP16(inpid.vendor);
+ uint16_t product = BSWAP16(inpid.product);
+ uint16_t version = BSWAP16(inpid.version);
+
+ if (input->should_ignore_device(vendor, product)) {
+ // This can be true in cases where Steam is passing information into the game to ignore
+ // original gamepads when using virtual rebindings (See SteamInput).
+ return;
+ }
+
MutexLock lock(joypads_mutex[joy_num]);
Joypad &joypad = joypads[joy_num];
joypad.reset();
@@ -399,10 +411,6 @@ void JoypadLinux::open_joypad(const char *p_path) {
setup_joypad_properties(joypad);
sprintf(uid, "%04x%04x", BSWAP16(inpid.bustype), 0);
if (inpid.vendor && inpid.product && inpid.version) {
- uint16_t vendor = BSWAP16(inpid.vendor);
- uint16_t product = BSWAP16(inpid.product);
- uint16_t version = BSWAP16(inpid.version);
-
sprintf(uid + String(uid).length(), "%04x%04x%04x%04x%04x%04x", vendor, 0, product, 0, version, 0);
input->joy_connection_changed(joy_num, true, name, uid);
} else {
diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp
index 310778388b..14d02a73c8 100644
--- a/platform/linuxbsd/os_linuxbsd.cpp
+++ b/platform/linuxbsd/os_linuxbsd.cpp
@@ -954,45 +954,33 @@ static String get_mountpoint(const String &p_path) {
}
Error OS_LinuxBSD::move_to_trash(const String &p_path) {
- String path = p_path.rstrip("/"); // Strip trailing slash when path points to a directory
+ // We try multiple methods, until we find one that works.
+ // So we only return on success until we exhausted possibilities.
+ String path = p_path.rstrip("/"); // Strip trailing slash when path points to a directory.
int err_code;
List<String> args;
args.push_back(path);
- args.push_front("trash"); // The command is `gio trash <file_name>` so we need to add it to args.
+
+ args.push_front("trash"); // The command is `gio trash <file_name>` so we add it before the path.
Error result = execute("gio", args, nullptr, &err_code); // For GNOME based machines.
- if (result == OK) { // The `execute` function has done its job without errors.
- if (!err_code) { // The shell command has been executed without errors.
- return OK;
- } else if (err_code == 1) {
- ERR_PRINT("move_to_trash: No such file or directory as " + path + ".");
- return ERR_FILE_NOT_FOUND;
- }
+ if (result == OK && err_code == 0) { // Success.
+ return OK;
}
args.pop_front();
args.push_front("move");
args.push_back("trash:/"); // The command is `kioclient5 move <file_name> trash:/`.
result = execute("kioclient5", args, nullptr, &err_code); // For KDE based machines.
- if (result == OK) { // The `execute` function has done its job without errors.
- if (!err_code) { // The shell command has been executed without errors.
- return OK;
- } else if (err_code == 1) {
- ERR_PRINT("move_to_trash: No such file or directory as " + path + ".");
- return ERR_FILE_NOT_FOUND;
- }
+ if (result == OK && err_code == 0) {
+ return OK;
}
args.pop_front();
args.pop_back();
result = execute("gvfs-trash", args, nullptr, &err_code); // For older Linux machines.
- if (result == OK) { // The `execute` function has done its job without errors.
- if (!err_code) { // The shell command has been executed without errors.
- return OK;
- } else if (err_code == 1) {
- ERR_PRINT("move_to_trash: No such file or directory as " + path + ".");
- return ERR_FILE_NOT_FOUND;
- }
+ if (result == OK && err_code == 0) {
+ return OK;
}
// If the commands `kioclient5`, `gio` or `gvfs-trash` don't work on the system we do it manually.
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index 8724bc871a..2643cd3b1a 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -40,7 +40,7 @@
#include "core/string/print_string.h"
#include "core/string/ustring.h"
#include "main/main.h"
-#include "scene/resources/texture.h"
+#include "scene/resources/atlas_texture.h"
#if defined(VULKAN_ENABLED)
#include "servers/rendering/renderer_rd/renderer_compositor_rd.h"
@@ -1449,6 +1449,12 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) {
DEBUG_LOG_X11("delete_sub_window: %lu (%u) \n", wd.x11_window, p_id);
+ window_set_rect_changed_callback(Callable(), p_id);
+ window_set_window_event_callback(Callable(), p_id);
+ window_set_input_event_callback(Callable(), p_id);
+ window_set_input_text_callback(Callable(), p_id);
+ window_set_drop_files_callback(Callable(), p_id);
+
while (wd.transient_children.size()) {
window_set_transient(*wd.transient_children.begin(), INVALID_WINDOW_ID);
}
@@ -2980,6 +2986,30 @@ Key DisplayServerX11::keyboard_get_keycode_from_physical(Key p_keycode) const {
return (Key)(key | modifiers);
}
+Key DisplayServerX11::keyboard_get_label_from_physical(Key p_keycode) const {
+ Key modifiers = p_keycode & KeyModifierMask::MODIFIER_MASK;
+ Key keycode_no_mod = p_keycode & KeyModifierMask::CODE_MASK;
+ unsigned int xkeycode = KeyMappingX11::get_xlibcode(keycode_no_mod);
+ KeySym xkeysym = XkbKeycodeToKeysym(x11_display, xkeycode, keyboard_get_current_layout(), 0);
+ if (is_ascii_lower_case(xkeysym)) {
+ xkeysym -= ('a' - 'A');
+ }
+
+ Key key = KeyMappingX11::get_keycode(xkeysym);
+#ifdef XKB_ENABLED
+ if (xkb_loaded_v08p) {
+ String keysym = String::chr(xkb_keysym_to_utf32(xkb_keysym_to_upper(xkeysym)));
+ key = fix_key_label(keysym[0], KeyMappingX11::get_keycode(xkeysym));
+ }
+#endif
+
+ // If not found, fallback to QWERTY.
+ // This should match the behavior of the event pump
+ if (key == Key::NONE) {
+ return p_keycode;
+ }
+ return (Key)(key | modifiers);
+}
DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, Window p_window, Atom p_property) {
Atom actual_type = None;
int actual_format = 0;
@@ -4880,6 +4910,8 @@ void DisplayServerX11::set_icon(const Ref<Image> &p_icon) {
Atom net_wm_icon = XInternAtom(x11_display, "_NET_WM_ICON", False);
if (p_icon.is_valid()) {
+ ERR_FAIL_COND(p_icon->get_width() <= 0 || p_icon->get_height() <= 0);
+
Ref<Image> img = p_icon->duplicate();
img->convert(Image::FORMAT_RGBA8);
@@ -5449,7 +5481,9 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode
}
#else
#ifdef XKB_ENABLED
- xkb_loaded = true;
+ bool xkb_loaded = true;
+ xkb_loaded_v05p = true;
+ xkb_loaded_v08p = true;
#endif
#endif
@@ -5476,6 +5510,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode
r_error = OK;
+#ifdef SOWRAP_ENABLED
{
if (!XcursorImageCreate || !XcursorImageLoadCursor || !XcursorImageDestroy || !XcursorGetDefaultSize || !XcursorGetTheme || !XcursorLibraryLoadImage) {
// There's no API to check version, check if functions are available instead.
@@ -5484,6 +5519,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode
return;
}
}
+#endif
for (int i = 0; i < CURSOR_MAX; i++) {
cursors[i] = None;
diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h
index 180362923b..70703d42c3 100644
--- a/platform/linuxbsd/x11/display_server_x11.h
+++ b/platform/linuxbsd/x11/display_server_x11.h
@@ -502,6 +502,7 @@ public:
virtual String keyboard_get_layout_language(int p_index) const override;
virtual String keyboard_get_layout_name(int p_index) const override;
virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override;
+ virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;
virtual void process_events() override;
diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h
index 93fa93b259..e5e0e53bfb 100644
--- a/platform/macos/display_server_macos.h
+++ b/platform/macos/display_server_macos.h
@@ -315,6 +315,8 @@ public:
virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override;
virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
+ virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+
virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;
@@ -326,6 +328,9 @@ public:
virtual void clipboard_set(const String &p_text) override;
virtual String clipboard_get() const override;
+ virtual Ref<Image> clipboard_get_image() const override;
+ virtual bool clipboard_has() const override;
+ virtual bool clipboard_has_image() const override;
virtual int get_screen_count() const override;
virtual int get_primary_screen() const override;
@@ -433,6 +438,7 @@ public:
virtual String keyboard_get_layout_language(int p_index) const override;
virtual String keyboard_get_layout_name(int p_index) const override;
virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override;
+ virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;
virtual void process_events() override;
virtual void force_process_and_drop_events() override;
diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm
index 5ccef68e7f..d64bb5211e 100644
--- a/platform/macos/display_server_macos.mm
+++ b/platform/macos/display_server_macos.mm
@@ -44,8 +44,10 @@
#include "core/io/marshalls.h"
#include "core/math/geometry_2d.h"
#include "core/os/keyboard.h"
+#include "drivers/png/png_driver_common.h"
#include "main/main.h"
-#include "scene/resources/texture.h"
+#include "scene/resources/atlas_texture.h"
+#include "scene/resources/image_texture.h"
#if defined(GLES3_ENABLED)
#include "drivers/gles3/rasterizer_gles3.h"
@@ -1847,6 +1849,176 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect
return OK;
}
+Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+ _THREAD_SAFE_METHOD_
+
+ NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()];
+ NSMutableArray *allowed_types = [[NSMutableArray alloc] init];
+ bool allow_other = false;
+ for (int i = 0; i < p_filters.size(); i++) {
+ Vector<String> tokens = p_filters[i].split(";");
+ if (tokens.size() > 0) {
+ if (tokens[0].strip_edges() == "*.*") {
+ allow_other = true;
+ } else {
+ [allowed_types addObject:[NSString stringWithUTF8String:tokens[0].replace("*.", "").strip_edges().utf8().get_data()]];
+ }
+ }
+ }
+
+ Callable callback = p_callback; // Make a copy for async completion handler.
+ switch (p_mode) {
+ case FILE_DIALOG_MODE_SAVE_FILE: {
+ NSSavePanel *panel = [NSSavePanel savePanel];
+
+ [panel setDirectoryURL:[NSURL fileURLWithPath:url]];
+ if ([allowed_types count]) {
+ [panel setAllowedFileTypes:allowed_types];
+ }
+ [panel setAllowsOtherFileTypes:allow_other];
+ [panel setExtensionHidden:YES];
+ [panel setCanSelectHiddenExtension:YES];
+ [panel setCanCreateDirectories:YES];
+ [panel setShowsHiddenFiles:p_show_hidden];
+ if (p_filename != "") {
+ NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
+ [panel setNameFieldStringValue:fileurl];
+ }
+
+ [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow]
+ completionHandler:^(NSInteger ret) {
+ if (ret == NSModalResponseOK) {
+ // Save bookmark for folder.
+ if (OS::get_singleton()->is_sandboxed()) {
+ NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+ bool skip = false;
+ for (id bookmark in bookmarks) {
+ NSError *error = nil;
+ BOOL isStale = NO;
+ NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+ if (!error && !isStale && ([[exurl path] compare:[[panel directoryURL] path]] == NSOrderedSame)) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ NSError *error = nil;
+ NSData *bookmark = [[panel directoryURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
+ if (!error) {
+ NSArray *new_bookmarks = [bookmarks arrayByAddingObject:bookmark];
+ [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
+ }
+ }
+ }
+ // Callback.
+ Vector<String> files;
+ String url;
+ url.parse_utf8([[[panel URL] path] UTF8String]);
+ files.push_back(url);
+ if (!callback.is_null()) {
+ Variant v_status = true;
+ Variant v_files = files;
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ } else {
+ if (!callback.is_null()) {
+ Variant v_status = false;
+ Variant v_files = Vector<String>();
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ }
+ }];
+ } break;
+ case FILE_DIALOG_MODE_OPEN_ANY:
+ case FILE_DIALOG_MODE_OPEN_FILE:
+ case FILE_DIALOG_MODE_OPEN_FILES:
+ case FILE_DIALOG_MODE_OPEN_DIR: {
+ NSOpenPanel *panel = [NSOpenPanel openPanel];
+
+ [panel setDirectoryURL:[NSURL fileURLWithPath:url]];
+ if ([allowed_types count]) {
+ [panel setAllowedFileTypes:allowed_types];
+ }
+ [panel setAllowsOtherFileTypes:allow_other];
+ [panel setExtensionHidden:YES];
+ [panel setCanSelectHiddenExtension:YES];
+ [panel setCanCreateDirectories:YES];
+ [panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)];
+ [panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)];
+ [panel setShowsHiddenFiles:p_show_hidden];
+ if (p_filename != "") {
+ NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
+ [panel setNameFieldStringValue:fileurl];
+ }
+ [panel setAllowsMultipleSelection:(p_mode == FILE_DIALOG_MODE_OPEN_FILES)];
+
+ [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow]
+ completionHandler:^(NSInteger ret) {
+ if (ret == NSModalResponseOK) {
+ // Save bookmark for folder.
+ NSArray *urls = [(NSOpenPanel *)panel URLs];
+ if (OS::get_singleton()->is_sandboxed()) {
+ NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+ NSMutableArray *new_bookmarks = [bookmarks mutableCopy];
+ for (NSUInteger i = 0; i != [urls count]; ++i) {
+ bool skip = false;
+ for (id bookmark in bookmarks) {
+ NSError *error = nil;
+ BOOL isStale = NO;
+ NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+ if (!error && !isStale && ([[exurl path] compare:[[urls objectAtIndex:i] path]] == NSOrderedSame)) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ NSError *error = nil;
+ NSData *bookmark = [[urls objectAtIndex:i] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
+ if (!error) {
+ [new_bookmarks addObject:bookmark];
+ }
+ }
+ }
+ [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
+ }
+ // Callback.
+ Vector<String> files;
+ for (NSUInteger i = 0; i != [urls count]; ++i) {
+ String url;
+ url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]);
+ files.push_back(url);
+ }
+ if (!callback.is_null()) {
+ Variant v_status = true;
+ Variant v_files = files;
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ } else {
+ if (!callback.is_null()) {
+ Variant v_status = false;
+ Variant v_files = Vector<String>();
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ }
+ }];
+ } break;
+ }
+
+ return OK;
+}
+
Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) {
_THREAD_SAFE_METHOD_
@@ -2100,6 +2272,37 @@ String DisplayServerMacOS::clipboard_get() const {
return ret;
}
+Ref<Image> DisplayServerMacOS::clipboard_get_image() const {
+ Ref<Image> image;
+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+ NSString *result = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSPasteboardTypeTIFF, NSPasteboardTypePNG, nil]];
+ if (!result) {
+ return image;
+ }
+ NSData *data = [pasteboard dataForType:result];
+ if (!data) {
+ return image;
+ }
+ NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:data];
+ NSData *pngData = [bitmap representationUsingType:NSPNGFileType properties:@{}];
+ image.instantiate();
+ PNGDriverCommon::png_to_image((const uint8_t *)pngData.bytes, pngData.length, false, image);
+ return image;
+}
+
+bool DisplayServerMacOS::clipboard_has() const {
+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+ NSArray *classArray = [NSArray arrayWithObject:[NSString class]];
+ NSDictionary *options = [NSDictionary dictionary];
+ return [pasteboard canReadObjectForClasses:classArray options:options];
+}
+
+bool DisplayServerMacOS::clipboard_has_image() const {
+ NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
+ NSString *result = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSPasteboardTypeTIFF, NSPasteboardTypePNG, nil]];
+ return result;
+}
+
int DisplayServerMacOS::get_screen_count() const {
_THREAD_SAFE_METHOD_
@@ -3085,14 +3288,14 @@ bool DisplayServerMacOS::window_is_focused(WindowID p_window) const {
}
bool DisplayServerMacOS::window_can_draw(WindowID p_window) const {
- return window_get_mode(p_window) != WINDOW_MODE_MINIMIZED;
+ return (window_get_mode(p_window) != WINDOW_MODE_MINIMIZED) && [windows[p_window].window_object isOnActiveSpace];
}
bool DisplayServerMacOS::can_any_window_draw() const {
_THREAD_SAFE_METHOD_
for (const KeyValue<WindowID, WindowData> &E : windows) {
- if (window_get_mode(E.key) != WINDOW_MODE_MINIMIZED) {
+ if ((window_get_mode(E.key) != WINDOW_MODE_MINIMIZED) && [E.value.window_object isOnActiveSpace]) {
return true;
}
}
@@ -3499,6 +3702,17 @@ Key DisplayServerMacOS::keyboard_get_keycode_from_physical(Key p_keycode) const
return (Key)(KeyMappingMacOS::remap_key(macos_keycode, 0, false) | modifiers);
}
+Key DisplayServerMacOS::keyboard_get_label_from_physical(Key p_keycode) const {
+ if (p_keycode == Key::PAUSE || p_keycode == Key::NONE) {
+ return p_keycode;
+ }
+
+ Key modifiers = p_keycode & KeyModifierMask::MODIFIER_MASK;
+ Key keycode_no_mod = p_keycode & KeyModifierMask::CODE_MASK;
+ unsigned int macos_keycode = KeyMappingMacOS::unmap_key(keycode_no_mod);
+ return (Key)(KeyMappingMacOS::remap_key(macos_keycode, 0, true) | modifiers);
+}
+
void DisplayServerMacOS::process_events() {
_THREAD_SAFE_METHOD_
@@ -3609,40 +3823,46 @@ void DisplayServerMacOS::set_native_icon(const String &p_filename) {
void DisplayServerMacOS::set_icon(const Ref<Image> &p_icon) {
_THREAD_SAFE_METHOD_
- Ref<Image> img = p_icon;
- img = img->duplicate();
- img->convert(Image::FORMAT_RGBA8);
- NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
- initWithBitmapDataPlanes:nullptr
- pixelsWide:img->get_width()
- pixelsHigh:img->get_height()
- bitsPerSample:8
- samplesPerPixel:4
- hasAlpha:YES
- isPlanar:NO
- colorSpaceName:NSDeviceRGBColorSpace
- bytesPerRow:img->get_width() * 4
- bitsPerPixel:32];
- ERR_FAIL_COND(imgrep == nil);
- uint8_t *pixels = [imgrep bitmapData];
+ if (p_icon.is_valid()) {
+ ERR_FAIL_COND(p_icon->get_width() <= 0 || p_icon->get_height() <= 0);
- int len = img->get_width() * img->get_height();
- const uint8_t *r = img->get_data().ptr();
+ Ref<Image> img = p_icon->duplicate();
+ img->convert(Image::FORMAT_RGBA8);
- /* Premultiply the alpha channel */
- for (int i = 0; i < len; i++) {
- uint8_t alpha = r[i * 4 + 3];
- pixels[i * 4 + 0] = (uint8_t)(((uint16_t)r[i * 4 + 0] * alpha) / 255);
- pixels[i * 4 + 1] = (uint8_t)(((uint16_t)r[i * 4 + 1] * alpha) / 255);
- pixels[i * 4 + 2] = (uint8_t)(((uint16_t)r[i * 4 + 2] * alpha) / 255);
- pixels[i * 4 + 3] = alpha;
- }
+ NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc]
+ initWithBitmapDataPlanes:nullptr
+ pixelsWide:img->get_width()
+ pixelsHigh:img->get_height()
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bytesPerRow:img->get_width() * 4
+ bitsPerPixel:32];
+ ERR_FAIL_COND(imgrep == nil);
+ uint8_t *pixels = [imgrep bitmapData];
- NSImage *nsimg = [[NSImage alloc] initWithSize:NSMakeSize(img->get_width(), img->get_height())];
- ERR_FAIL_COND(nsimg == nil);
+ int len = img->get_width() * img->get_height();
+ const uint8_t *r = img->get_data().ptr();
- [nsimg addRepresentation:imgrep];
- [NSApp setApplicationIconImage:nsimg];
+ /* Premultiply the alpha channel */
+ for (int i = 0; i < len; i++) {
+ uint8_t alpha = r[i * 4 + 3];
+ pixels[i * 4 + 0] = (uint8_t)(((uint16_t)r[i * 4 + 0] * alpha) / 255);
+ pixels[i * 4 + 1] = (uint8_t)(((uint16_t)r[i * 4 + 1] * alpha) / 255);
+ pixels[i * 4 + 2] = (uint8_t)(((uint16_t)r[i * 4 + 2] * alpha) / 255);
+ pixels[i * 4 + 3] = alpha;
+ }
+
+ NSImage *nsimg = [[NSImage alloc] initWithSize:NSMakeSize(img->get_width(), img->get_height())];
+ ERR_FAIL_COND(nsimg == nil);
+
+ [nsimg addRepresentation:imgrep];
+ [NSApp setApplicationIconImage:nsimg];
+ } else {
+ [NSApp setApplicationIconImage:nil];
+ }
}
DisplayServer *DisplayServerMacOS::create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error) {
diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
index 9199701eb3..6af816989d 100644
--- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
+++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
@@ -96,6 +96,9 @@
<member name="codesign/entitlements/app_sandbox/files_pictures" type="int" setter="" getter="">
Allows read or write access to the user's "Pictures" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_pictures_read-write]com.apple.security.files.pictures.read-write[/url].
</member>
+ <member name="codesign/entitlements/app_sandbox/files_user_selected" type="int" setter="" getter="">
+ Allows read or write access to the locations the user has selected using a native file dialog. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_user-selected_read-write]com.apple.security.files.user-selected.read-write[/url].
+ </member>
<member name="codesign/entitlements/app_sandbox/helper_executables" type="Array" setter="" getter="">
List of helper executables to embedded to the app bundle. Sandboxed app are limited to execute only these executable. See [url=https://developer.apple.com/documentation/xcode/embedding-a-helper-tool-in-a-sandboxed-app]Embedding a command-line tool in a sandboxed app[/url].
</member>
diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp
index 2d185db812..81f9707f6b 100644
--- a/platform/macos/export/export_plugin.cpp
+++ b/platform/macos/export/export_plugin.cpp
@@ -41,6 +41,7 @@
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/editor_scale.h"
+#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For svg and regex.
#ifdef MODULE_SVG_ENABLED
@@ -425,6 +426,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
@@ -1359,7 +1361,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
String src_pkg_name;
- EditorProgress ep("export", "Exporting for macOS", 3, true);
+ EditorProgress ep("export", TTR("Exporting for macOS"), 3, true);
if (p_debug) {
src_pkg_name = p_preset->get("custom_template/debug");
@@ -1922,6 +1924,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");
ent_f->store_line("<true/>");
}
+ if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) {
+ ent_f->store_line("<key>com.apple.security.files.user-selected.read-only</key>");
+ ent_f->store_line("<true/>");
+ }
+ if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) {
+ ent_f->store_line("<key>com.apple.security.files.user-selected.read-write</key>");
+ ent_f->store_line("<true/>");
+ }
}
ent_f->store_line("</dict>");
@@ -2442,11 +2452,10 @@ EditorExportPlatformMacOS::EditorExportPlatformMacOS() {
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, _macos_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false);
logo = ImageTexture::create_from_image(img);
- img_loader.create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false);
run_icon = ImageTexture::create_from_image(img);
#endif
diff --git a/platform/macos/export/run_icon.svg b/platform/macos/export/run_icon.svg
index c7067bb4b6..647270ce22 100644
--- a/platform/macos/export/run_icon.svg
+++ b/platform/macos/export/run_icon.svg
@@ -1 +1 @@
-<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path style="color:#000;fill:#e0e0e0;fill-opacity:1;stroke-width:.93168;-inkscape-stroke:none" d="M4.418 1.055c-1.82 0-3.365 1.537-3.365 3.363v7.164c0 1.829 1.548 3.363 3.365 3.363h7.164c1.828 0 3.363-1.544 3.363-3.363V4.418c0-1.822-1.537-3.363-3.363-3.363H7.729Zm3.875 1.164h3.291c1.149 0 2.2 1.053 2.2 2.199v7.164c0 1.14-1.052 2.2-2.2 2.2h-2.15a8.884 8.884 0 0 1-.358-1.598c-.135.02-.487.082-.693.117a3.947 3.947 0 0 1-.049.004l-.008-.002a7.345 7.345 0 0 1-1.205-.004 7.114 7.114 0 0 1-.926-.139 6.057 6.057 0 0 1-.867-.271 3.843 3.843 0 0 1-.988-.566 3.214 3.214 0 0 1-.397-.378 2.8 2.8 0 0 1-.318-.441.558.558 0 0 1-.059-.424.564.564 0 0 1 .881-.299.56.56 0 0 1 .145.164c.083.138.188.26.312.362.096.082.2.158.307.224.12.075.243.142.371.201.285.139.583.247.89.319a5.35 5.35 0 0 0 1.282.158c.065 0 .129-.005.184-.006.056 0 .102-.005.148-.008l.096-.006c.114-.009.228-.02.31-.032.083-.013.11-.021.143-.028.099-.022.204-.058.327-.089a28.438 28.438 0 0 1-.06-1.929V8.53H6.887c.048-1.963.746-4.357 1.181-5.677.1-.293.184-.527.225-.633ZM4.973 5.03h.002a.562.562 0 0 1 .558.559v.805a.556.556 0 0 1-.558.558.56.56 0 0 1-.397-.162.565.565 0 0 1-.164-.396V5.59a.561.561 0 0 1 .559-.559Z"/><path style="color:#000;fill:#e0e0e0;fill-opacity:1;stroke-linecap:round;-inkscape-stroke:none" d="M26.117 11.467c.008.11.014.225.022.328.022.283.052.565.088.846l.012.053c1.238-.252 2.448-.829 3.011-1.803a.6.6 0 1 0-1.039-.6c-.28.486-1.161.936-2.094 1.176z" transform="translate(-15.37 .357) scale(.93168)"/><g style="fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-opacity:1"><path style="color:#000;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-width:1.2;stroke-linecap:round;stroke-opacity:1;-inkscape-stroke:none" d="M27.836 5.585v.862" transform="translate(-15.37 .357) scale(.93168)"/><path style="color:#000;fill:#e0e0e0;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-opacity:1;-inkscape-stroke:none" d="M27.836 4.984a.6.6 0 0 0-.6.6v.863a.6.6 0 0 0 .6.6.6.6 0 0 0 .6-.6v-.863a.6.6 0 0 0-.6-.6Z" transform="translate(-15.37 .357) scale(.93168)"/></g></svg>
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path fill="#e0e0e0" d="M4.418 1.055a3.364 3.364 0 0 0-3.364 3.364v7.164a3.364 3.364 0 0 0 3.364 3.364h7.164a3.364 3.364 0 0 0 3.364-3.364V4.418a3.364 3.364 0 0 0-3.364-3.364H7.729Zm3.875 1.164h3.291a2.2 2.2 0 0 1 2.2 2.2v7.164a2.2 2.2 0 0 1-2.2 2.2h-2.15a8.884 8.884 0 0 1-.358-1.598c-.135.02-.487.082-.693.117a7.345 7.345 0 0 1-1.254 0 7.114 7.114 0 0 1-.926-.139 6.057 6.057 0 0 1-.867-.271 3.843 3.843 0 0 1-.988-.566 3.214 3.214 0 0 1-.397-.378 2.8 2.8 0 0 1-.318-.441.56.56 0 0 1 .968-.56c.083.138.188.26.312.362.096.082.2.158.307.224.12.075.243.142.371.201.285.139.583.247.89.319a5.35 5.35 0 0 0 1.282.158c.065 0 .129-.005.184-.006.056 0 .102-.005.148-.008l.096-.006c.114-.009.228-.02.31-.032.083-.013.11-.021.143-.028.099-.022.204-.058.327-.089a28.438 28.438 0 0 1-.06-1.929V8.53H6.887c.048-1.963.746-4.357 1.181-5.677.1-.293.184-.527.225-.633ZM5.531 6.394a.56.56 0 0 1-1.118 0v-.9a.56.56 0 0 1 1.118 0Zm3.432 4.646.02.306c.02.264.049.527.082.788l.011.05c1.154-.235 2.281-.773 2.806-1.68a.56.56 0 1 0-.968-.56c-.261.454-1.082.873-1.951 1.097zM10 6.364a.56.56 0 0 0 1.118 0v-.9a.56.56 0 0 0-1.118 0z"/></svg>
diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h
index ab61649d19..ae94b6296d 100644
--- a/platform/macos/os_macos.h
+++ b/platform/macos/os_macos.h
@@ -113,6 +113,10 @@ public:
virtual String get_unique_id() const override;
virtual String get_processor_name() const override;
+ virtual bool is_sandboxed() const override;
+ virtual Vector<String> get_granted_permissions() const override;
+ virtual void revoke_granted_permissions() override;
+
virtual bool _check_internal_feature_support(const String &p_feature) override;
virtual void disable_crash_handler() override;
diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm
index fe6d8d9fb0..c17ea95f4f 100644
--- a/platform/macos/os_macos.mm
+++ b/platform/macos/os_macos.mm
@@ -76,6 +76,36 @@ String OS_MacOS::get_processor_name() const {
ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name. Returning an empty string."));
}
+bool OS_MacOS::is_sandboxed() const {
+ return has_environment("APP_SANDBOX_CONTAINER_ID");
+}
+
+Vector<String> OS_MacOS::get_granted_permissions() const {
+ Vector<String> ret;
+
+ if (is_sandboxed()) {
+ NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+ for (id bookmark in bookmarks) {
+ NSError *error = nil;
+ BOOL isStale = NO;
+ NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+ if (!error && !isStale) {
+ String url_string;
+ url_string.parse_utf8([[url path] UTF8String]);
+ ret.push_back(url_string);
+ }
+ }
+ }
+
+ return ret;
+}
+
+void OS_MacOS::revoke_granted_permissions() {
+ if (is_sandboxed()) {
+ [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"sec_bookmarks"];
+ }
+}
+
void OS_MacOS::initialize_core() {
OS_Unix::initialize_core();
@@ -85,6 +115,18 @@ void OS_MacOS::initialize_core() {
}
void OS_MacOS::finalize() {
+ if (is_sandboxed()) {
+ NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+ for (id bookmark in bookmarks) {
+ NSError *error = nil;
+ BOOL isStale = NO;
+ NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+ if (!error && !isStale) {
+ [url stopAccessingSecurityScopedResource];
+ }
+ }
+ }
+
#ifdef COREMIDI_ENABLED
midi_driver.close();
#endif
@@ -189,7 +231,7 @@ Error OS_MacOS::open_dynamic_library(const String p_path, void *&p_library_handl
}
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + ".");
+ ERR_FAIL_COND_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;
@@ -733,6 +775,23 @@ void OS_MacOS::run() {
}
OS_MacOS::OS_MacOS() {
+ if (is_sandboxed()) {
+ // Load security-scoped bookmarks, request access, remove stale or invalid bookmarks.
+ NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+ NSMutableArray *new_bookmarks = [[NSMutableArray alloc] init];
+ for (id bookmark in bookmarks) {
+ NSError *error = nil;
+ BOOL isStale = NO;
+ NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+ if (!error && !isStale) {
+ if ([url startAccessingSecurityScopedResource]) {
+ [new_bookmarks addObject:bookmark];
+ }
+ }
+ }
+ [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
+ }
+
main_loop = nullptr;
Vector<Logger *> loggers;
diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp
index 0332fbf718..c92520b755 100644
--- a/platform/uwp/export/export_plugin.cpp
+++ b/platform/uwp/export/export_plugin.cpp
@@ -34,6 +34,7 @@
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
+#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For svg and regex.
#ifdef MODULE_SVG_ENABLED
@@ -131,11 +132,11 @@ void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options)
bool EditorExportPlatformUWP::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
#ifndef DEV_ENABLED
// We don't provide export templates for the UWP platform currently as it
- // has not been ported for Godot 4.0. This is skipped in DEV_ENABLED so that
+ // has not been ported for Godot 4. This is skipped in DEV_ENABLED so that
// contributors can still test the pipeline if/when we can build it again.
- r_error = "The UWP platform is currently not supported in Godot 4.0.\n";
+ r_error = "The UWP platform is currently not supported in Godot 4.\n";
return false;
-#endif
+#else
String err;
bool valid = false;
@@ -175,16 +176,17 @@ bool EditorExportPlatformUWP::has_valid_export_configuration(const Ref<EditorExp
}
return valid;
+#endif // DEV_ENABLED
}
bool EditorExportPlatformUWP::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
#ifndef DEV_ENABLED
// We don't provide export templates for the UWP platform currently as it
- // has not been ported for Godot 4.0. This is skipped in DEV_ENABLED so that
+ // has not been ported for Godot 4. This is skipped in DEV_ENABLED so that
// contributors can still test the pipeline if/when we can build it again.
- r_error = "The UWP platform is currently not supported in Godot 4.0.\n";
+ r_error = "The UWP platform is currently not supported in Godot 4.\n";
return false;
-#endif
+#else
String err;
bool valid = true;
@@ -258,6 +260,7 @@ bool EditorExportPlatformUWP::has_valid_project_configuration(const Ref<EditorEx
r_error = err;
return valid;
+#endif // DEV_ENABLED
}
Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
@@ -265,7 +268,7 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p
String src_appx;
- EditorProgress ep("export", "Exporting for UWP", 7, true);
+ EditorProgress ep("export", TTR("Exporting for UWP"), 7, true);
if (p_debug) {
src_appx = p_preset->get("custom_template/debug");
@@ -515,8 +518,7 @@ EditorExportPlatformUWP::EditorExportPlatformUWP() {
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, _uwp_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _uwp_logo_svg, EDSCALE, upsample, false);
logo = ImageTexture::create_from_image(img);
#endif
diff --git a/platform/uwp/export/export_plugin.h b/platform/uwp/export/export_plugin.h
index cc86bdb280..147279e5c5 100644
--- a/platform/uwp/export/export_plugin.h
+++ b/platform/uwp/export/export_plugin.h
@@ -44,6 +44,7 @@
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/export/editor_export_platform.h"
+#include "scene/resources/compressed_texture.h"
#include "thirdparty/minizip/unzip.h"
#include "thirdparty/minizip/zip.h"
diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp
index df7923660c..b9cd9d0baa 100644
--- a/platform/uwp/os_uwp.cpp
+++ b/platform/uwp/os_uwp.cpp
@@ -745,7 +745,7 @@ static String format_error_message(DWORD id) {
Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
String full_path = "game/" + p_path;
p_library_handle = (void *)LoadPackagedLibrary((LPCWSTR)(full_path.utf16().get_data()), 0);
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + full_path + ", error: " + format_error_message(GetLastError()) + ".");
+ ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", full_path, format_error_message(GetLastError())));
if (r_resolved_path != nullptr) {
*r_resolved_path = full_path;
diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp
index 951ce110e0..93b0496d74 100644
--- a/platform/web/display_server_web.cpp
+++ b/platform/web/display_server_web.cpp
@@ -35,6 +35,7 @@
#include "os_web.h"
#include "core/config/project_settings.h"
+#include "scene/resources/atlas_texture.h"
#include "servers/rendering/dummy/rasterizer_dummy.h"
#ifdef GLES3_ENABLED
@@ -731,35 +732,40 @@ void DisplayServerWeb::send_window_event_callback(int p_notification) {
}
void DisplayServerWeb::set_icon(const Ref<Image> &p_icon) {
- ERR_FAIL_COND(p_icon.is_null());
- Ref<Image> icon = p_icon;
- if (icon->is_compressed()) {
- icon = icon->duplicate();
- ERR_FAIL_COND(icon->decompress() != OK);
- }
- if (icon->get_format() != Image::FORMAT_RGBA8) {
- if (icon == p_icon) {
+ if (p_icon.is_valid()) {
+ ERR_FAIL_COND(p_icon->get_width() <= 0 || p_icon->get_height() <= 0);
+
+ Ref<Image> icon = p_icon;
+ if (icon->is_compressed()) {
icon = icon->duplicate();
+ ERR_FAIL_COND(icon->decompress() != OK);
+ }
+ if (icon->get_format() != Image::FORMAT_RGBA8) {
+ if (icon == p_icon) {
+ icon = icon->duplicate();
+ }
+ icon->convert(Image::FORMAT_RGBA8);
}
- icon->convert(Image::FORMAT_RGBA8);
- }
- png_image png_meta;
- memset(&png_meta, 0, sizeof png_meta);
- png_meta.version = PNG_IMAGE_VERSION;
- png_meta.width = icon->get_width();
- png_meta.height = icon->get_height();
- png_meta.format = PNG_FORMAT_RGBA;
+ png_image png_meta;
+ memset(&png_meta, 0, sizeof png_meta);
+ png_meta.version = PNG_IMAGE_VERSION;
+ png_meta.width = icon->get_width();
+ png_meta.height = icon->get_height();
+ png_meta.format = PNG_FORMAT_RGBA;
- PackedByteArray png;
- size_t len;
- PackedByteArray data = icon->get_data();
- ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr));
+ PackedByteArray png;
+ size_t len;
+ PackedByteArray data = icon->get_data();
+ ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr));
- png.resize(len);
- ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr));
+ png.resize(len);
+ ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr));
- godot_js_display_window_icon_set(png.ptr(), len);
+ godot_js_display_window_icon_set(png.ptr(), len);
+ } else {
+ godot_js_display_window_icon_set(nullptr, 0);
+ }
}
void DisplayServerWeb::_dispatch_input_event(const Ref<InputEvent> &p_event) {
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index 38d7ed7fb6..38e2714d9f 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -37,6 +37,7 @@
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "editor/export/editor_export.h"
+#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For mono and svg.
#ifdef MODULE_SVG_ENABLED
@@ -359,17 +360,16 @@ Ref<Texture2D> EditorExportPlatformWeb::get_logo() const {
}
bool EditorExportPlatformWeb::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;
- bool extensions = (bool)p_preset->get("variant/extensions_support");
-
#ifdef MODULE_MONO_ENABLED
- err += TTR("Exporting to Web is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Web with C#/Mono instead.") + "\n";
- err += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
// Don't check for additional errors, as this particular error cannot be resolved.
- r_error = err;
+ r_error += TTR("Exporting to Web is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Web with C#/Mono instead.") + "\n";
+ r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
return false;
-#endif
+#else
+
+ String err;
+ bool valid = false;
+ bool extensions = (bool)p_preset->get("variant/extensions_support");
// Look for export templates (first official, and if defined custom templates).
bool dvalid = exists_export_template(_get_template_name(extensions, true), &err);
@@ -396,6 +396,7 @@ bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExp
}
return valid;
+#endif // !MODULE_MONO_ENABLED
}
bool EditorExportPlatformWeb::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
@@ -674,11 +675,10 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
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, _web_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false);
logo = ImageTexture::create_from_image(img);
- img_loader.create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);
run_icon = ImageTexture::create_from_image(img);
#endif
diff --git a/platform/web/export/run_icon.svg b/platform/web/export/run_icon.svg
index 494f53cb90..fa95e64e79 100644
--- a/platform/web/export/run_icon.svg
+++ b/platform/web/export/run_icon.svg
@@ -1 +1 @@
-<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M3.143 1 5.48 27.504 15.967 31l10.553-3.496L28.857 1ZM23.78 9.565H11.473l.275 3.308h11.759l-.911 9.937-6.556 1.808v.02h-.073l-6.61-1.828-.402-5.076h3.195l.234 2.552 3.583.97 3.595-.97.402-4.165H8.788L7.93 6.37h16.145Z" fill="#eb6428" style="fill:#e0e0e0;fill-opacity:1" transform="translate(.586 .586) scale(.46337)"/></svg>
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m2 1 1.09 12.357 4.9 1.63 4.9-1.63L13.98 1zm9.622 3.994h-5.74l.129 1.541h5.482l-.424 4.634-3.057.843v.01h-.033l-3.082-.853-.187-2.367h1.489l.11 1.19 1.67.452 1.676-.453.187-1.942h-5.21l-.4-4.546h7.527z" fill="#eb6428" style="fill:#e0e0e0;fill-opacity:1"/></svg>
diff --git a/platform/web/http_client_web.cpp b/platform/web/http_client_web.cpp
index 3e4ba5a2ae..ea9226a5a4 100644
--- a/platform/web/http_client_web.cpp
+++ b/platform/web/http_client_web.cpp
@@ -149,7 +149,15 @@ Error HTTPClientWeb::get_response_headers(List<String> *r_response) {
}
int64_t HTTPClientWeb::get_response_body_length() const {
- return godot_js_fetch_body_length_get(js_id);
+ // Body length cannot be consistently retrieved from the web.
+ // Reading the "content-length" value will return a meaningless value when the response is compressed,
+ // as reading will return uncompressed chunks in any case, resulting in a mismatch between the detected
+ // body size and the actual size returned by repeatedly calling read_response_body_chunk.
+ // Additionally, while "content-length" is considered a safe CORS header, "content-encoding" is not,
+ // so using the "content-encoding" to decide if "content-length" is meaningful is not an option either.
+ // We simply must accept the fact that browsers are awful when it comes to networking APIs.
+ // See GH-47597, and GH-79327.
+ return -1;
}
PackedByteArray HTTPClientWeb::read_response_body_chunk() {
diff --git a/platform/web/http_client_web.h b/platform/web/http_client_web.h
index bb9672ab82..4d3c457a7d 100644
--- a/platform/web/http_client_web.h
+++ b/platform/web/http_client_web.h
@@ -51,7 +51,6 @@ extern int godot_js_fetch_read_headers(int p_id, void (*parse_callback)(int p_si
extern int godot_js_fetch_read_chunk(int p_id, uint8_t *p_buf, int p_buf_size);
extern void godot_js_fetch_free(int p_id);
extern godot_js_fetch_state_t godot_js_fetch_state_get(int p_id);
-extern int godot_js_fetch_body_length_get(int p_id);
extern int godot_js_fetch_http_status_get(int p_id);
extern int godot_js_fetch_is_chunked(int p_id);
diff --git a/platform/web/js/libs/library_godot_display.js b/platform/web/js/libs/library_godot_display.js
index ea2a846f90..746f858923 100644
--- a/platform/web/js/libs/library_godot_display.js
+++ b/platform/web/js/libs/library_godot_display.js
@@ -568,16 +568,23 @@ const GodotDisplay = {
godot_js_display_window_icon_set__sig: 'vii',
godot_js_display_window_icon_set: function (p_ptr, p_len) {
let link = document.getElementById('-gd-engine-icon');
- if (link === null) {
- link = document.createElement('link');
- link.rel = 'icon';
- link.id = '-gd-engine-icon';
- document.head.appendChild(link);
- }
const old_icon = GodotDisplay.window_icon;
- const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
- GodotDisplay.window_icon = URL.createObjectURL(png);
- link.href = GodotDisplay.window_icon;
+ if (p_ptr) {
+ if (link === null) {
+ link = document.createElement('link');
+ link.rel = 'icon';
+ link.id = '-gd-engine-icon';
+ document.head.appendChild(link);
+ }
+ const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' });
+ GodotDisplay.window_icon = URL.createObjectURL(png);
+ link.href = GodotDisplay.window_icon;
+ } else {
+ if (link) {
+ link.remove();
+ }
+ GodotDisplay.window_icon = null;
+ }
if (old_icon) {
URL.revokeObjectURL(old_icon);
}
diff --git a/platform/web/js/libs/library_godot_fetch.js b/platform/web/js/libs/library_godot_fetch.js
index 1bb48bfd6a..4ef24903e3 100644
--- a/platform/web/js/libs/library_godot_fetch.js
+++ b/platform/web/js/libs/library_godot_fetch.js
@@ -50,22 +50,17 @@ const GodotFetch = {
return;
}
let chunked = false;
- let bodySize = -1;
response.headers.forEach(function (value, header) {
const v = value.toLowerCase().trim();
const h = header.toLowerCase().trim();
if (h === 'transfer-encoding' && v === 'chunked') {
chunked = true;
}
- if (h === 'content-length') {
- bodySize = parseInt(v, 10);
- }
});
obj.status = response.status;
obj.response = response;
obj.reader = response.body.getReader();
obj.chunked = chunked;
- obj.bodySize = bodySize;
},
onerror: function (id, err) {
@@ -87,7 +82,6 @@ const GodotFetch = {
reading: false,
status: 0,
chunks: [],
- bodySize: -1,
};
const id = IDHandler.add(obj);
const init = {
@@ -224,15 +218,6 @@ const GodotFetch = {
return p_buf_size - to_read;
},
- godot_js_fetch_body_length_get__sig: 'ii',
- godot_js_fetch_body_length_get: function (p_id) {
- const obj = IDHandler.get(p_id);
- if (!obj || !obj.response) {
- return -1;
- }
- return obj.bodySize;
- },
-
godot_js_fetch_is_chunked__sig: 'ii',
godot_js_fetch_is_chunked: function (p_id) {
const obj = IDHandler.get(p_id);
diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp
index 5115ff50da..9ee8f90e89 100644
--- a/platform/web/os_web.cpp
+++ b/platform/web/os_web.cpp
@@ -232,7 +232,7 @@ bool OS_Web::is_userfs_persistent() const {
Error OS_Web::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
String path = p_path.get_file();
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror());
+ ERR_FAIL_COND_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;
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 4138f53a9d..b1dccdcefe 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -32,9 +32,11 @@
#include "os_windows.h"
+#include "core/config/project_settings.h"
#include "core/io/marshalls.h"
+#include "drivers/png/png_driver_common.h"
#include "main/main.h"
-#include "scene/resources/texture.h"
+#include "scene/resources/atlas_texture.h"
#if defined(GLES3_ENABLED)
#include "drivers/gles3/rasterizer_gles3.h"
@@ -42,6 +44,8 @@
#include <avrt.h>
#include <dwmapi.h>
+#include <shlwapi.h>
+#include <shobjidl.h>
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
@@ -87,6 +91,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
case FEATURE_HIDPI:
case FEATURE_ICON:
case FEATURE_NATIVE_ICON:
+ case FEATURE_NATIVE_DIALOG:
case FEATURE_SWAP_BUFFERS:
case FEATURE_KEEP_SCREEN_ON:
case FEATURE_TEXT_TO_SPEECH:
@@ -213,6 +218,129 @@ void DisplayServerWindows::tts_stop() {
tts->stop();
}
+Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+ _THREAD_SAFE_METHOD_
+
+ Vector<Char16String> filter_names;
+ Vector<Char16String> filter_exts;
+ for (const String &E : p_filters) {
+ Vector<String> tokens = E.split(";");
+ if (tokens.size() == 2) {
+ filter_exts.push_back(tokens[0].strip_edges().utf16());
+ filter_names.push_back(tokens[1].strip_edges().utf16());
+ } else if (tokens.size() == 1) {
+ filter_exts.push_back(tokens[0].strip_edges().utf16());
+ filter_names.push_back(tokens[0].strip_edges().utf16());
+ }
+ }
+
+ Vector<COMDLG_FILTERSPEC> filters;
+ for (int i = 0; i < filter_names.size(); i++) {
+ filters.push_back({ (LPCWSTR)filter_names[i].ptr(), (LPCWSTR)filter_exts[i].ptr() });
+ }
+
+ HRESULT hr = S_OK;
+ IFileDialog *pfd = nullptr;
+ if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) {
+ hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileSaveDialog, (void **)&pfd);
+ } else {
+ hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd);
+ }
+ if (SUCCEEDED(hr)) {
+ DWORD flags;
+ pfd->GetOptions(&flags);
+ if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
+ flags |= FOS_ALLOWMULTISELECT;
+ }
+ if (p_mode == FILE_DIALOG_MODE_OPEN_DIR) {
+ flags |= FOS_PICKFOLDERS;
+ }
+ if (p_show_hidden) {
+ flags |= FOS_FORCESHOWHIDDEN;
+ }
+ pfd->SetOptions(flags | FOS_FORCEFILESYSTEM);
+ pfd->SetTitle((LPCWSTR)p_title.utf16().ptr());
+
+ String dir = ProjectSettings::get_singleton()->globalize_path(p_current_directory);
+ if (dir == ".") {
+ dir = OS::get_singleton()->get_executable_path().get_base_dir();
+ }
+ dir = dir.replace("/", "\\");
+
+ IShellItem *shellitem = nullptr;
+ hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem);
+ if (SUCCEEDED(hr)) {
+ pfd->SetDefaultFolder(shellitem);
+ pfd->SetFolder(shellitem);
+ }
+
+ pfd->SetFileName((LPCWSTR)p_filename.utf16().ptr());
+ pfd->SetFileTypes(filters.size(), filters.ptr());
+ pfd->SetFileTypeIndex(0);
+
+ hr = pfd->Show(nullptr);
+ if (SUCCEEDED(hr)) {
+ Vector<String> file_names;
+
+ if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
+ IShellItemArray *results;
+ hr = static_cast<IFileOpenDialog *>(pfd)->GetResults(&results);
+ if (SUCCEEDED(hr)) {
+ DWORD count = 0;
+ results->GetCount(&count);
+ for (DWORD i = 0; i < count; i++) {
+ IShellItem *result;
+ results->GetItemAt(i, &result);
+
+ PWSTR file_path = nullptr;
+ hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path);
+ if (SUCCEEDED(hr)) {
+ file_names.push_back(String::utf16((const char16_t *)file_path));
+ CoTaskMemFree(file_path);
+ }
+ result->Release();
+ }
+ results->Release();
+ }
+ } else {
+ IShellItem *result;
+ hr = pfd->GetResult(&result);
+ if (SUCCEEDED(hr)) {
+ PWSTR file_path = nullptr;
+ hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path);
+ if (SUCCEEDED(hr)) {
+ file_names.push_back(String::utf16((const char16_t *)file_path));
+ CoTaskMemFree(file_path);
+ }
+ result->Release();
+ }
+ }
+ if (!p_callback.is_null()) {
+ Variant v_status = true;
+ Variant v_files = file_names;
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ p_callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ } else {
+ if (!p_callback.is_null()) {
+ Variant v_status = false;
+ Variant v_files = Vector<String>();
+ Variant *v_args[2] = { &v_status, &v_files };
+ Variant ret;
+ Callable::CallError ce;
+ p_callback.callp((const Variant **)&v_args, 2, ret, ce);
+ }
+ }
+ pfd->Release();
+
+ return OK;
+ } else {
+ return ERR_CANT_OPEN;
+ }
+}
+
void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) {
_THREAD_SAFE_METHOD_
@@ -341,6 +469,68 @@ String DisplayServerWindows::clipboard_get() const {
return ret;
}
+Ref<Image> DisplayServerWindows::clipboard_get_image() const {
+ Ref<Image> image;
+ if (!windows.has(last_focused_window)) {
+ return image; // No focused window?
+ }
+ if (!OpenClipboard(windows[last_focused_window].hWnd)) {
+ ERR_FAIL_V_MSG(image, "Unable to open clipboard.");
+ }
+ UINT png_format = RegisterClipboardFormatA("PNG");
+ if (png_format && IsClipboardFormatAvailable(png_format)) {
+ HANDLE png_handle = GetClipboardData(png_format);
+ if (png_handle) {
+ size_t png_size = GlobalSize(png_handle);
+ uint8_t *png_data = (uint8_t *)GlobalLock(png_handle);
+ image.instantiate();
+
+ PNGDriverCommon::png_to_image(png_data, png_size, false, image);
+
+ GlobalUnlock(png_handle);
+ }
+ } else if (IsClipboardFormatAvailable(CF_DIB)) {
+ HGLOBAL mem = GetClipboardData(CF_DIB);
+ if (mem != NULL) {
+ BITMAPINFO *ptr = static_cast<BITMAPINFO *>(GlobalLock(mem));
+
+ if (ptr != NULL) {
+ BITMAPINFOHEADER *info = &ptr->bmiHeader;
+ PackedByteArray pba;
+
+ for (LONG y = info->biHeight - 1; y > -1; y--) {
+ for (LONG x = 0; x < info->biWidth; x++) {
+ tagRGBQUAD *rgbquad = ptr->bmiColors + (info->biWidth * y) + x;
+ pba.append(rgbquad->rgbRed);
+ pba.append(rgbquad->rgbGreen);
+ pba.append(rgbquad->rgbBlue);
+ pba.append(rgbquad->rgbReserved);
+ }
+ }
+ image.instantiate();
+ image->create_from_data(info->biWidth, info->biHeight, false, Image::Format::FORMAT_RGBA8, pba);
+
+ GlobalUnlock(mem);
+ }
+ }
+ }
+
+ CloseClipboard();
+
+ return image;
+}
+
+bool DisplayServerWindows::clipboard_has() const {
+ return (IsClipboardFormatAvailable(CF_TEXT) ||
+ IsClipboardFormatAvailable(CF_UNICODETEXT) ||
+ IsClipboardFormatAvailable(CF_OEMTEXT));
+}
+
+bool DisplayServerWindows::clipboard_has_image() const {
+ UINT png_format = RegisterClipboardFormatA("PNG");
+ return ((png_format && IsClipboardFormatAvailable(png_format)) || IsClipboardFormatAvailable(CF_DIB));
+}
+
typedef struct {
int count;
int screen;
@@ -618,7 +808,7 @@ Color DisplayServerWindows::screen_get_pixel(const Point2i &p_position) const {
COLORREF col = GetPixel(dc, p.x, p.y);
if (col != CLR_INVALID) {
ReleaseDC(NULL, dc);
- return Color(float(col & 0x000000FF) / 256.0, float((col & 0x0000FF00) >> 8) / 256.0, float((col & 0x00FF0000) >> 16) / 256.0, 1.0);
+ return Color(float(col & 0x000000FF) / 255.0f, float((col & 0x0000FF00) >> 8) / 255.0f, float((col & 0x00FF0000) >> 16) / 255.0f, 1.0f);
}
ReleaseDC(NULL, dc);
}
@@ -997,7 +1187,7 @@ void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) {
ERR_FAIL_COND(!windows.has(p_window));
if (windows[p_window].mpass || windows[p_window].mpath.size() == 0) {
- SetWindowRgn(windows[p_window].hWnd, nullptr, TRUE);
+ SetWindowRgn(windows[p_window].hWnd, nullptr, FALSE);
} else {
POINT *points = (POINT *)memalloc(sizeof(POINT) * windows[p_window].mpath.size());
for (int i = 0; i < windows[p_window].mpath.size(); i++) {
@@ -1011,8 +1201,7 @@ void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) {
}
HRGN region = CreatePolygonRgn(points, windows[p_window].mpath.size(), ALTERNATE);
- SetWindowRgn(windows[p_window].hWnd, region, TRUE);
- DeleteObject(region);
+ SetWindowRgn(windows[p_window].hWnd, region, FALSE);
memfree(points);
}
}
@@ -2010,6 +2199,38 @@ Key DisplayServerWindows::keyboard_get_keycode_from_physical(Key p_keycode) cons
return (Key)(KeyMappingWindows::get_keysym(vk) | modifiers);
}
+Key DisplayServerWindows::keyboard_get_label_from_physical(Key p_keycode) const {
+ Key modifiers = p_keycode & KeyModifierMask::MODIFIER_MASK;
+ Key keycode_no_mod = (Key)(p_keycode & KeyModifierMask::CODE_MASK);
+
+ if (keycode_no_mod == Key::PRINT ||
+ keycode_no_mod == Key::KP_ADD ||
+ keycode_no_mod == Key::KP_5 ||
+ (keycode_no_mod >= Key::KEY_0 && keycode_no_mod <= Key::KEY_9)) {
+ return p_keycode;
+ }
+
+ unsigned int scancode = KeyMappingWindows::get_scancode(keycode_no_mod);
+ if (scancode == 0) {
+ return p_keycode;
+ }
+
+ Key keycode = KeyMappingWindows::get_keysym(MapVirtualKey(scancode, MAPVK_VSC_TO_VK));
+
+ HKL current_layout = GetKeyboardLayout(0);
+ static BYTE keyboard_state[256];
+ memset(keyboard_state, 0, 256);
+ wchar_t chars[256] = {};
+ UINT extended_code = MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX);
+ if (ToUnicodeEx(extended_code, scancode, keyboard_state, chars, 255, 4, current_layout) > 0) {
+ String keysym = String::utf16((char16_t *)chars, 255);
+ if (!keysym.is_empty()) {
+ return fix_key_label(keysym[0], keycode) | modifiers;
+ }
+ }
+ return p_keycode;
+}
+
String _get_full_layout_name_from_registry(HKL p_layout) {
String id = "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\" + String::num_int64((int64_t)p_layout, 16, false).lpad(8, "0");
String ret;
@@ -2194,55 +2415,65 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) {
void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) {
_THREAD_SAFE_METHOD_
- ERR_FAIL_COND(!p_icon.is_valid());
- if (icon != p_icon) {
- icon = p_icon->duplicate();
- if (icon->get_format() != Image::FORMAT_RGBA8) {
- icon->convert(Image::FORMAT_RGBA8);
+ if (p_icon.is_valid()) {
+ ERR_FAIL_COND(p_icon->get_width() <= 0 || p_icon->get_height() <= 0);
+
+ Ref<Image> img = p_icon;
+ if (img != icon) {
+ img = img->duplicate();
+ img->convert(Image::FORMAT_RGBA8);
}
- }
- int w = icon->get_width();
- int h = icon->get_height();
-
- // Create temporary bitmap buffer.
- int icon_len = 40 + h * w * 4;
- Vector<BYTE> v;
- v.resize(icon_len);
- BYTE *icon_bmp = v.ptrw();
-
- encode_uint32(40, &icon_bmp[0]);
- encode_uint32(w, &icon_bmp[4]);
- encode_uint32(h * 2, &icon_bmp[8]);
- encode_uint16(1, &icon_bmp[12]);
- encode_uint16(32, &icon_bmp[14]);
- encode_uint32(BI_RGB, &icon_bmp[16]);
- encode_uint32(w * h * 4, &icon_bmp[20]);
- encode_uint32(0, &icon_bmp[24]);
- encode_uint32(0, &icon_bmp[28]);
- encode_uint32(0, &icon_bmp[32]);
- encode_uint32(0, &icon_bmp[36]);
-
- uint8_t *wr = &icon_bmp[40];
- const uint8_t *r = icon->get_data().ptr();
-
- for (int i = 0; i < h; i++) {
- for (int j = 0; j < w; j++) {
- const uint8_t *rpx = &r[((h - i - 1) * w + j) * 4];
- uint8_t *wpx = &wr[(i * w + j) * 4];
- wpx[0] = rpx[2];
- wpx[1] = rpx[1];
- wpx[2] = rpx[0];
- wpx[3] = rpx[3];
+
+ int w = img->get_width();
+ int h = img->get_height();
+
+ // Create temporary bitmap buffer.
+ int icon_len = 40 + h * w * 4;
+ Vector<BYTE> v;
+ v.resize(icon_len);
+ BYTE *icon_bmp = v.ptrw();
+
+ encode_uint32(40, &icon_bmp[0]);
+ encode_uint32(w, &icon_bmp[4]);
+ encode_uint32(h * 2, &icon_bmp[8]);
+ encode_uint16(1, &icon_bmp[12]);
+ encode_uint16(32, &icon_bmp[14]);
+ encode_uint32(BI_RGB, &icon_bmp[16]);
+ encode_uint32(w * h * 4, &icon_bmp[20]);
+ encode_uint32(0, &icon_bmp[24]);
+ encode_uint32(0, &icon_bmp[28]);
+ encode_uint32(0, &icon_bmp[32]);
+ encode_uint32(0, &icon_bmp[36]);
+
+ uint8_t *wr = &icon_bmp[40];
+ const uint8_t *r = img->get_data().ptr();
+
+ for (int i = 0; i < h; i++) {
+ for (int j = 0; j < w; j++) {
+ const uint8_t *rpx = &r[((h - i - 1) * w + j) * 4];
+ uint8_t *wpx = &wr[(i * w + j) * 4];
+ wpx[0] = rpx[2];
+ wpx[1] = rpx[1];
+ wpx[2] = rpx[0];
+ wpx[3] = rpx[3];
+ }
}
- }
- HICON hicon = CreateIconFromResource(icon_bmp, icon_len, TRUE, 0x00030000);
+ HICON hicon = CreateIconFromResource(icon_bmp, icon_len, TRUE, 0x00030000);
+ ERR_FAIL_COND(!hicon);
+
+ icon = img;
- // Set the icon for the window.
- SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon);
+ // Set the icon for the window.
+ SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hicon);
- // Set the icon in the task manager (should we do this?).
- SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_BIG, (LPARAM)hicon);
+ // Set the icon in the task manager (should we do this?).
+ SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_BIG, (LPARAM)hicon);
+ } else {
+ icon = Ref<Image>();
+ SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_SMALL, 0);
+ SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_SETICON, ICON_BIG, 0);
+ }
}
void DisplayServerWindows::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) {
@@ -3606,6 +3837,10 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
} break;
case WM_DESTROY: {
Input::get_singleton()->flush_buffered_events();
+ if (window_mouseover_id == window_id) {
+ window_mouseover_id = INVALID_WINDOW_ID;
+ _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_EXIT);
+ }
} break;
case WM_SETCURSOR: {
if (LOWORD(lParam) == HTCLIENT) {
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index 7228de7d31..59c4442604 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -511,6 +511,8 @@ public:
virtual bool is_dark_mode() const override;
virtual Color get_accent_color() const override;
+ virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+
virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;
@@ -520,6 +522,9 @@ public:
virtual void clipboard_set(const String &p_text) override;
virtual String clipboard_get() const override;
+ virtual Ref<Image> clipboard_get_image() const override;
+ virtual bool clipboard_has() const override;
+ virtual bool clipboard_has_image() const override;
virtual int get_screen_count() const override;
virtual int get_primary_screen() const override;
@@ -623,6 +628,7 @@ public:
virtual String keyboard_get_layout_language(int p_index) const override;
virtual String keyboard_get_layout_name(int p_index) const override;
virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override;
+ virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;
virtual int tablet_get_driver_count() const override;
virtual String tablet_get_driver_name(int p_driver) const override;
diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp
index b521a649be..0ef07c3275 100644
--- a/platform/windows/export/export_plugin.cpp
+++ b/platform/windows/export/export_plugin.cpp
@@ -1011,11 +1011,10 @@ EditorExportPlatformWindows::EditorExportPlatformWindows() {
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, _windows_logo_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _windows_logo_svg, EDSCALE, upsample, false);
set_logo(ImageTexture::create_from_image(img));
- img_loader.create_image_from_string(img, _windows_run_icon_svg, EDSCALE, upsample, false);
+ ImageLoaderSVG::create_image_from_string(img, _windows_run_icon_svg, EDSCALE, upsample, false);
run_icon = ImageTexture::create_from_image(img);
#endif
diff --git a/platform/windows/export/run_icon.svg b/platform/windows/export/run_icon.svg
index 0897276ef7..6a18433ed2 100644
--- a/platform/windows/export/run_icon.svg
+++ b/platform/windows/export/run_icon.svg
@@ -1 +1 @@
-<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m1.095 2.997 5.66-.78v5.469h-5.66zm0 10.006 5.66.78v-5.4h-5.66zm6.282.863 7.528 1.04V8.381H7.377Zm0-11.732v5.552h7.528V1.095Z" fill="#00abed" style="stroke-width:.460341;fill:#e0e0e0;fill-opacity:1"/></svg>
+<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m1.095 2.997 5.66-.78v5.469h-5.66zm0 10.006 5.66.78v-5.4h-5.66zm6.282.863 7.528 1.04V8.381H7.377Zm0-11.732v5.552h7.528V1.095Z" fill="#00abed" style="fill:#e0e0e0;fill-opacity:1"/></svg>
diff --git a/platform/windows/gl_manager_windows.cpp b/platform/windows/gl_manager_windows.cpp
index 0334bdd973..d3972c7bbc 100644
--- a/platform/windows/gl_manager_windows.cpp
+++ b/platform/windows/gl_manager_windows.cpp
@@ -53,6 +53,7 @@
#if defined(__GNUC__)
// Workaround GCC warning from -Wcast-function-type.
#define wglGetProcAddress (void *)wglGetProcAddress
+#define GetProcAddress (void *)GetProcAddress
#endif
typedef HGLRC(APIENTRY *PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC, HGLRC, const int *);
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index cb70f93a62..df93631ef0 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -389,13 +389,13 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han
}
missing += E;
}
- ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, missing dependencies: (%s), error: \"%s\".", p_path, missing, format_error_message(err_code)));
+ ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Missing dependencies: %s. Error: %s.", p_path, missing, format_error_message(err_code)));
} else {
- ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, error: \"%s\"." + p_path, format_error_message(err_code)));
+ ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, format_error_message(err_code)));
}
}
#else
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, error: \"%s\"." + p_path, format_error_message(GetLastError())));
+ ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, format_error_message(GetLastError())));
#endif
if (cookie) {