summaryrefslogtreecommitdiffstats
path: root/platform/android
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android')
-rw-r--r--platform/android/detect.py54
-rw-r--r--platform/android/display_server_android.cpp64
-rw-r--r--platform/android/display_server_android.h11
-rw-r--r--platform/android/doc_classes/EditorExportPlatformAndroid.xml17
-rw-r--r--platform/android/export/export.cpp22
-rw-r--r--platform/android/export/export_plugin.cpp54
-rw-r--r--platform/android/export/export_plugin.h7
-rw-r--r--platform/android/export/gradle_export_util.cpp23
-rw-r--r--platform/android/export/gradle_export_util.h3
-rw-r--r--platform/android/java/app/res/values/themes.xml4
-rw-r--r--platform/android/java/editor/build.gradle2
-rw-r--r--platform/android/java/editor/src/main/AndroidManifest.xml1
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt20
-rw-r--r--platform/android/java/editor/src/main/res/values/strings.xml1
-rw-r--r--platform/android/java/editor/src/main/res/values/themes.xml3
-rw-r--r--platform/android/java/lib/build.gradle2
-rw-r--r--platform/android/java/lib/res/values/dimens.xml2
-rw-r--r--platform/android/java/lib/res/values/strings.xml3
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt64
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java1
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java10
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt200
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt72
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java22
-rw-r--r--platform/android/java/nativeSrcsConfigs/CMakeLists.txt2
-rw-r--r--platform/android/java_godot_lib_jni.cpp24
-rw-r--r--platform/android/java_godot_lib_jni.h2
-rw-r--r--platform/android/java_godot_wrapper.cpp60
-rw-r--r--platform/android/java_godot_wrapper.h7
-rw-r--r--platform/android/net_socket_android.cpp8
-rw-r--r--platform/android/net_socket_android.h4
31 files changed, 690 insertions, 79 deletions
diff --git a/platform/android/detect.py b/platform/android/detect.py
index 0a10754e24..03a208391c 100644
--- a/platform/android/detect.py
+++ b/platform/android/detect.py
@@ -5,6 +5,7 @@ import sys
from typing import TYPE_CHECKING
from methods import print_error, print_warning
+from platform_methods import validate_arch
if TYPE_CHECKING:
from SCons.Script.SConscript import SConsEnvironment
@@ -30,6 +31,7 @@ def get_opts():
),
BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False),
+ BoolVariable("swappy", "Use Swappy Frame Pacing library", False),
]
@@ -95,15 +97,19 @@ def install_ndk_if_needed(env: "SConsEnvironment"):
env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env)
+def detect_swappy():
+ archs = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"]
+ has_swappy = True
+ for arch in archs:
+ if not os.path.isfile(f"thirdparty/swappy-frame-pacing/{arch}/libswappy_static.a"):
+ has_swappy = False
+ return has_swappy
+
+
def configure(env: "SConsEnvironment"):
# Validate arch.
supported_arches = ["x86_32", "x86_64", "arm32", "arm64"]
- if env["arch"] not in supported_arches:
- print_error(
- 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.'
- % (env["arch"], ", ".join(supported_arches))
- )
- sys.exit(255)
+ validate_arch(env["arch"], get_name(), supported_arches)
if get_min_sdk_version(env["ndk_platform"]) < get_min_target_api():
print_warning(
@@ -171,31 +177,52 @@ def configure(env: "SConsEnvironment"):
env["AS"] = compiler_path + "/clang"
env.Append(
- CCFLAGS=(
- "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split()
- )
+ CCFLAGS=(["-fpic", "-ffunction-sections", "-funwind-tables", "-fstack-protector-strong", "-fvisibility=hidden"])
)
+ has_swappy = detect_swappy()
+ if not has_swappy:
+ print_warning(
+ "Swappy Frame Pacing not detected! It is strongly recommended you download it from https://github.com/darksylinc/godot-swappy/releases and extract it so that the following files can be found:\n"
+ + " thirdparty/swappy-frame-pacing/arm64-v8a/libswappy_static.a\n"
+ + " thirdparty/swappy-frame-pacing/armeabi-v7a/libswappy_static.a\n"
+ + " thirdparty/swappy-frame-pacing/x86/libswappy_static.a\n"
+ + " thirdparty/swappy-frame-pacing/x86_64/libswappy_static.a\n"
+ + "Without Swappy, Godot apps on Android will inevitable suffer stutter and struggle to keep consistent 30/60/90/120 fps. Though Swappy cannot guarantee your app will be stutter-free, not having Swappy will guarantee there will be stutter even on the best phones and the most simple of scenes."
+ )
+ if env["swappy"]:
+ print_error("Use build option `swappy=no` to ignore missing Swappy dependency and build without it.")
+ sys.exit(255)
+
if get_min_sdk_version(env["ndk_platform"]) >= 24:
env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)])
if env["arch"] == "x86_32":
# The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least
env.Append(CCFLAGS=["-mstackrealign"])
+ if has_swappy:
+ env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/x86"])
+ elif env["arch"] == "x86_64":
+ if has_swappy:
+ env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/x86_64"])
elif env["arch"] == "arm32":
- env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split())
+ env.Append(CCFLAGS=["-march=armv7-a", "-mfloat-abi=softfp"])
env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"])
env.Append(CPPDEFINES=["__ARM_NEON__"])
+ if has_swappy:
+ env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/armeabi-v7a"])
elif env["arch"] == "arm64":
env.Append(CCFLAGS=["-mfix-cortex-a53-835769"])
env.Append(CPPDEFINES=["__ARM_ARCH_8A__"])
+ if has_swappy:
+ env.Append(LIBPATH=["#thirdparty/swappy-frame-pacing/arm64-v8a"])
env.Append(CCFLAGS=["-ffp-contract=off"])
# Link flags
- env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split())
- env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so")
+ env.Append(LINKFLAGS=["-Wl,--gc-sections", "-Wl,--no-undefined", "-Wl,-z,now"])
+ env.Append(LINKFLAGS=["-Wl,-soname,libgodot_android.so"])
env.Prepend(CPPPATH=["#platform/android"])
env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED"])
@@ -203,6 +230,9 @@ def configure(env: "SConsEnvironment"):
if env["vulkan"]:
env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
+ if has_swappy:
+ env.Append(CPPDEFINES=["SWAPPY_FRAME_PACING_ENABLED"])
+ env.Append(LIBS=["swappy_static"])
if not env["use_volk"]:
env.Append(LIBS=["vulkan"])
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index fa5b970a96..38f6931c8a 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -71,8 +71,9 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const {
case FEATURE_MOUSE:
//case FEATURE_MOUSE_WARP:
//case FEATURE_NATIVE_DIALOG:
- //case FEATURE_NATIVE_DIALOG_INPUT:
- //case FEATURE_NATIVE_DIALOG_FILE:
+ case FEATURE_NATIVE_DIALOG_INPUT:
+ case FEATURE_NATIVE_DIALOG_FILE:
+ //case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
//case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD:
@@ -176,6 +177,38 @@ bool DisplayServerAndroid::clipboard_has() const {
}
}
+Error DisplayServerAndroid::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) {
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL_V(godot_java, FAILED);
+ input_dialog_callback = p_callback;
+ return godot_java->show_input_dialog(p_title, p_description, p_partial);
+}
+
+void DisplayServerAndroid::emit_input_dialog_callback(String p_text) {
+ if (input_dialog_callback.is_valid()) {
+ input_dialog_callback.call_deferred(p_text);
+ }
+}
+
+Error DisplayServerAndroid::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) {
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL_V(godot_java, FAILED);
+ file_picker_callback = p_callback;
+ return godot_java->show_file_picker(p_current_directory, p_filename, p_mode, p_filters);
+}
+
+void DisplayServerAndroid::emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths) {
+ if (file_picker_callback.is_valid()) {
+ file_picker_callback.call_deferred(p_ok, p_selected_paths, 0);
+ }
+}
+
+Color DisplayServerAndroid::get_accent_color() const {
+ GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
+ ERR_FAIL_NULL_V(godot_java, Color(0, 0, 0, 0));
+ return godot_java->get_accent_color();
+}
+
TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, Array());
@@ -389,6 +422,14 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type,
}
return 0;
}
+ case EGL_DISPLAY: {
+ // @todo Find a way to get this from the Java side.
+ return 0;
+ }
+ case EGL_CONFIG: {
+ // @todo Find a way to get this from the Java side.
+ return 0;
+ }
#endif
default: {
return 0;
@@ -596,12 +637,6 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
native_menu = memnew(NativeMenu);
-#if defined(GLES3_ENABLED)
- if (rendering_driver == "opengl3") {
- RasterizerGLES3::make_current(false);
- }
-#endif
-
#if defined(RD_ENABLED)
rendering_context = nullptr;
rendering_device = nullptr;
@@ -616,19 +651,24 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
if (rendering_context->initialize() != OK) {
memdelete(rendering_context);
rendering_context = nullptr;
+#if defined(GLES3_ENABLED)
bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
if (fallback_to_opengl3 && rendering_driver != "opengl3") {
WARN_PRINT("Your device seem not to support Vulkan, switching to OpenGL 3.");
rendering_driver = "opengl3";
OS::get_singleton()->set_current_rendering_method("gl_compatibility");
OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
- } else {
+ } else
+#endif
+ {
ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
r_error = ERR_UNAVAILABLE;
return;
}
}
+ }
+ if (rendering_context) {
union {
#ifdef VULKAN_ENABLED
RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan;
@@ -668,6 +708,12 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
}
#endif
+#if defined(GLES3_ENABLED)
+ if (rendering_driver == "opengl3") {
+ RasterizerGLES3::make_current(false);
+ }
+#endif
+
Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events);
r_error = OK;
diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h
index 65c6a53446..1744ad3069 100644
--- a/platform/android/display_server_android.h
+++ b/platform/android/display_server_android.h
@@ -87,6 +87,9 @@ class DisplayServerAndroid : public DisplayServer {
Callable system_theme_changed;
+ Callable input_dialog_callback;
+ Callable file_picker_callback;
+
void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const;
static void _dispatch_input_events(const Ref<InputEvent> &p_event);
@@ -116,6 +119,14 @@ public:
virtual String clipboard_get() const override;
virtual bool clipboard_has() const override;
+ virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
+ void emit_input_dialog_callback(String p_text);
+
+ virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+ void emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths);
+
+ virtual Color get_accent_color() const override;
+
virtual TypedArray<Rect2> get_display_cutouts() const override;
virtual Rect2i get_display_safe_area() const override;
diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
index 2fe5539e56..983683fd78 100644
--- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml
+++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
@@ -151,6 +151,9 @@
<member name="permissions/access_location_extra_commands" type="bool" setter="" getter="">
Allows access to the extra location provider commands. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_LOCATION_EXTRA_COMMANDS]ACCESS_LOCATION_EXTRA_COMMANDS[/url].
</member>
+ <member name="permissions/access_media_location" type="bool" setter="" getter="">
+ Allows an application to access any geographic locations persisted in the user's shared collection. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_MEDIA_LOCATION]ACCESS_MEDIA_LOCATION[/url].
+ </member>
<member name="permissions/access_mock_location" type="bool" setter="" getter="">
Allows an application to create mock location providers for testing.
</member>
@@ -412,6 +415,18 @@
<member name="permissions/read_logs" type="bool" setter="" getter="">
Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url].
</member>
+ <member name="permissions/read_media_audio" type="bool" setter="" getter="">
+ Allows an application to read audio files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_AUDIO]READ_MEDIA_AUDIO[/url].
+ </member>
+ <member name="permissions/read_media_images" type="bool" setter="" getter="">
+ Allows an application to read image files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_IMAGES]READ_MEDIA_IMAGES[/url].
+ </member>
+ <member name="permissions/read_media_video" type="bool" setter="" getter="">
+ Allows an application to read video files from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VIDEO]READ_MEDIA_VIDEO[/url].
+ </member>
+ <member name="permissions/read_media_visual_user_selected" type="bool" setter="" getter="">
+ Allows an application to read image or video files from external storage that a user has selected via the permission prompt photo picker. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_MEDIA_VISUAL_USER_SELECTED]READ_MEDIA_VISUAL_USER_SELECTED[/url].
+ </member>
<member name="permissions/read_phone_state" type="bool" setter="" getter="">
Allows read only access to phone state. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE]READ_PHONE_STATE[/url].
</member>
@@ -577,7 +592,7 @@
Allows an application to write to the user dictionary.
</member>
<member name="screen/immersive_mode" type="bool" setter="" getter="">
- If [code]true[/code], hides navigation and status bar.
+ If [code]true[/code], hides navigation and status bar. See [method DisplayServer.window_set_mode] to toggle it at runtime.
</member>
<member name="screen/support_large" type="bool" setter="" getter="">
Indicates whether the application supports larger screen form-factors.
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index 93ac498ab3..dc7a287a91 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -37,6 +37,8 @@
#include "editor/editor_settings.h"
#include "editor/export/editor_export.h"
+String get_default_android_sdk_path();
+
void register_android_exporter_types() {
GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformAndroid);
}
@@ -49,11 +51,15 @@ void register_android_exporter() {
EDITOR_DEF_BASIC("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));
-#ifndef ANDROID_ENABLED
+#ifdef ANDROID_ENABLED
+ EDITOR_DEF_BASIC("export/android/install_exported_apk", true);
+#else
EDITOR_DEF_BASIC("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
- EDITOR_DEF_BASIC("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
+
+ EDITOR_DEF_BASIC("export/android/android_sdk_path", OS::get_singleton()->has_environment("ANDROID_HOME") ? OS::get_singleton()->get_environment("ANDROID_HOME") : get_default_android_sdk_path());
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
+
EDITOR_DEF("export/android/force_system_user", false);
EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
@@ -67,3 +73,15 @@ void register_android_exporter() {
Ref<EditorExportPlatformAndroid> exporter = Ref<EditorExportPlatformAndroid>(memnew(EditorExportPlatformAndroid));
EditorExport::get_singleton()->add_export_platform(exporter);
}
+
+inline String get_default_android_sdk_path() {
+#ifdef WINDOWS_ENABLED
+ return OS::get_singleton()->get_environment("LOCALAPPDATA").path_join("Android/Sdk");
+#elif LINUXBSD_ENABLED
+ return OS::get_singleton()->get_environment("HOME").path_join("Android/Sdk");
+#elif MACOS_ENABLED
+ return OS::get_singleton()->get_environment("HOME").path_join("Library/Android/sdk");
+#else
+ return String();
+#endif
+}
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index 41f460ca8f..a7b0879056 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -68,6 +68,7 @@ static const char *android_perms[] = {
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"ACCESS_LOCATION_EXTRA_COMMANDS",
+ "ACCESS_MEDIA_LOCATION",
"ACCESS_MOCK_LOCATION",
"ACCESS_NETWORK_STATE",
"ACCESS_SURFACE_FLINGER",
@@ -155,6 +156,10 @@ static const char *android_perms[] = {
"READ_HISTORY_BOOKMARKS",
"READ_INPUT_STATE",
"READ_LOGS",
+ "READ_MEDIA_AUDIO",
+ "READ_MEDIA_IMAGES",
+ "READ_MEDIA_VIDEO",
+ "READ_MEDIA_VISUAL_USER_SELECTED",
"READ_PHONE_STATE",
"READ_PROFILE",
"READ_SMS",
@@ -783,15 +788,16 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj
return OK;
}
-Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
+Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed) {
APKExportData *ed = static_cast<APKExportData *>(p_userdata);
- String dst_path = p_path.replace_first("res://", "assets/");
+ const String path = ResourceUID::ensure_path(p_path);
+ const String dst_path = path.replace_first("res://", "assets/");
- store_in_apk(ed, dst_path, p_data, _should_compress_asset(p_path, p_data) ? Z_DEFLATED : 0);
+ store_in_apk(ed, dst_path, p_data, _should_compress_asset(path, p_data) ? Z_DEFLATED : 0);
return OK;
}
-Error EditorExportPlatformAndroid::ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
+Error EditorExportPlatformAndroid::ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed) {
return OK;
}
@@ -1559,7 +1565,7 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &
str = get_project_name(package_name);
} else {
- String lang = str.substr(str.rfind("-") + 1, str.length()).replace("-", "_");
+ String lang = str.substr(str.rfind_char('-') + 1, str.length()).replace("-", "_");
if (appnames.has(lang)) {
str = appnames[lang];
} else {
@@ -1693,7 +1699,7 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &
path = static_cast<String>(p_preset->get(launcher_adaptive_icon_monochrome_option)).strip_edges();
if (!path.is_empty()) {
print_verbose("Loading adaptive monochrome icon from " + path);
- ImageLoader::load_image(path, background);
+ ImageLoader::load_image(path, monochrome);
}
}
@@ -1778,6 +1784,12 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
if (!is_package_name_valid(pn, &pn_err)) {
return TTR("Invalid package name:") + " " + pn_err;
}
+ } else if (p_name == launcher_adaptive_icon_monochrome_option) {
+ String monochrome_icon_path = p_preset->get(launcher_adaptive_icon_monochrome_option);
+
+ if (monochrome_icon_path.is_empty()) {
+ return TTR("No adaptive monochrome icon specified; default Godot monochrome icon will be used.");
+ }
} 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 = _get_plugins_names(Ref<EditorExportPreset>(p_preset));
@@ -1892,7 +1904,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "com.example.$genname", false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true));
- r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video,Undefined"), 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));
@@ -2517,7 +2529,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Check for the bin directory.
Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
if (errn != OK) {
- err += TTR("Invalid Java SDK path in Editor Settings.");
+ err += TTR("Invalid Java SDK path in Editor Settings.") + " ";
err += TTR("Missing 'bin' directory!");
err += "\n";
valid = false;
@@ -2525,7 +2537,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Check for the `java` command.
String java_path = get_java_path();
if (!FileAccess::exists(java_path)) {
- err += TTR("Unable to find 'java' command using the Java SDK path.");
+ err += TTR("Unable to find 'java' command using the Java SDK path.") + " ";
err += TTR("Please check the Java SDK directory specified in Editor Settings.");
err += "\n";
valid = false;
@@ -2542,7 +2554,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Check for the platform-tools directory.
Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);
if (errn != OK) {
- err += TTR("Invalid Android SDK path in Editor Settings.");
+ err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
err += TTR("Missing 'platform-tools' directory!");
err += "\n";
valid = false;
@@ -2551,7 +2563,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Validate that adb is available.
String adb_path = get_adb_path();
if (!FileAccess::exists(adb_path)) {
- err += TTR("Unable to find Android SDK platform-tools' adb command.");
+ err += TTR("Unable to find Android SDK platform-tools' adb command.") + " ";
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
err += "\n";
valid = false;
@@ -2560,7 +2572,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Check for the build-tools directory.
Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);
if (errn != OK) {
- err += TTR("Invalid Android SDK path in Editor Settings.");
+ err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
err += TTR("Missing 'build-tools' directory!");
err += "\n";
valid = false;
@@ -2573,7 +2585,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Validate that apksigner is available.
String apksigner_path = get_apksigner_path(target_sdk_version.to_int());
if (!FileAccess::exists(apksigner_path)) {
- err += TTR("Unable to find Android SDK build-tools' apksigner command.");
+ err += TTR("Unable to find Android SDK build-tools' apksigner command.") + " ";
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
err += "\n";
valid = false;
@@ -2887,6 +2899,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
#endif
print_verbose("Successfully completed signing build.");
+
+#ifdef ANDROID_ENABLED
+ bool prompt_apk_install = EDITOR_GET("export/android/install_exported_apk");
+ if (prompt_apk_install) {
+ OS_Android::get_singleton()->shell_open(apk_path);
+ }
+#endif
+
return OK;
}
@@ -3151,9 +3171,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
user_data.libs_directory = gradle_build_directory.path_join("libs");
user_data.debug = p_debug;
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
- err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so);
+ err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);
} else {
- err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so);
+ err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);
}
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
@@ -3544,7 +3564,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
APKExportData ed;
ed.ep = &ep;
ed.apk = unaligned_apk;
- err = export_project_files(p_preset, p_debug, ignore_apk_file, &ed, save_apk_so);
+ err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &ed, save_apk_so);
} else {
if (apk_expansion) {
err = save_apk_expansion_file(p_preset, p_debug, p_path);
@@ -3556,7 +3576,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
APKExportData ed;
ed.ep = &ep;
ed.apk = unaligned_apk;
- err = export_project_files(p_preset, p_debug, save_apk_file, &ed, save_apk_so);
+ err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);
}
}
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index 7e1d626486..23b6f9b193 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -35,6 +35,7 @@
#include "godot_plugin_config.h"
#endif // DISABLE_DEPRECATED
+#include "core/io/image.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
#include "editor/export/editor_export_platform.h"
@@ -141,9 +142,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
static Error save_apk_so(void *p_userdata, const SharedObject &p_so);
- static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
+ static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
- static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
+ static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so);
@@ -185,7 +186,7 @@ protected:
void _notification(int p_what);
public:
- typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
+ typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp
index 9eddef6a4c..433888581f 100644
--- a/platform/android/export/gradle_export_util.cpp
+++ b/platform/android/export/gradle_export_util.cpp
@@ -88,6 +88,8 @@ int _get_app_category_value(int category_index) {
return 7;
case APP_CATEGORY_SOCIAL:
return 4;
+ case APP_CATEGORY_UNDEFINED:
+ return -1;
case APP_CATEGORY_VIDEO:
return 2;
case APP_CATEGORY_GAME:
@@ -167,10 +169,11 @@ Error store_string_at_path(const String &p_path, const String &p_data) {
// It is used by the export_project_files method to save all the asset files into the gradle project.
// It's functionality mirrors that of the method save_apk_file.
// This method will be called ONLY when gradle build is enabled.
-Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
+Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed) {
CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
- String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/");
- print_verbose("Saving project files from " + p_path + " into " + dst_path);
+ const String path = ResourceUID::ensure_path(p_path);
+ const String dst_path = path.replace_first("res://", export_data->assets_directory + "/");
+ print_verbose("Saving project files from " + path + " into " + dst_path);
Error err = store_file_at_path(dst_path, p_data);
return err;
}
@@ -311,17 +314,21 @@ String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform,
" <application android:label=\"@string/godot_project_name_string\"\n"
" android:allowBackup=\"%s\"\n"
" android:icon=\"@mipmap/icon\"\n"
- " android:appCategory=\"%s\"\n"
" android:isGame=\"%s\"\n"
" android:hasFragileUserData=\"%s\"\n"
- " android:requestLegacyExternalStorage=\"%s\"\n"
- " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n"
- " tools:ignore=\"GoogleAppIndexingWarning\">\n\n",
+ " android:requestLegacyExternalStorage=\"%s\"\n",
bool_to_string(p_preset->get("user_data_backup/allow")),
- _get_app_category_label(app_category_index),
bool_to_string(is_game),
bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
bool_to_string(p_has_read_write_storage_permission));
+ if (app_category_index != APP_CATEGORY_UNDEFINED) {
+ manifest_application_text += vformat(" android:appCategory=\"%s\"\n", _get_app_category_label(app_category_index));
+ manifest_application_text += " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
+ } else {
+ manifest_application_text += " tools:remove=\"android:appCategory\"\n";
+ manifest_application_text += " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
+ }
+ manifest_application_text += " tools:ignore=\"GoogleAppIndexingWarning\">\n\n";
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
for (int i = 0; i < export_plugins.size(); i++) {
diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h
index 9f8e476f73..a528fd5211 100644
--- a/platform/android/export/gradle_export_util.h
+++ b/platform/android/export/gradle_export_util.h
@@ -55,6 +55,7 @@ static const int APP_CATEGORY_NEWS = 5;
static const int APP_CATEGORY_PRODUCTIVITY = 6;
static const int APP_CATEGORY_SOCIAL = 7;
static const int APP_CATEGORY_VIDEO = 8;
+static const int APP_CATEGORY_UNDEFINED = 9;
// Supported XR modes.
// This should match the entries in 'platform/android/java/lib/src/org/godotengine/godot/xr/XRMode.java'
@@ -92,7 +93,7 @@ Error store_string_at_path(const String &p_path, const String &p_data);
// It is used by the export_project_files method to save all the asset files into the gradle project.
// It's functionality mirrors that of the method save_apk_file.
// This method will be called ONLY when gradle build is enabled.
-Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
+Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed);
// Creates strings.xml files inside the gradle project for different locales.
Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir);
diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml
index 3ab8401928..3c86e54df5 100644
--- a/platform/android/java/app/res/values/themes.xml
+++ b/platform/android/java/app/res/values/themes.xml
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/>
+ <style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+ <item name ="android:windowDrawsSystemBarBackgrounds">false</item>
+ </style>
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
<!-- Set the splash screen background, animated icon, and animation
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 45222ca3b0..276d74b75b 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -173,7 +173,7 @@ dependencies {
implementation "androidx.window:window:1.3.0"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
- implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
+ implementation "org.bouncycastle:bcprov-jdk15to18:1.78"
// Meta dependencies
horizonosImplementation "org.godotengine:godot-openxr-vendors-meta:3.0.0-stable"
diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml
index a875745860..c1eb03b31f 100644
--- a/platform/android/java/editor/src/main/AndroidManifest.xml
+++ b/platform/android/java/editor/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.VIBRATE" />
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="false"
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt
index 7b6d1f6bd1..6aa2ba7195 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt
@@ -390,7 +390,7 @@ abstract class BaseGodotEditor : GodotActivity() {
* If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the
* editor setting or device and screen metrics.
*
- * If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default
+ * If the launch policy is [LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE] but PIP is not supported, fallback to the default
* launch policy.
*/
private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy {
@@ -453,9 +453,9 @@ abstract class BaseGodotEditor : GodotActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Check if we got the MANAGE_EXTERNAL_STORAGE permission
- if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- if (!Environment.isExternalStorageManager()) {
+ when (requestCode) {
+ PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
Toast.makeText(
this,
R.string.denied_storage_permission_error_msg,
@@ -463,6 +463,16 @@ abstract class BaseGodotEditor : GodotActivity() {
).show()
}
}
+
+ PermissionsUtil.REQUEST_INSTALL_PACKAGES_REQ_CODE -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !packageManager.canRequestPackageInstalls()) {
+ Toast.makeText(
+ this,
+ R.string.denied_install_packages_permission_error_msg,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
}
}
@@ -514,7 +524,7 @@ abstract class BaseGodotEditor : GodotActivity() {
override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") {
- return isNativeXRDevice();
+ return isNativeXRDevice()
}
if (featureTag == "horizonos") {
diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml
index 0ad54ac3a1..a25b6c0a2d 100644
--- a/platform/android/java/editor/src/main/res/values/strings.xml
+++ b/platform/android/java/editor/src/main/res/values/strings.xml
@@ -2,5 +2,6 @@
<resources>
<string name="godot_game_activity_name">Godot Play window</string>
<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
+ <string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string>
<string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
</resources>
diff --git a/platform/android/java/editor/src/main/res/values/themes.xml b/platform/android/java/editor/src/main/res/values/themes.xml
index 2b352247db..8de2c6e288 100644
--- a/platform/android/java/editor/src/main/res/values/themes.xml
+++ b/platform/android/java/editor/src/main/res/values/themes.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <style name="GodotEditorTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <style name="GodotEditorTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+ <item name ="android:windowDrawsSystemBarBackgrounds">false</item>
</style>
<style name="GodotEditorSplashScreenTheme" parent="Theme.SplashScreen.IconBackground">
diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle
index f6aee434e5..f273105efc 100644
--- a/platform/android/java/lib/build.gradle
+++ b/platform/android/java/lib/build.gradle
@@ -106,8 +106,8 @@ android {
boolean devBuild = buildType == "dev"
boolean debugSymbols = devBuild
boolean runTests = devBuild
- boolean productionBuild = !devBuild
boolean storeRelease = buildType == "release"
+ boolean productionBuild = storeRelease
def sconsTarget = flavorName
if (sconsTarget == "template") {
diff --git a/platform/android/java/lib/res/values/dimens.xml b/platform/android/java/lib/res/values/dimens.xml
index 9034dbbcc1..287d1c8920 100644
--- a/platform/android/java/lib/res/values/dimens.xml
+++ b/platform/android/java/lib/res/values/dimens.xml
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="text_edit_height">48dp</dimen>
+ <dimen name="input_dialog_padding_horizontal">10dp</dimen>
+ <dimen name="input_dialog_padding_vertical">5dp</dimen>
</resources>
diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml
index 03752e092e..e44addadd0 100644
--- a/platform/android/java/lib/res/values/strings.xml
+++ b/platform/android/java/lib/res/values/strings.xml
@@ -55,4 +55,7 @@
<string name="kilobytes_per_second">%1$s KB/s</string>
<string name="time_remaining">Time remaining: %1$s</string>
<string name="time_remaining_notification">%1$s left</string>
+
+ <!-- Labels for the dialog action buttons -->
+ <string name="dialog_ok">OK</string>
</resources>
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index 567b134234..3ad8e6bc9e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -44,6 +44,7 @@ import android.os.*
import android.util.Log
import android.util.TypedValue
import android.view.*
+import android.widget.EditText
import android.widget.FrameLayout
import androidx.annotation.Keep
import androidx.annotation.StringRes
@@ -56,6 +57,7 @@ import com.google.android.vending.expansion.downloader.*
import org.godotengine.godot.error.Error
import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.input.GodotInputHandler
+import org.godotengine.godot.io.FilePicker
import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler
import org.godotengine.godot.plugin.AndroidRuntimePlugin
@@ -81,6 +83,7 @@ import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
+
/**
* Core component used to interface with the native layer of the engine.
*
@@ -477,12 +480,17 @@ class Godot(private val context: Context) {
// ...add to FrameLayout
containerLayout?.addView(editText)
renderView = if (usesVulkan()) {
- if (!meetsVulkanRequirements(activity.packageManager)) {
+ if (meetsVulkanRequirements(activity.packageManager)) {
+ GodotVulkanRenderView(host, this, godotInputHandler)
+ } else if (canFallbackToOpenGL()) {
+ // Fallback to OpenGl.
+ GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
+ } else {
throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
}
- GodotVulkanRenderView(host, this, godotInputHandler)
+
} else {
- // Fallback to openGl
+ // Fallback to OpenGl.
GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
}
@@ -670,6 +678,9 @@ class Godot(private val context: Context) {
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainActivityResult(requestCode, resultCode, data)
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ FilePicker.handleActivityResult(context, requestCode, resultCode, data)
+ }
}
/**
@@ -772,7 +783,7 @@ class Godot(private val context: Context) {
val builder = AlertDialog.Builder(activity)
builder.setMessage(message).setTitle(title)
builder.setPositiveButton(
- "OK"
+ R.string.dialog_ok
) { dialog: DialogInterface, id: Int ->
okCallback?.run()
dialog.cancel()
@@ -817,6 +828,13 @@ class Godot(private val context: Context) {
}
/**
+ * Returns true if can fallback to OpenGL.
+ */
+ private fun canFallbackToOpenGL(): Boolean {
+ return java.lang.Boolean.parseBoolean(GodotLib.getGlobal("rendering/rendering_device/fallback_to_opengl3"))
+ }
+
+ /**
* Returns true if the device meets the base requirements for Vulkan support, false otherwise.
*/
private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean {
@@ -876,6 +894,44 @@ class Godot(private val context: Context) {
mClipboard.setPrimaryClip(clip)
}
+ @Keep
+ private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ FilePicker.showFilePicker(context, getActivity(), currentDirectory, filename, fileMode, filters)
+ }
+ }
+
+ /**
+ * Popup a dialog to input text.
+ */
+ @Keep
+ private fun showInputDialog(title: String, message: String, existingText: String) {
+ val activity: Activity = getActivity() ?: return
+ val inputField = EditText(activity)
+ val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.input_dialog_padding_horizontal)
+ val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.input_dialog_padding_vertical)
+ inputField.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
+ inputField.setText(existingText)
+ runOnUiThread {
+ val builder = AlertDialog.Builder(activity)
+ builder.setMessage(message).setTitle(title).setView(inputField)
+ builder.setPositiveButton(R.string.dialog_ok) {
+ dialog: DialogInterface, id: Int ->
+ GodotLib.inputDialogCallback(inputField.text.toString())
+ dialog.dismiss()
+ }
+ val dialog = builder.create()
+ dialog.show()
+ }
+ }
+
+ @Keep
+ private fun getAccentColor(): Int {
+ val value = TypedValue()
+ context.theme.resolveAttribute(android.R.attr.colorAccent, value, true)
+ return value.data
+ }
+
/**
* Destroys the Godot Engine and kill the process it's running in.
*/
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
index f060c7aaff..79751dd58f 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
@@ -47,6 +47,7 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
+import android.view.Surface;
import android.view.WindowInsets;
import androidx.core.content.FileProvider;
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 295a4a6340..13ae2150d7 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -225,6 +225,16 @@ public class GodotLib {
public static native void onNightModeChanged();
/**
+ * Invoked on the input dialog submitted.
+ */
+ public static native void inputDialogCallback(String p_text);
+
+ /**
+ * Invoked on the file picker closed.
+ */
+ public static native void filePickerCallback(boolean p_ok, String[] p_selected_paths);
+
+ /**
* Invoked on the GL thread to configure the height of the virtual keyboard.
*/
public static native void setVirtualKeyboardHeight(int p_height);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt b/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt
new file mode 100644
index 0000000000..19fb452892
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt
@@ -0,0 +1,200 @@
+/**************************************************************************/
+/* FilePicker.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.io
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.DocumentsContract
+import android.util.Log
+import android.webkit.MimeTypeMap
+import androidx.annotation.RequiresApi
+import org.godotengine.godot.GodotLib
+import org.godotengine.godot.io.file.MediaStoreData
+
+/**
+ * Utility class for managing file selection and file picker activities.
+ *
+ * It provides methods to launch a file picker and handle the result, supporting various file modes,
+ * including opening files, directories, and saving files.
+ */
+internal class FilePicker {
+ companion object {
+ private const val FILE_PICKER_REQUEST = 1000
+ private val TAG = FilePicker::class.java.simpleName
+
+ // Constants for fileMode values
+ private const val FILE_MODE_OPEN_FILE = 0
+ private const val FILE_MODE_OPEN_FILES = 1
+ private const val FILE_MODE_OPEN_DIR = 2
+ private const val FILE_MODE_OPEN_ANY = 3
+ private const val FILE_MODE_SAVE_FILE = 4
+
+ /**
+ * Handles the result from a file picker activity and processes the selected file(s) or directory.
+ *
+ * @param context The context from which the file picker was launched.
+ * @param requestCode The request code used when starting the file picker activity.
+ * @param resultCode The result code returned by the activity.
+ * @param data The intent data containing the selected file(s) or directory.
+ */
+ @RequiresApi(Build.VERSION_CODES.Q)
+ fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == FILE_PICKER_REQUEST) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ Log.d(TAG, "File picker canceled")
+ GodotLib.filePickerCallback(false, emptyArray())
+ return
+ }
+ if (resultCode == Activity.RESULT_OK) {
+ val selectedPaths: MutableList<String> = mutableListOf()
+ // Handle multiple file selection.
+ val clipData = data?.clipData
+ if (clipData != null) {
+ for (i in 0 until clipData.itemCount) {
+ val uri = clipData.getItemAt(i).uri
+ uri?.let {
+ val filepath = MediaStoreData.getFilePathFromUri(context, uri)
+ if (filepath != null) {
+ selectedPaths.add(filepath)
+ } else {
+ Log.d(TAG, "null filepath URI: $it")
+ }
+ }
+ }
+ } else {
+ val uri: Uri? = data?.data
+ uri?.let {
+ val filepath = MediaStoreData.getFilePathFromUri(context, uri)
+ if (filepath != null) {
+ selectedPaths.add(filepath)
+ } else {
+ Log.d(TAG, "null filepath URI: $it")
+ }
+ }
+ }
+
+ if (selectedPaths.isNotEmpty()) {
+ GodotLib.filePickerCallback(true, selectedPaths.toTypedArray())
+ } else {
+ GodotLib.filePickerCallback(false, emptyArray())
+ }
+ }
+ }
+ }
+
+ /**
+ * Launches a file picker activity with specified settings based on the mode, initial directory,
+ * file type filters, and other parameters.
+ *
+ * @param context The context from which to start the file picker.
+ * @param activity The activity instance used to initiate the picker. Required for activity results.
+ * @param currentDirectory The directory path to start the file picker in.
+ * @param filename The name of the file when using save mode.
+ * @param fileMode The mode to operate in, specifying open, save, or directory select.
+ * @param filters Array of MIME types to filter file selection.
+ */
+ @RequiresApi(Build.VERSION_CODES.Q)
+ fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
+ val intent = when (fileMode) {
+ FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT)
+ else -> Intent(Intent.ACTION_OPEN_DOCUMENT)
+ }
+ val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) {
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory)
+ } else {
+ Log.d(TAG, "Error cannot set initial directory")
+ }
+ if (fileMode == FILE_MODE_OPEN_FILES) {
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES
+ } else if (fileMode == FILE_MODE_SAVE_FILE) {
+ intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE
+ }
+ // ACTION_OPEN_DOCUMENT_TREE does not support intent type
+ if (fileMode != FILE_MODE_OPEN_DIR) {
+ intent.type = "*/*"
+ if (filters.isNotEmpty()) {
+ val resolvedFilters = filters.map { resolveMimeType(it) }.distinct()
+ if (resolvedFilters.size == 1) {
+ intent.type = resolvedFilters[0]
+ } else {
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, resolvedFilters.toTypedArray())
+ }
+ }
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
+ activity?.startActivityForResult(intent, FILE_PICKER_REQUEST)
+ }
+
+ /**
+ * Retrieves the MIME type for a given file extension.
+ *
+ * @param ext the extension whose MIME type is to be determined.
+ * @return the MIME type as a string, or "application/octet-stream" if the type is unknown.
+ */
+ private fun resolveMimeType(ext: String): String {
+ val mimeTypeMap = MimeTypeMap.getSingleton()
+ var input = ext
+
+ // Fix for extensions like "*.txt" or ".txt".
+ if (ext.contains(".")) {
+ input = ext.substring(ext.indexOf(".") + 1);
+ }
+
+ // Check if the input is already a valid MIME type.
+ if (mimeTypeMap.hasMimeType(input)) {
+ return input
+ }
+
+ val resolvedMimeType = mimeTypeMap.getMimeTypeFromExtension(input)
+ if (resolvedMimeType != null) {
+ return resolvedMimeType
+ }
+ // Check for wildcard MIME types like "image/*".
+ if (input.contains("/*")) {
+ val category = input.substringBefore("/*")
+ return when (category) {
+ "image" -> "image/*"
+ "video" -> "video/*"
+ "audio" -> "audio/*"
+ else -> "application/octet-stream"
+ }
+ }
+ // Fallback to a generic MIME type if the input is neither a valid extension nor MIME type.
+ return "application/octet-stream"
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
index 97362e2542..46bd465e90 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
@@ -38,6 +38,7 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
+import android.util.Log
import androidx.annotation.RequiresApi
import java.io.File
@@ -46,6 +47,7 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.nio.channels.FileChannel
+
/**
* Implementation of [DataAccess] which handles access and interactions with file and data
* under scoped storage via the MediaStore API.
@@ -81,6 +83,10 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
+ private const val AUTHORITY_MEDIA_DOCUMENTS = "com.android.providers.media.documents"
+ private const val AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS = "com.android.externalstorage.documents"
+ private const val AUTHORITY_DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents"
+
private fun getSelectionByPathArguments(path: String): Array<String> {
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
}
@@ -230,6 +236,72 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
)
return updated > 0
}
+
+ fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? {
+ if (!directoryExists(directoryPath)) {
+ return null
+ }
+ // Check if the path is under external storage.
+ val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath
+ if (directoryPath.startsWith(externalStorageRoot)) {
+ val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/')
+ val uri = Uri.Builder()
+ .scheme("content")
+ .authority(AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS)
+ .appendPath("document")
+ .appendPath("primary:$relativePath")
+ .build()
+ return uri
+ }
+ return null
+ }
+
+ fun getFilePathFromUri(context: Context, uri: Uri): String? {
+ // Converts content uri to filepath.
+ val id = getIdFromUri(uri) ?: return null
+
+ if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) {
+ val split = id.split(":")
+ val fileName = split.last()
+ val relativePath = split.dropLast(1).joinToString("/")
+ val fullPath = File(Environment.getExternalStorageDirectory(), "$relativePath/$fileName").absolutePath
+ return fullPath
+ } else {
+ val id = id.toLongOrNull() ?: return null
+ val dataItems = queryById(context, id)
+ return if (dataItems.isNotEmpty()) {
+ val dataItem = dataItems[0]
+ File(Environment.getExternalStorageDirectory(), File(dataItem.relativePath, dataItem.displayName).toString()).absolutePath
+ } else {
+ null
+ }
+ }
+ }
+
+ private fun getIdFromUri(uri: Uri): String? {
+ return try {
+ if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS || uri.authority == AUTHORITY_MEDIA_DOCUMENTS || uri.authority == AUTHORITY_DOWNLOADS_DOCUMENTS) {
+ val documentId = uri.lastPathSegment ?: throw IllegalArgumentException("Invalid URI: $uri")
+ documentId.substringAfter(":")
+ } else {
+ throw IllegalArgumentException("Unsupported URI format: $uri")
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "Failed to parse ID from URI: $uri", e)
+ null
+ }
+ }
+
+ private fun directoryExists(path: String): Boolean {
+ return try {
+ val file = File(path)
+ file.isDirectory && file.exists()
+ } catch (e: SecurityException) {
+ Log.d(TAG, "Failed to check directoryExists: $path", e)
+ false
+ }
+ }
+
}
private val id: Long
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 4e8e82a70a..885873e46d 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
@@ -49,7 +49,6 @@ import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -66,6 +65,7 @@ public final class PermissionsUtil {
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
+ public static final int REQUEST_INSTALL_PACKAGES_REQ_CODE = 3002;
private PermissionsUtil() {
}
@@ -105,10 +105,20 @@ public final class PermissionsUtil {
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
}
}
+ } else if (permission.equals(Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !activity.getPackageManager().canRequestPackageInstalls()) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
+ intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
+ activity.startActivityForResult(intent, REQUEST_INSTALL_PACKAGES_REQ_CODE);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to request permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ }
+ }
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, permission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
+ if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Requesting permission " + permission);
requestedPermissions.add(permission);
}
@@ -174,7 +184,7 @@ public final class PermissionsUtil {
try {
PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) {
+ if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE);
return false;
}
@@ -215,7 +225,7 @@ public final class PermissionsUtil {
try {
manifestPermissions = getManifestPermissions(activity);
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "Unable to retrieve manifest permissions", e);
return false;
}
@@ -242,7 +252,7 @@ public final class PermissionsUtil {
try {
manifestPermissions = getManifestPermissions(context);
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ Log.e(TAG, "Unable to retrieve manifest permissions", e);
return new String[0];
}
if (manifestPermissions.isEmpty()) {
@@ -259,7 +269,7 @@ public final class PermissionsUtil {
} else {
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(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
+ if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(manifestPermission);
}
}
diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt
index a7d2774db5..a5ecafeb09 100644
--- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt
+++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt
@@ -21,4 +21,4 @@ target_include_directories(${PROJECT_NAME}
${ANDROID_ROOT_DIR}
${OPENXR_INCLUDE_DIR})
-add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED)
+add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED -DDEBUG_ENABLED)
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 1a256959cd..5c1e78dcc4 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -540,6 +540,30 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JN
}
}
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text) {
+ DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
+ if (ds) {
+ String text = jstring_to_string(p_text, env);
+ ds->emit_input_dialog_callback(text);
+ }
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths) {
+ DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
+ if (ds) {
+ Vector<String> selected_paths;
+
+ jint length = env->GetArrayLength(p_selected_paths);
+ for (jint i = 0; i < length; ++i) {
+ jstring java_string = (jstring)env->GetObjectArrayElement(p_selected_paths, i);
+ String path = jstring_to_string(java_string, env);
+ selected_paths.push_back(path);
+ env->DeleteLocalRef(java_string);
+ }
+ ds->emit_file_picker_callback(p_ok, selected_paths);
+ }
+}
+
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) {
String permission = jstring_to_string(p_permission, env);
if (permission == "android.permission.RECORD_AUDIO" && p_result) {
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index 2165ce264b..31a7598a7b 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -67,6 +67,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index d3b30e4589..5ecd789d43 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -64,9 +64,12 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V");
_is_dark_mode_supported = p_env->GetMethodID(godot_class, "isDarkModeSupported", "()Z");
_is_dark_mode = p_env->GetMethodID(godot_class, "isDarkMode", "()Z");
+ _get_accent_color = p_env->GetMethodID(godot_class, "getAccentColor", "()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");
+ _show_input_dialog = p_env->GetMethodID(godot_class, "showInputDialog", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
+ _show_file_picker = p_env->GetMethodID(godot_class, "showFilePicker", "(Ljava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)V");
_request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z");
_request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z");
_get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;");
@@ -212,6 +215,23 @@ bool GodotJavaWrapper::is_dark_mode() {
}
}
+Color GodotJavaWrapper::get_accent_color() {
+ if (_get_accent_color) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, Color(0, 0, 0, 0));
+ int accent_color = env->CallIntMethod(godot_instance, _get_accent_color);
+
+ // Convert ARGB to RGBA.
+ int alpha = (accent_color >> 24) & 0xFF;
+ int red = (accent_color >> 16) & 0xFF;
+ int green = (accent_color >> 8) & 0xFF;
+ int blue = accent_color & 0xFF;
+ return Color(red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
+ } else {
+ return Color(0, 0, 0, 0);
+ }
+}
+
bool GodotJavaWrapper::has_get_clipboard() {
return _get_clipboard != nullptr;
}
@@ -268,6 +288,46 @@ bool GodotJavaWrapper::has_clipboard() {
}
}
+Error GodotJavaWrapper::show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text) {
+ if (_show_input_dialog) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+ jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data());
+ jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data());
+ jstring jStrExistingText = env->NewStringUTF(p_existing_text.utf8().get_data());
+ env->CallVoidMethod(godot_instance, _show_input_dialog, jStrTitle, jStrMessage, jStrExistingText);
+ env->DeleteLocalRef(jStrTitle);
+ env->DeleteLocalRef(jStrMessage);
+ env->DeleteLocalRef(jStrExistingText);
+ return OK;
+ } else {
+ return ERR_UNCONFIGURED;
+ }
+}
+
+Error GodotJavaWrapper::show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters) {
+ if (_show_file_picker) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+ jstring j_current_directory = env->NewStringUTF(p_current_directory.utf8().get_data());
+ jstring j_filename = env->NewStringUTF(p_filename.utf8().get_data());
+ jint j_mode = p_mode;
+ jobjectArray j_filters = env->NewObjectArray(p_filters.size(), env->FindClass("java/lang/String"), nullptr);
+ for (int i = 0; i < p_filters.size(); ++i) {
+ jstring j_filter = env->NewStringUTF(p_filters[i].utf8().get_data());
+ env->SetObjectArrayElement(j_filters, i, j_filter);
+ env->DeleteLocalRef(j_filter);
+ }
+ env->CallVoidMethod(godot_instance, _show_file_picker, j_current_directory, j_filename, j_mode, j_filters);
+ env->DeleteLocalRef(j_current_directory);
+ env->DeleteLocalRef(j_filename);
+ env->DeleteLocalRef(j_filters);
+ return OK;
+ } else {
+ return ERR_UNCONFIGURED;
+ }
+}
+
bool GodotJavaWrapper::request_permission(const String &p_name) {
if (_request_permission) {
JNIEnv *env = get_jni_env();
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 51d7f98541..512779169a 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -34,6 +34,7 @@
#include "java_godot_view_wrapper.h"
#include "string_android.h"
+#include "core/math/color.h"
#include "core/templates/list.h"
#include <android/log.h>
@@ -55,9 +56,12 @@ private:
jmethodID _alert = nullptr;
jmethodID _is_dark_mode_supported = nullptr;
jmethodID _is_dark_mode = nullptr;
+ jmethodID _get_accent_color = nullptr;
jmethodID _get_clipboard = nullptr;
jmethodID _set_clipboard = nullptr;
jmethodID _has_clipboard = nullptr;
+ jmethodID _show_input_dialog = nullptr;
+ jmethodID _show_file_picker = nullptr;
jmethodID _request_permission = nullptr;
jmethodID _request_permissions = nullptr;
jmethodID _get_granted_permissions = nullptr;
@@ -97,12 +101,15 @@ public:
void alert(const String &p_message, const String &p_title);
bool is_dark_mode_supported();
bool is_dark_mode();
+ Color get_accent_color();
bool has_get_clipboard();
String get_clipboard();
bool has_set_clipboard();
void set_clipboard(const String &p_text);
bool has_has_clipboard();
bool has_clipboard();
+ Error show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text);
+ Error show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters);
bool request_permission(const String &p_name);
bool request_permissions();
Vector<String> get_granted_permissions() const;
diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp
index 8f0ee51fac..9ab7d6a04f 100644
--- a/platform/android/net_socket_android.cpp
+++ b/platform/android/net_socket_android.cpp
@@ -84,7 +84,7 @@ NetSocketAndroid::~NetSocketAndroid() {
}
void NetSocketAndroid::close() {
- NetSocketPosix::close();
+ NetSocketUnix::close();
if (wants_broadcast) {
multicast_lock_release();
}
@@ -96,7 +96,7 @@ void NetSocketAndroid::close() {
}
Error NetSocketAndroid::set_broadcasting_enabled(bool p_enabled) {
- Error err = NetSocketPosix::set_broadcasting_enabled(p_enabled);
+ Error err = NetSocketUnix::set_broadcasting_enabled(p_enabled);
if (err != OK) {
return err;
}
@@ -115,7 +115,7 @@ Error NetSocketAndroid::set_broadcasting_enabled(bool p_enabled) {
}
Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) {
- Error err = NetSocketPosix::join_multicast_group(p_multi_address, p_if_name);
+ Error err = NetSocketUnix::join_multicast_group(p_multi_address, p_if_name);
if (err != OK) {
return err;
}
@@ -129,7 +129,7 @@ Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, c
}
Error NetSocketAndroid::leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) {
- Error err = NetSocketPosix::leave_multicast_group(p_multi_address, p_if_name);
+ Error err = NetSocketUnix::leave_multicast_group(p_multi_address, p_if_name);
if (err != OK) {
return err;
}
diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h
index 26cb2d4e3d..c33146d2d8 100644
--- a/platform/android/net_socket_android.h
+++ b/platform/android/net_socket_android.h
@@ -31,7 +31,7 @@
#ifndef NET_SOCKET_ANDROID_H
#define NET_SOCKET_ANDROID_H
-#include "drivers/unix/net_socket_posix.h"
+#include "drivers/unix/net_socket_unix.h"
#include <jni.h>
@@ -44,7 +44,7 @@
* the lock when broadcasting is enabled/disabled on a socket, or that socket
* joins/leaves a multicast group.
*/
-class NetSocketAndroid : public NetSocketPosix {
+class NetSocketAndroid : public NetSocketUnix {
private:
static jobject net_utils;
static jclass cls;