summaryrefslogtreecommitdiffstats
path: root/platform/android
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android')
-rw-r--r--platform/android/SCsub30
-rw-r--r--platform/android/android_input_handler.cpp5
-rw-r--r--platform/android/android_keys_utils.cpp9
-rw-r--r--platform/android/android_keys_utils.h20
-rw-r--r--platform/android/api/java_class_wrapper.h3
-rw-r--r--platform/android/api/jni_singleton.h11
-rw-r--r--platform/android/detect.py12
-rw-r--r--platform/android/dir_access_jandroid.cpp10
-rw-r--r--platform/android/dir_access_jandroid.h3
-rw-r--r--platform/android/display_server_android.cpp161
-rw-r--r--platform/android/display_server_android.h18
-rw-r--r--platform/android/doc_classes/EditorExportPlatformAndroid.xml81
-rw-r--r--platform/android/export/export.cpp10
-rw-r--r--platform/android/export/export_plugin.cpp535
-rw-r--r--platform/android/export/export_plugin.h32
-rw-r--r--platform/android/export/gradle_export_util.cpp8
-rw-r--r--platform/android/export/gradle_export_util.h3
-rw-r--r--platform/android/file_access_android.cpp98
-rw-r--r--platform/android/file_access_android.h17
-rw-r--r--platform/android/file_access_filesystem_jandroid.cpp87
-rw-r--r--platform/android/file_access_filesystem_jandroid.h9
-rw-r--r--platform/android/java/app/AndroidManifest.xml1
-rw-r--r--platform/android/java/app/assetPacks/installTime/build.gradle4
-rw-r--r--platform/android/java/app/build.gradle81
-rw-r--r--platform/android/java/app/config.gradle54
-rw-r--r--platform/android/java/app/gradle.properties3
-rw-r--r--platform/android/java/app/settings.gradle4
-rw-r--r--platform/android/java/build.gradle101
-rw-r--r--platform/android/java/editor/build.gradle19
-rw-r--r--platform/android/java/editor/src/main/AndroidManifest.xml7
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt19
-rw-r--r--platform/android/java/gradle.properties3
-rw-r--r--platform/android/java/gradle/wrapper/gradle-wrapper.properties3
-rw-r--r--platform/android/java/lib/AndroidManifest.xml1
-rw-r--r--platform/android/java/lib/build.gradle16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt220
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt39
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java38
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java44
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java44
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java35
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt20
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java30
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt17
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt39
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt53
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt37
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt83
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java148
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java10
-rw-r--r--platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt104
-rw-r--r--platform/android/java/nativeSrcsConfigs/AndroidManifest.xml2
-rw-r--r--platform/android/java/nativeSrcsConfigs/build.gradle2
-rw-r--r--platform/android/java/settings.gradle5
-rw-r--r--platform/android/java_class_wrapper.cpp18
-rw-r--r--platform/android/java_godot_io_wrapper.cpp11
-rw-r--r--platform/android/java_godot_lib_jni.cpp23
-rw-r--r--platform/android/java_godot_lib_jni.h1
-rw-r--r--platform/android/java_godot_view_wrapper.cpp1
-rw-r--r--platform/android/java_godot_view_wrapper.h2
-rw-r--r--platform/android/java_godot_wrapper.cpp36
-rw-r--r--platform/android/java_godot_wrapper.h8
-rw-r--r--platform/android/net_socket_android.cpp12
-rw-r--r--platform/android/net_socket_android.h5
-rw-r--r--platform/android/os_android.cpp92
-rw-r--r--platform/android/os_android.h10
-rw-r--r--platform/android/rendering_context_driver_vulkan_android.cpp (renamed from platform/android/vulkan_context_android.cpp)38
-rw-r--r--platform/android/rendering_context_driver_vulkan_android.h (renamed from platform/android/vulkan_context_android.h)30
-rw-r--r--platform/android/tts_android.cpp10
-rw-r--r--platform/android/tts_android.h1
76 files changed, 2072 insertions, 718 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub
index dfc921cc54..31bc7c25b0 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -1,5 +1,7 @@
#!/usr/bin/env python
+import subprocess
+
Import("env")
android_files = [
@@ -21,7 +23,7 @@ android_files = [
"android_keys_utils.cpp",
"display_server_android.cpp",
"plugin/godot_plugin_jni.cpp",
- "vulkan_context_android.cpp",
+ "rendering_context_driver_vulkan_android.cpp",
]
env_android = env.Clone()
@@ -77,3 +79,29 @@ if lib_arch_dir != "":
str(env["ANDROID_NDK_ROOT"]) + "/sources/cxx-stl/llvm-libc++/libs/" + lib_arch_dir + "/libc++_shared.so"
)
env_android.Command(out_dir + "/libc++_shared.so", stl_lib_path, Copy("$TARGET", "$SOURCE"))
+
+ def generate_apk(target, source, env):
+ if env["target"] != "editor" and env["dev_build"]:
+ subprocess.run(
+ [
+ "./gradlew",
+ "generateDevTemplate",
+ "--quiet",
+ ],
+ cwd="platform/android/java",
+ )
+ else:
+ # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task.
+ subprocess.run(
+ [
+ "./gradlew",
+ "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates",
+ "--quiet",
+ ],
+ cwd="platform/android/java",
+ )
+
+ if env["generate_apk"]:
+ generate_apk_command = env_android.Command("generate_apk", [], generate_apk)
+ command = env_android.AlwaysBuild(generate_apk_command)
+ env_android.Depends(command, [lib])
diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp
index bd194478d9..373dd399e4 100644
--- a/platform/android/android_input_handler.cpp
+++ b/platform/android/android_input_handler.cpp
@@ -124,6 +124,7 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod
ev->set_physical_keycode(physical_keycode);
ev->set_key_label(fix_key_label(p_key_label, keycode));
ev->set_unicode(fix_unicode(unicode));
+ ev->set_location(godot_location_from_android_code(p_physical_keycode));
ev->set_pressed(p_pressed);
ev->set_echo(p_echo);
@@ -206,6 +207,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const
ev->set_index(touch[i].id);
ev->set_position(p_points[idx].pos);
ev->set_relative(p_points[idx].pos - touch[i].pos);
+ ev->set_relative_screen_position(ev->get_relative());
Input::get_singleton()->parse_input_event(ev);
touch.write[i].pos = p_points[idx].pos;
}
@@ -305,6 +307,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an
ev->set_position(p_event_pos);
ev->set_global_position(p_event_pos);
ev->set_relative(p_event_pos - hover_prev_pos);
+ ev->set_relative_screen_position(ev->get_relative());
Input::get_singleton()->parse_input_event(ev);
hover_prev_pos = p_event_pos;
} break;
@@ -341,10 +344,12 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an
ev->set_position(hover_prev_pos);
ev->set_global_position(hover_prev_pos);
ev->set_relative(p_event_pos);
+ ev->set_relative_screen_position(p_event_pos);
} else {
ev->set_position(p_event_pos);
ev->set_global_position(p_event_pos);
ev->set_relative(p_event_pos - hover_prev_pos);
+ ev->set_relative_screen_position(ev->get_relative());
mouse_event_info.pos = p_event_pos;
hover_prev_pos = p_event_pos;
}
diff --git a/platform/android/android_keys_utils.cpp b/platform/android/android_keys_utils.cpp
index f50437e82a..83ee98e8bc 100644
--- a/platform/android/android_keys_utils.cpp
+++ b/platform/android/android_keys_utils.cpp
@@ -38,3 +38,12 @@ Key godot_code_from_android_code(unsigned int p_code) {
}
return Key::UNKNOWN;
}
+
+KeyLocation godot_location_from_android_code(unsigned int p_code) {
+ for (int i = 0; android_godot_location_pairs[i].android_code != AKEYCODE_MAX; i++) {
+ if (android_godot_location_pairs[i].android_code == p_code) {
+ return android_godot_location_pairs[i].godot_code;
+ }
+ }
+ return KeyLocation::UNSPECIFIED;
+}
diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h
index 5cf5628a8b..77c0911f2b 100644
--- a/platform/android/android_keys_utils.h
+++ b/platform/android/android_keys_utils.h
@@ -177,4 +177,24 @@ static AndroidGodotCodePair android_godot_code_pairs[] = {
Key godot_code_from_android_code(unsigned int p_code);
+// Key location determination.
+struct AndroidGodotLocationPair {
+ unsigned int android_code = 0;
+ KeyLocation godot_code = KeyLocation::UNSPECIFIED;
+};
+
+static AndroidGodotLocationPair android_godot_location_pairs[] = {
+ { AKEYCODE_ALT_LEFT, KeyLocation::LEFT },
+ { AKEYCODE_ALT_RIGHT, KeyLocation::RIGHT },
+ { AKEYCODE_SHIFT_LEFT, KeyLocation::LEFT },
+ { AKEYCODE_SHIFT_RIGHT, KeyLocation::RIGHT },
+ { AKEYCODE_CTRL_LEFT, KeyLocation::LEFT },
+ { AKEYCODE_CTRL_RIGHT, KeyLocation::RIGHT },
+ { AKEYCODE_META_LEFT, KeyLocation::LEFT },
+ { AKEYCODE_META_RIGHT, KeyLocation::RIGHT },
+ { AKEYCODE_MAX, KeyLocation::UNSPECIFIED }
+};
+
+KeyLocation godot_location_from_android_code(unsigned int p_code);
+
#endif // ANDROID_KEYS_UTILS_H
diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h
index b1481ebf7b..e21a331ab9 100644
--- a/platform/android/api/java_class_wrapper.h
+++ b/platform/android/api/java_class_wrapper.h
@@ -209,8 +209,6 @@ class JavaClassWrapper : public Object {
#ifdef ANDROID_ENABLED
RBMap<String, Ref<JavaClass>> class_cache;
friend class JavaClass;
- jclass activityClass;
- jmethodID findClass;
jmethodID getDeclaredMethods;
jmethodID getFields;
jmethodID getParameterTypes;
@@ -229,7 +227,6 @@ class JavaClassWrapper : public Object {
jmethodID Long_longValue;
jmethodID Float_floatValue;
jmethodID Double_doubleValue;
- jobject classLoader;
bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig);
#endif
diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h
index a2d1c08168..5b30c392e7 100644
--- a/platform/android/api/jni_singleton.h
+++ b/platform/android/api/jni_singleton.h
@@ -241,6 +241,17 @@ public:
instance = nullptr;
#endif
}
+
+ ~JNISingleton() {
+#ifdef ANDROID_ENABLED
+ if (instance) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(instance);
+ }
+#endif
+ }
};
#endif // JNI_SINGLETON_H
diff --git a/platform/android/detect.py b/platform/android/detect.py
index a417ef454b..fea8ec3287 100644
--- a/platform/android/detect.py
+++ b/platform/android/detect.py
@@ -6,7 +6,7 @@ import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from SCons import Environment
+ from SCons.Script.SConscript import SConsEnvironment
def get_name():
@@ -28,6 +28,7 @@ def get_opts():
"android-" + str(get_min_target_api()),
),
BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False),
+ BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False),
]
@@ -50,7 +51,7 @@ def get_min_sdk_version(platform):
return int(platform.split("-")[1])
-def get_android_ndk_root(env):
+def get_android_ndk_root(env: "SConsEnvironment"):
return env["ANDROID_HOME"] + "/ndk/" + get_ndk_version()
@@ -68,12 +69,13 @@ def get_flags():
return [
("arch", "arm64"), # Default for convenience.
("target", "template_debug"),
+ ("supported", ["mono"]),
]
# Check if Android NDK version is installed
# If not, install it.
-def install_ndk_if_needed(env):
+def install_ndk_if_needed(env: "SConsEnvironment"):
print("Checking for Android NDK...")
sdk_root = env["ANDROID_HOME"]
if not os.path.exists(get_android_ndk_root(env)):
@@ -95,7 +97,7 @@ def install_ndk_if_needed(env):
env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env)
-def configure(env: "Environment"):
+def configure(env: "SConsEnvironment"):
# Validate arch.
supported_arches = ["x86_32", "x86_64", "arm32", "arm64"]
if env["arch"] not in supported_arches:
@@ -200,7 +202,7 @@ def configure(env: "Environment"):
env.Append(LIBS=["OpenSLES", "EGL", "android", "log", "z", "dl"])
if env["vulkan"]:
- env.Append(CPPDEFINES=["VULKAN_ENABLED"])
+ env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
if not env["use_volk"]:
env.Append(LIBS=["vulkan"])
diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp
index d24d3fa389..ab90527bfa 100644
--- a/platform/android/dir_access_jandroid.cpp
+++ b/platform/android/dir_access_jandroid.cpp
@@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) {
}
}
-Error DirAccessJAndroid::make_dir_recursive(String p_dir) {
+Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
// Check if the directory exists already
if (dir_exists(p_dir)) {
return ERR_ALREADY_EXISTS;
@@ -321,6 +321,14 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
}
+void DirAccessJAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(dir_access_handler);
+}
+
DirAccessJAndroid::DirAccessJAndroid() {
}
diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h
index 5ee4c85659..68578b0fa9 100644
--- a/platform/android/dir_access_jandroid.h
+++ b/platform/android/dir_access_jandroid.h
@@ -77,7 +77,7 @@ public:
virtual bool dir_exists(String p_dir) override;
virtual Error make_dir(String p_dir) override;
- virtual Error make_dir_recursive(String p_dir) override;
+ virtual Error make_dir_recursive(const String &p_dir) override;
virtual Error rename(String p_from, String p_to) override;
virtual Error remove(String p_name) override;
@@ -89,6 +89,7 @@ public:
virtual uint64_t get_space_left() override;
static void setup(jobject p_dir_access_handler);
+ static void terminate();
DirAccessJAndroid();
~DirAccessJAndroid();
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index dd5ab46bd7..c6f2f82117 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -37,11 +37,13 @@
#include "core/config/project_settings.h"
-#if defined(VULKAN_ENABLED)
-#include "vulkan_context_android.h"
-
-#include "drivers/vulkan/rendering_device_vulkan.h"
+#if defined(RD_ENABLED)
#include "servers/rendering/renderer_rd/renderer_compositor_rd.h"
+#include "servers/rendering/rendering_device.h"
+
+#if defined(VULKAN_ENABLED)
+#include "rendering_context_driver_vulkan_android.h"
+#endif
#endif
#ifdef GLES3_ENABLED
@@ -56,15 +58,21 @@ DisplayServerAndroid *DisplayServerAndroid::get_singleton() {
bool DisplayServerAndroid::has_feature(Feature p_feature) const {
switch (p_feature) {
+#ifndef DISABLE_DEPRECATED
+ case FEATURE_GLOBAL_MENU: {
+ return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
+ } break;
+#endif
case FEATURE_CURSOR_SHAPE:
//case FEATURE_CUSTOM_CURSOR_SHAPE:
- //case FEATURE_GLOBAL_MENU:
//case FEATURE_HIDPI:
//case FEATURE_ICON:
//case FEATURE_IME:
case FEATURE_MOUSE:
//case FEATURE_MOUSE_WARP:
//case FEATURE_NATIVE_DIALOG:
+ //case FEATURE_NATIVE_DIALOG_INPUT:
+ //case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD:
@@ -125,6 +133,16 @@ bool DisplayServerAndroid::is_dark_mode() const {
return godot_java->is_dark_mode();
}
+void DisplayServerAndroid::set_system_theme_change_callback(const Callable &p_callable) {
+ system_theme_changed = p_callable;
+}
+
+void DisplayServerAndroid::emit_system_theme_changed() {
+ if (system_theme_changed.is_valid()) {
+ system_theme_changed.call_deferred();
+ }
+}
+
void DisplayServerAndroid::clipboard_set(const String &p_text) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL(godot_java);
@@ -515,20 +533,41 @@ void DisplayServerAndroid::register_android_driver() {
}
void DisplayServerAndroid::reset_window() {
-#if defined(VULKAN_ENABLED)
- if (rendering_driver == "vulkan") {
- ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
- ERR_FAIL_NULL(native_window);
+#if defined(RD_ENABLED)
+ if (rendering_context) {
+ if (rendering_device) {
+ rendering_device->screen_free(MAIN_WINDOW_ID);
+ }
- ERR_FAIL_NULL(context_vulkan);
- VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID);
- context_vulkan->window_destroy(MAIN_WINDOW_ID);
+ VSyncMode last_vsync_mode = rendering_context->window_get_vsync_mode(MAIN_WINDOW_ID);
+ rendering_context->window_destroy(MAIN_WINDOW_ID);
+
+ union {
+#ifdef VULKAN_ENABLED
+ RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan;
+#endif
+ } wpd;
+#ifdef VULKAN_ENABLED
+ if (rendering_driver == "vulkan") {
+ ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
+ ERR_FAIL_NULL(native_window);
+ wpd.vulkan.window = native_window;
+ }
+#endif
+
+ if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) {
+ ERR_PRINT(vformat("Failed to reset %s window.", rendering_driver));
+ memdelete(rendering_context);
+ rendering_context = nullptr;
+ return;
+ }
Size2i display_size = OS_Android::get_singleton()->get_display_size();
- if (context_vulkan->window_create(native_window, last_vsync_mode, display_size.width, display_size.height) != OK) {
- memdelete(context_vulkan);
- context_vulkan = nullptr;
- ERR_FAIL_MSG("Failed to reset Vulkan window.");
+ rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height);
+ rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, last_vsync_mode);
+
+ if (rendering_device) {
+ rendering_device->screen_create(MAIN_WINDOW_ID);
}
}
#endif
@@ -545,36 +584,59 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on");
+ native_menu = memnew(NativeMenu);
+
#if defined(GLES3_ENABLED)
if (rendering_driver == "opengl3") {
RasterizerGLES3::make_current(false);
}
#endif
-#if defined(VULKAN_ENABLED)
- context_vulkan = nullptr;
- rendering_device_vulkan = nullptr;
+#if defined(RD_ENABLED)
+ rendering_context = nullptr;
+ rendering_device = nullptr;
+#if defined(VULKAN_ENABLED)
if (rendering_driver == "vulkan") {
- ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
- ERR_FAIL_NULL(native_window);
-
- context_vulkan = memnew(VulkanContextAndroid);
- if (context_vulkan->initialize() != OK) {
- memdelete(context_vulkan);
- context_vulkan = nullptr;
- ERR_FAIL_MSG("Failed to initialize Vulkan context");
+ rendering_context = memnew(RenderingContextDriverVulkanAndroid);
+ }
+#endif
+
+ if (rendering_context) {
+ if (rendering_context->initialize() != OK) {
+ ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
+ memdelete(rendering_context);
+ rendering_context = nullptr;
+ return;
}
- Size2i display_size = OS_Android::get_singleton()->get_display_size();
- if (context_vulkan->window_create(native_window, p_vsync_mode, display_size.width, display_size.height) != OK) {
- memdelete(context_vulkan);
- context_vulkan = nullptr;
- ERR_FAIL_MSG("Failed to create Vulkan window.");
+ union {
+#ifdef VULKAN_ENABLED
+ RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan;
+#endif
+ } wpd;
+#ifdef VULKAN_ENABLED
+ if (rendering_driver == "vulkan") {
+ ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
+ ERR_FAIL_NULL(native_window);
+ wpd.vulkan.window = native_window;
}
+#endif
- rendering_device_vulkan = memnew(RenderingDeviceVulkan);
- rendering_device_vulkan->initialize(context_vulkan);
+ if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) {
+ ERR_PRINT(vformat("Failed to create %s window.", rendering_driver));
+ memdelete(rendering_context);
+ rendering_context = nullptr;
+ return;
+ }
+
+ Size2i display_size = OS_Android::get_singleton()->get_display_size();
+ rendering_context->window_set_size(MAIN_WINDOW_ID, display_size.width, display_size.height);
+ rendering_context->window_set_vsync_mode(MAIN_WINDOW_ID, p_vsync_mode);
+
+ rendering_device = memnew(RenderingDevice);
+ rendering_device->initialize(rendering_context, MAIN_WINDOW_ID);
+ rendering_device->screen_create(MAIN_WINDOW_ID);
RendererCompositorRD::make_current();
}
@@ -587,16 +649,17 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
}
DisplayServerAndroid::~DisplayServerAndroid() {
-#if defined(VULKAN_ENABLED)
- if (rendering_driver == "vulkan") {
- if (rendering_device_vulkan) {
- rendering_device_vulkan->finalize();
- memdelete(rendering_device_vulkan);
- }
+ if (native_menu) {
+ memdelete(native_menu);
+ native_menu = nullptr;
+ }
- if (context_vulkan) {
- memdelete(context_vulkan);
- }
+#if defined(RD_ENABLED)
+ if (rendering_device) {
+ memdelete(rendering_device);
+ }
+ if (rendering_context) {
+ memdelete(rendering_context);
}
#endif
}
@@ -687,17 +750,17 @@ void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor
}
void DisplayServerAndroid::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) {
-#if defined(VULKAN_ENABLED)
- if (context_vulkan) {
- context_vulkan->set_vsync_mode(p_window, p_vsync_mode);
+#if defined(RD_ENABLED)
+ if (rendering_context) {
+ rendering_context->window_set_vsync_mode(p_window, p_vsync_mode);
}
#endif
}
DisplayServer::VSyncMode DisplayServerAndroid::window_get_vsync_mode(WindowID p_window) const {
-#if defined(VULKAN_ENABLED)
- if (context_vulkan) {
- return context_vulkan->get_vsync_mode(p_window);
+#if defined(RD_ENABLED)
+ if (rendering_context) {
+ return rendering_context->window_get_vsync_mode(p_window);
}
#endif
return DisplayServer::VSYNC_ENABLED;
diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h
index 54912212dc..e1914f4d18 100644
--- a/platform/android/display_server_android.h
+++ b/platform/android/display_server_android.h
@@ -33,9 +33,9 @@
#include "servers/display_server.h"
-#if defined(VULKAN_ENABLED)
-class VulkanContextAndroid;
-class RenderingDeviceVulkan;
+#if defined(RD_ENABLED)
+class RenderingContextDriver;
+class RenderingDevice;
#endif
class DisplayServerAndroid : public DisplayServer {
@@ -72,10 +72,11 @@ class DisplayServerAndroid : public DisplayServer {
CursorShape cursor_shape = CursorShape::CURSOR_ARROW;
-#if defined(VULKAN_ENABLED)
- VulkanContextAndroid *context_vulkan = nullptr;
- RenderingDeviceVulkan *rendering_device_vulkan = nullptr;
+#if defined(RD_ENABLED)
+ RenderingContextDriver *rendering_context = nullptr;
+ RenderingDevice *rendering_device = nullptr;
#endif
+ NativeMenu *native_menu = nullptr;
ObjectID window_attached_instance_id;
@@ -84,6 +85,8 @@ class DisplayServerAndroid : public DisplayServer {
Callable input_text_callback;
Callable rect_changed_callback;
+ Callable system_theme_changed;
+
void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const;
static void _dispatch_input_events(const Ref<InputEvent> &p_event);
@@ -103,8 +106,11 @@ public:
virtual void tts_resume() override;
virtual void tts_stop() override;
+ void emit_system_theme_changed();
+
virtual bool is_dark_mode_supported() const override;
virtual bool is_dark_mode() const override;
+ virtual void set_system_theme_change_callback(const Callable &p_callable) override;
virtual void clipboard_set(const String &p_text) override;
virtual String clipboard_get() const override;
diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
index dae968378b..020e432155 100644
--- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml
+++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
@@ -15,11 +15,11 @@
Array of random bytes that the licensing Policy uses to create an [url=https://developer.android.com/google/play/licensing/adding-licensing#impl-Obfuscator]Obfuscator[/url].
</member>
<member name="apk_expansion/enable" type="bool" setter="" getter="">
- If [code]true[/code], project resources are stored in the separate APK expansion file, instead APK.
- [b]Note:[/b] APK expansion should be enabled to use PCK encryption.
+ If [code]true[/code], project resources are stored in the separate APK expansion file, instead of the APK.
+ [b]Note:[/b] APK expansion should be enabled to use PCK encryption. See [url=https://developer.android.com/google/play/expansion-files]APK Expansion Files[/url]
</member>
<member name="apk_expansion/public_key" type="String" setter="" getter="">
- Base64 encoded RSA public key for your publisher account, available from the profile page on the "Play Console".
+ Base64 encoded RSA public key for your publisher account, available from the profile page on the "Google Play Console".
</member>
<member name="architectures/arm64-v8a" type="bool" setter="" getter="">
If [code]true[/code], [code]arm64[/code] binaries are included into exported project.
@@ -34,22 +34,34 @@
If [code]true[/code], [code]x86_64[/code] binaries are included into exported project.
</member>
<member name="command_line/extra_args" type="String" setter="" getter="">
- A list of additional command line arguments, exported project will receive when started.
+ A list of additional command line arguments, separated by space, which the exported project will receive when started.
</member>
<member name="custom_template/debug" type="String" setter="" getter="">
- Path to the custom export template. If left empty, default template is used.
+ Path to an APK file to use as a custom export template for debug exports. If left empty, default template is used.
+ [b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled.
</member>
<member name="custom_template/release" type="String" setter="" getter="">
- Path to the custom export template. If left empty, default template is used.
+ Path to an APK file to use as a custom export template for release exports. If left empty, default template is used.
+ [b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled.
+ </member>
+ <member name="gradle_build/android_source_template" type="String" setter="" getter="">
+ Path to a ZIP file holding the source for the export template used in a Gradle build. If left empty, the default template is used.
+ </member>
+ <member name="gradle_build/compress_native_libraries" type="bool" setter="" getter="">
+ If [code]true[/code], native libraries are compressed when performing a Gradle build.
+ [b]Note:[/b] Although your binary may be smaller, your application may load slower because the native libraries are not loaded directly from the binary at runtime.
</member>
<member name="gradle_build/export_format" type="int" setter="" getter="">
- Export format for Gradle build.
+ Application export format (*.apk or *.aab).
+ </member>
+ <member name="gradle_build/gradle_build_directory" type="String" setter="" getter="">
+ Path to the Gradle build directory. If left empty, then [code]res://android[/code] will be used.
</member>
<member name="gradle_build/min_sdk" type="String" setter="" getter="">
- Minimal Android SDK version for Gradle build.
+ Minimum Android API level required for the application to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:minSdkVersion[/url].
</member>
<member name="gradle_build/target_sdk" type="String" setter="" getter="">
- Target Android SDK version for Gradle build.
+ The Android API level on which the application is designed to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:targetSdkVersion[/url].
</member>
<member name="gradle_build/use_gradle_build" type="bool" setter="" getter="">
If [code]true[/code], Gradle build is used instead of pre-built APK.
@@ -85,25 +97,25 @@
Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_USER[/code].
</member>
<member name="launcher_icons/adaptive_background_432x432" type="String" setter="" getter="">
- Background layer of the application adaptive icon file.
+ Background layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
</member>
<member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter="">
- Foreground layer of the application adaptive icon file.
+ Foreground layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url].
</member>
<member name="launcher_icons/main_192x192" type="String" setter="" getter="">
Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon].
</member>
<member name="package/app_category" type="int" setter="" getter="">
- Application category for the Play Store.
+ Application category for the Google Play Store. Only define this if your application fits one of the categories well. See [url=https://developer.android.com/guide/topics/manifest/application-element#appCategory]android:appCategory[/url].
</member>
<member name="package/exclude_from_recents" type="bool" setter="" getter="">
- If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications.
+ If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications. See [url=https://developer.android.com/guide/topics/manifest/activity-element#exclude]android:excludeFromRecents[/url].
</member>
<member name="package/name" type="String" setter="" getter="">
Name of the application.
</member>
<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.
+ If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. See [url=https://developer.android.com/guide/topics/manifest/application-element#fragileuserdata]android:hasFragileUserData[/url].
</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.
@@ -119,7 +131,10 @@
If [code]true[/code], package signing is enabled.
</member>
<member name="package/unique_name" type="String" setter="" getter="">
- Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]).
+ Unique application identifier in a reverse-DNS format. The reverse DNS format should preferably match a domain name you control, but this is not strictly required. For instance, if you own [code]example.com[/code], your package unique name should preferably be of the form [code]com.example.mygame[/code]. This identifier can only contain lowercase alphanumeric characters ([code]a-z[/code], and [code]0-9[/code]), underscores ([code]_[/code]), and periods ([code].[/code]). Each component of the reverse DNS format must start with a letter: for instance, [code]com.example.8game[/code] is not valid.
+ If [code]$genname[/code] is present in the value, it will be replaced by the project name converted to lowercase. If there are invalid characters in the project name, they will be stripped. If all characters in the project name are stripped, [code]$genname[/code] is replaced by [code]noname[/code].
+ [b]Note:[/b] Changing the package name will cause the package to be considered as a new package, with its own installation and data paths. The new package won't be usable to update existing installations.
+ [b]Note:[/b] When publishing to Google Play, the package name must be [i]globally[/i] unique. This means no other apps published on Google Play must be using the same package name as yours. Otherwise, you'll be prevented from publishing your app on Google Play.
</member>
<member name="permissions/access_checkin_properties" type="bool" setter="" getter="">
Allows read/write access to the "properties" table in the checkin database. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_CHECKIN_PROPERTIES]ACCESS_CHECKIN_PROPERTIES[/url].
@@ -259,8 +274,7 @@
<member name="permissions/custom_permissions" type="PackedStringArray" setter="" getter="">
Array of custom permission strings.
</member>
- <member name="permissions/delete_cache_files" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/delete_cache_files" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/delete_packages" type="bool" setter="" getter="">
Allows an application to delete packages. See [url=https://developer.android.com/reference/android/Manifest.permission#DELETE_PACKAGES]DELETE_PACKAGES[/url].
@@ -295,8 +309,7 @@
<member name="permissions/get_package_size" type="bool" setter="" getter="">
Allows an application to find out the space used by any package. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_PACKAGE_SIZE]GET_PACKAGE_SIZE[/url].
</member>
- <member name="permissions/get_tasks" type="bool" setter="" getter="">
- Deprecated in API level 21.
+ <member name="permissions/get_tasks" type="bool" setter="" getter="" deprecated="Deprecated in API level 21.">
</member>
<member name="permissions/get_top_activity_info" type="bool" setter="" getter="">
Allows an application to retrieve private information about the current top activity.
@@ -364,13 +377,14 @@
<member name="permissions/nfc" type="bool" setter="" getter="">
Allows applications to perform I/O operations over NFC. See [url=https://developer.android.com/reference/android/Manifest.permission#NFC]NFC[/url].
</member>
- <member name="permissions/persistent_activity" type="bool" setter="" getter="">
- Allow an application to make its activities persistent.
- Deprecated in API level 15.
+ <member name="permissions/persistent_activity" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
+ Allows an application to make its activities persistent.
+ </member>
+ <member name="permissions/post_notifications" type="bool" setter="" getter="">
+ Allows an application to post notifications. Added in API level 33. See [url=https://developer.android.com/develop/ui/views/notifications/notification-permission]Notification runtime permission[/url].
</member>
- <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="">
+ <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="" deprecated="Deprecated in API level 29.">
Allows an application to see the number being dialed during an outgoing call with the option to redirect the call to a different number or abort the call altogether. See [url=https://developer.android.com/reference/android/Manifest.permission#PROCESS_OUTGOING_CALLS]PROCESS_OUTGOING_CALLS[/url].
- Deprecated in API level 29.
</member>
<member name="permissions/read_calendar" type="bool" setter="" getter="">
Allows an application to read the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALENDAR]READ_CALENDAR[/url].
@@ -381,9 +395,8 @@
<member name="permissions/read_contacts" type="bool" setter="" getter="">
Allows an application to read the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CONTACTS]READ_CONTACTS[/url].
</member>
- <member name="permissions/read_external_storage" type="bool" setter="" getter="">
+ <member name="permissions/read_external_storage" type="bool" setter="" getter="" deprecated="Deprecated in API level 33.">
Allows an application to read from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE]READ_EXTERNAL_STORAGE[/url].
- Deprecated in API level 33.
</member>
<member name="permissions/read_frame_buffer" type="bool" setter="" getter="">
Allows an application to take screen shots and more generally get access to the frame buffer data.
@@ -391,8 +404,7 @@
<member name="permissions/read_history_bookmarks" type="bool" setter="" getter="">
Allows an application to read (but not write) the user's browsing history and bookmarks.
</member>
- <member name="permissions/read_input_state" type="bool" setter="" getter="">
- Deprecated in API level 16.
+ <member name="permissions/read_input_state" type="bool" setter="" getter="" deprecated="Deprecated in API level 16.">
</member>
<member name="permissions/read_logs" type="bool" setter="" getter="">
Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url].
@@ -439,8 +451,7 @@
<member name="permissions/reorder_tasks" type="bool" setter="" getter="">
Allows an application to change the Z-order of tasks. See [url=https://developer.android.com/reference/android/Manifest.permission#REORDER_TASKS]REORDER_TASKS[/url].
</member>
- <member name="permissions/restart_packages" type="bool" setter="" getter="">
- Deprecated in API level 15.
+ <member name="permissions/restart_packages" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
</member>
<member name="permissions/send_respond_via_message" type="bool" setter="" getter="">
Allows an application (Phone) to send a request to other applications to handle the respond-via-message action during incoming calls. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_RESPOND_VIA_MESSAGE]SEND_RESPOND_VIA_MESSAGE[/url].
@@ -469,8 +480,7 @@
<member name="permissions/set_pointer_speed" type="bool" setter="" getter="">
Allows low-level access to setting the pointer speed.
</member>
- <member name="permissions/set_preferred_applications" type="bool" setter="" getter="">
- Deprecated in API level 15.
+ <member name="permissions/set_preferred_applications" type="bool" setter="" getter="" deprecated="Deprecated in API level 15.">
</member>
<member name="permissions/set_process_limit" type="bool" setter="" getter="">
Allows an application to set the maximum number of (not needed) application processes that can be running. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_PROCESS_LIMIT]SET_PROCESS_LIMIT[/url].
@@ -496,8 +506,7 @@
<member name="permissions/subscribed_feeds_read" type="bool" setter="" getter="">
Allows an application to allow access the subscribed feeds ContentProvider.
</member>
- <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/system_alert_window" type="bool" setter="" getter="">
Allows an app to create windows using the type WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, shown on top of all other apps. See [url=https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW]SYSTEM_ALERT_WINDOW[/url].
@@ -505,8 +514,7 @@
<member name="permissions/transmit_ir" type="bool" setter="" getter="">
Allows using the device's IR transmitter, if available. See [url=https://developer.android.com/reference/android/Manifest.permission#TRANSMIT_IR]TRANSMIT_IR[/url].
</member>
- <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="">
- Deprecated.
+ <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="" deprecated="">
</member>
<member name="permissions/update_device_stats" type="bool" setter="" getter="">
Allows an application to update device statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#UPDATE_DEVICE_STATS]UPDATE_DEVICE_STATS[/url].
@@ -590,6 +598,7 @@
Application version visible to the user. Falls back to [member ProjectSettings.application/config/version] if left empty.
</member>
<member name="xr_features/xr_mode" type="int" setter="" getter="">
+ The extended reality (XR) mode for this application.
</member>
</members>
</class>
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index ee1ec2790d..6a6d7149ff 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -33,6 +33,7 @@
#include "export_plugin.h"
#include "core/os/os.h"
+#include "editor/editor_paths.h"
#include "editor/editor_settings.h"
#include "editor/export/editor_export.h"
@@ -42,12 +43,15 @@ void register_android_exporter_types() {
void register_android_exporter() {
#ifndef ANDROID_ENABLED
+ EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
- EDITOR_DEF("export/android/debug_keystore", "");
+ EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
- EDITOR_DEF("export/android/debug_keystore_user", "androiddebugkey");
- EDITOR_DEF("export/android/debug_keystore_pass", "android");
+ EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ EDITOR_DEF("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));
EDITOR_DEF("export/android/force_system_user", false);
EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index c3015ec260..3b1a534daf 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -45,9 +45,10 @@
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
-#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
+#include "editor/export/export_template_manager.h"
#include "editor/import/resource_importer_texture_settings.h"
+#include "editor/themes/editor_scale.h"
#include "main/splash.gen.h"
#include "scene/resources/image_texture.h"
@@ -140,6 +141,7 @@ static const char *android_perms[] = {
"MOUNT_UNMOUNT_FILESYSTEMS",
"NFC",
"PERSISTENT_ACTIVITY",
+ "POST_NOTIFICATIONS",
"PROCESS_OUTGOING_CALLS",
"READ_CALENDAR",
"READ_CALL_LOG",
@@ -208,12 +210,14 @@ static const char *android_perms[] = {
nullptr
};
+static const char *MISMATCHED_VERSIONS_MESSAGE = "Android build version mismatch:\n| Template installed: %s\n| Requested version: %s\nPlease reinstall Android build template from 'Project' menu.";
+
static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi/splash.png";
static const char *LEGACY_BUILD_SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi-v4/splash.png";
static const char *SPLASH_BG_COLOR_PATH = "res/drawable-nodpi/splash_bg_color.png";
static const char *LEGACY_BUILD_SPLASH_BG_COLOR_PATH = "res/drawable-nodpi-v4/splash_bg_color.png";
-static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash_drawable.xml";
-static const char *GDEXTENSION_LIBS_PATH = "res://android/build/libs/gdextensionlibs.json";
+static const char *SPLASH_CONFIG_PATH = "res/drawable/splash_drawable.xml";
+static const char *GDEXTENSION_LIBS_PATH = "libs/gdextensionlibs.json";
static const int icon_densities_count = 6;
static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192");
@@ -250,12 +254,12 @@ static const LauncherIcon launcher_adaptive_icon_backgrounds[icon_densities_coun
static const int EXPORT_FORMAT_APK = 0;
static const int EXPORT_FORMAT_AAB = 1;
-static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
-static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
+static const char *APK_ASSETS_DIRECTORY = "assets";
+static const char *AAB_ASSETS_DIRECTORY = "assetPacks/installTime/src/main/assets";
static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
static const int VULKAN_MIN_SDK_VERSION = 24;
-static const int DEFAULT_TARGET_SDK_VERSION = 33; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
+static const int DEFAULT_TARGET_SDK_VERSION = 34; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
#ifndef ANDROID_ENABLED
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
@@ -291,7 +295,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
// Check for devices updates
String adb = get_adb_path();
- if (FileAccess::exists(adb)) {
+ if (ea->has_runnable_preset.is_set() && FileAccess::exists(adb)) {
String devices;
List<String> args;
args.push_back("devices");
@@ -376,14 +380,15 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
} else if (p.begins_with("ro.build.version.sdk=")) {
d.api_level = p.get_slice("=", 1).to_int();
} else if (p.begins_with("ro.product.cpu.abi=")) {
- d.description += "CPU: " + p.get_slice("=", 1).strip_edges() + "\n";
+ d.architecture = p.get_slice("=", 1).strip_edges();
+ d.description += "CPU: " + d.architecture + "\n";
} else if (p.begins_with("ro.product.manufacturer=")) {
d.description += "Manufacturer: " + p.get_slice("=", 1).strip_edges() + "\n";
} else if (p.begins_with("ro.board.platform=")) {
d.description += "Chipset: " + p.get_slice("=", 1).strip_edges() + "\n";
} else if (p.begins_with("ro.opengles.version=")) {
uint32_t opengl = p.get_slice("=", 1).to_int();
- d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl)&0xFF) + "\n";
+ d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n";
}
}
@@ -412,7 +417,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
- if (EDITOR_GET("export/android/shutdown_adb_on_exit")) {
+ if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) {
String adb = get_adb_path();
if (!FileAccess::exists(adb)) {
return; //adb not configured
@@ -423,6 +428,25 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
OS::get_singleton()->execute(adb, args);
}
}
+
+void EditorExportPlatformAndroid::_update_preset_status() {
+ const int preset_count = EditorExport::get_singleton()->get_export_preset_count();
+ bool has_runnable = false;
+
+ for (int i = 0; i < preset_count; i++) {
+ const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
+ if (preset->get_platform() == this && preset->is_runnable()) {
+ has_runnable = true;
+ break;
+ }
+ }
+
+ if (has_runnable) {
+ has_runnable_preset.set();
+ } else {
+ has_runnable_preset.clear();
+ }
+}
#endif
String EditorExportPlatformAndroid::get_project_name(const String &p_name) const {
@@ -460,7 +484,7 @@ String EditorExportPlatformAndroid::get_valid_basename() const {
if (is_digit(c) && first) {
continue;
}
- if (is_ascii_alphanumeric_char(c)) {
+ if (is_ascii_identifier_char(c)) {
name += String::chr(c);
first = false;
}
@@ -474,7 +498,8 @@ String EditorExportPlatformAndroid::get_valid_basename() const {
}
String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const {
- return p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY;
+ String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
+ return gradle_build_directory.path_join(p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY);
}
bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, String *r_error) const {
@@ -537,13 +562,6 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package,
return false;
}
- if (p_package.find("$genname") >= 0 && !is_project_name_valid()) {
- if (r_error) {
- *r_error = TTR("The project name does not meet the requirement for the package name format. Please explicitly specify the package name.");
- }
- return false;
- }
-
return true;
}
@@ -781,11 +799,10 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
}
if (abi_index != -1) {
exported = true;
- String base = "res://android/build/libs";
String type = export_data->debug ? "debug" : "release";
String abi = abis[abi_index].abi;
String filename = p_so.path.get_file();
- String dst_path = base.path_join(type).path_join(abi).path_join(filename);
+ String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename);
Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path);
print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);
Error err = store_file_at_path(dst_path, data);
@@ -812,6 +829,84 @@ bool EditorExportPlatformAndroid::_uses_vulkan() {
return uses_vulkan;
}
+void EditorExportPlatformAndroid::_notification(int p_what) {
+#ifndef ANDROID_ENABLED
+ switch (p_what) {
+ case NOTIFICATION_POSTINITIALIZE: {
+ if (EditorExport::get_singleton()) {
+ EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status));
+ }
+ } break;
+
+ case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+ if (EditorSettings::get_singleton()->check_changed_settings_in_group("export/android")) {
+ _create_editor_debug_keystore_if_needed();
+ }
+ } break;
+ }
+#endif
+}
+
+void EditorExportPlatformAndroid::_create_editor_debug_keystore_if_needed() {
+ // Check if we have a valid keytool path.
+ String keytool_path = get_keytool_path();
+ if (!FileAccess::exists(keytool_path)) {
+ return;
+ }
+
+ // Check if the current editor debug keystore exists.
+ String editor_debug_keystore = EDITOR_GET("export/android/debug_keystore");
+ if (FileAccess::exists(editor_debug_keystore)) {
+ return;
+ }
+
+ // Generate the debug keystore.
+ String keystore_path = EditorPaths::get_singleton()->get_debug_keystore_path();
+ String keystores_dir = keystore_path.get_base_dir();
+ if (!DirAccess::exists(keystores_dir)) {
+ Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ Error err = dir_access->make_dir_recursive(keystores_dir);
+ if (err != OK) {
+ WARN_PRINT(TTR("Error creating keystores directory:") + "\n" + keystores_dir);
+ return;
+ }
+ }
+
+ if (!FileAccess::exists(keystore_path)) {
+ String output;
+ List<String> args;
+ args.push_back("-genkey");
+ args.push_back("-keystore");
+ args.push_back(keystore_path);
+ args.push_back("-storepass");
+ args.push_back("android");
+ args.push_back("-alias");
+ args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ args.push_back("-keypass");
+ args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
+ args.push_back("-keyalg");
+ args.push_back("RSA");
+ args.push_back("-keysize");
+ args.push_back("2048");
+ args.push_back("-validity");
+ args.push_back("10000");
+ args.push_back("-dname");
+ args.push_back("cn=Godot, ou=Godot Engine, o=Stichting Godot, c=NL");
+ Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);
+ print_verbose(output);
+ if (error != OK) {
+ WARN_PRINT("Error: Unable to create debug keystore");
+ return;
+ }
+ }
+
+ // Update the editor settings.
+ EditorSettings::get_singleton()->set("export/android/debug_keystore", keystore_path);
+ EditorSettings::get_singleton()->set("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
+ EditorSettings::get_singleton()->set("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
+ print_verbose("Updated editor debug keystore to " + keystore_path);
+}
+
void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
const char **aperms = android_perms;
while (*aperms) {
@@ -874,7 +969,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug);
manifest_text += "</manifest>\n";
- String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
+ String manifest_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join(vformat("src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")));
print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text);
store_string_at_path(manifest_path, manifest_text);
@@ -1118,7 +1213,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
}
for (int i = 0; i < feature_names.size(); i++) {
- String feature_name = feature_names[i];
+ const String &feature_name = feature_names[i];
bool feature_required = feature_required_list[i];
int feature_version = feature_versions[i];
bool has_version_attribute = feature_version != -1;
@@ -1366,6 +1461,14 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
p_manifest = ret;
}
+String EditorExportPlatformAndroid::_get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug) {
+ String keystore_preference = p_debug ? "keystore/debug" : "keystore/release";
+ String keystore_env_variable = p_debug ? ENV_ANDROID_KEYSTORE_DEBUG_PATH : ENV_ANDROID_KEYSTORE_RELEASE_PATH;
+ String keystore_path = p_preset->get_or_env(keystore_preference, keystore_env_variable);
+
+ return ProjectSettings::get_singleton()->globalize_path(keystore_path).simplify_path();
+}
+
String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) {
uint32_t offset = 0;
uint32_t len = 0;
@@ -1542,18 +1645,32 @@ void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_n
String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) {
bool scale_splash = GLOBAL_GET("application/boot_splash/fullsize");
bool apply_filter = GLOBAL_GET("application/boot_splash/use_filter");
+ bool show_splash_image = GLOBAL_GET("application/boot_splash/show_image");
String project_splash_path = GLOBAL_GET("application/boot_splash/image");
- if (!project_splash_path.is_empty()) {
- splash_image.instantiate();
- print_verbose("Loading splash image: " + project_splash_path);
- const Error err = ImageLoader::load_image(project_splash_path, splash_image);
- if (err) {
- if (OS::get_singleton()->is_stdout_verbose()) {
- print_error("- unable to load splash image from " + project_splash_path + " (" + itos(err) + ")");
+ // Setup the splash bg color.
+ bool bg_color_valid = false;
+ Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid);
+ if (!bg_color_valid) {
+ bg_color = boot_splash_bg_color;
+ }
+
+ if (show_splash_image) {
+ if (!project_splash_path.is_empty()) {
+ splash_image.instantiate();
+ print_verbose("Loading splash image: " + project_splash_path);
+ const Error err = ImageLoader::load_image(project_splash_path, splash_image);
+ if (err) {
+ if (OS::get_singleton()->is_stdout_verbose()) {
+ print_error("- unable to load splash image from " + project_splash_path + " (" + itos(err) + ")");
+ }
+ splash_image.unref();
}
- splash_image.unref();
}
+ } else {
+ splash_image.instantiate();
+ splash_image->initialize_data(1, 1, false, Image::FORMAT_RGBA8);
+ splash_image->set_pixel(0, 0, bg_color);
}
if (splash_image.is_null()) {
@@ -1577,13 +1694,6 @@ String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, R
splash_image->resize(width, height);
}
- // Setup the splash bg color
- bool bg_color_valid;
- Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid);
- if (!bg_color_valid) {
- bg_color = boot_splash_bg_color;
- }
-
print_verbose("Creating splash background color image.");
splash_bg_color_image.instantiate();
splash_bg_color_image->initialize_data(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format());
@@ -1605,7 +1715,11 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &
print_verbose("Loading regular icon from " + path);
if (path.is_empty() || ImageLoader::load_image(path, icon) != OK) {
print_verbose("- falling back to project icon: " + project_icon_path);
- ImageLoader::load_image(project_icon_path, icon);
+ if (!project_icon_path.is_empty()) {
+ ImageLoader::load_image(project_icon_path, icon);
+ } else {
+ ERR_PRINT("No project icon specified. Please specify one in the Project Settings under Application -> Config -> Icon");
+ }
}
// Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default).
@@ -1624,15 +1738,6 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &
}
}
-void EditorExportPlatformAndroid::store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) {
- store_image(launcher_icon.export_path, data);
-}
-
-void EditorExportPlatformAndroid::store_image(const String &export_path, const Vector<uint8_t> &data) {
- String img_path = export_path.insert(0, "res://android/build/");
- store_file_at_path(img_path, data);
-}
-
void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
const String &processed_splash_config_xml,
const Ref<Image> &splash_image,
@@ -1640,26 +1745,30 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
const Ref<Image> &main_image,
const Ref<Image> &foreground,
const Ref<Image> &background) {
+ String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
+
// Store the splash configuration
if (!processed_splash_config_xml.is_empty()) {
print_verbose("Storing processed splash configuration: " + String("\n") + processed_splash_config_xml);
- store_string_at_path(SPLASH_CONFIG_PATH, processed_splash_config_xml);
+ store_string_at_path(gradle_build_dir.path_join(SPLASH_CONFIG_PATH), processed_splash_config_xml);
}
// Store the splash image
if (splash_image.is_valid() && !splash_image->is_empty()) {
- print_verbose("Storing splash image in " + String(SPLASH_IMAGE_EXPORT_PATH));
+ String splash_export_path = gradle_build_dir.path_join(SPLASH_IMAGE_EXPORT_PATH);
+ print_verbose("Storing splash image in " + splash_export_path);
Vector<uint8_t> data;
_load_image_data(splash_image, data);
- store_image(SPLASH_IMAGE_EXPORT_PATH, data);
+ store_file_at_path(splash_export_path, data);
}
// Store the splash bg color image
if (splash_bg_color_image.is_valid() && !splash_bg_color_image->is_empty()) {
- print_verbose("Storing splash background image in " + String(SPLASH_BG_COLOR_PATH));
+ String splash_bg_color_path = gradle_build_dir.path_join(SPLASH_BG_COLOR_PATH);
+ print_verbose("Storing splash background image in " + splash_bg_color_path);
Vector<uint8_t> data;
_load_image_data(splash_bg_color_image, data);
- store_image(SPLASH_BG_COLOR_PATH, data);
+ store_file_at_path(splash_bg_color_path, data);
}
// Prepare images to be resized for the icons. If some image ends up being uninitialized,
@@ -1670,7 +1779,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
print_verbose("Processing launcher icon for dimension " + itos(launcher_icons[i].dimensions) + " into " + launcher_icons[i].export_path);
Vector<uint8_t> data;
_process_launcher_icons(launcher_icons[i].export_path, main_image, launcher_icons[i].dimensions, data);
- store_image(launcher_icons[i], data);
+ store_file_at_path(gradle_build_dir.path_join(launcher_icons[i].export_path), data);
}
if (foreground.is_valid() && !foreground->is_empty()) {
@@ -1678,7 +1787,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
Vector<uint8_t> data;
_process_launcher_icons(launcher_adaptive_icon_foregrounds[i].export_path, foreground,
launcher_adaptive_icon_foregrounds[i].dimensions, data);
- store_image(launcher_adaptive_icon_foregrounds[i], data);
+ store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_foregrounds[i].export_path), data);
}
if (background.is_valid() && !background->is_empty()) {
@@ -1686,7 +1795,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
Vector<uint8_t> data;
_process_launcher_icons(launcher_adaptive_icon_backgrounds[i].export_path, background,
launcher_adaptive_icon_backgrounds[i].dimensions, data);
- store_image(launcher_adaptive_icon_backgrounds[i], data);
+ store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_backgrounds[i].export_path), data);
}
}
}
@@ -1710,7 +1819,6 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres
Vector<ABI> abis = get_enabled_abis(p_preset);
for (int i = 0; i < abis.size(); ++i) {
r_features->push_back(abis[i].arch);
- r_features->push_back(abis[i].abi);
}
}
@@ -1741,6 +1849,11 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) {
return TTR("OpenXR requires \"Use Gradle Build\" to be enabled");
}
+ } else if (p_name == "gradle_build/compress_native_libraries") {
+ bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
+ if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) {
+ return TTR("\"Compress Native Libraries\" is only valid when \"Use Gradle Build\" is enabled.");
+ }
} else if (p_name == "gradle_build/export_format") {
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) {
@@ -1795,7 +1908,10 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, false, true));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, true));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/compress_native_libraries"), false, false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));
// Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).
// This implies doing validation that the string is a proper int.
@@ -1824,10 +1940,10 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
@@ -1873,6 +1989,41 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
}
}
+bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
+ if (p_preset == nullptr) {
+ return true;
+ }
+
+ bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
+ if (p_option == "graphics/opengl_debug" ||
+ p_option == "command_line/extra_args" ||
+ p_option == "permissions/custom_permissions" ||
+ p_option == "gradle_build/compress_native_libraries" ||
+ p_option == "package/retain_data_on_uninstall" ||
+ p_option == "package/exclude_from_recents" ||
+ p_option == "package/show_in_app_library" ||
+ p_option == "package/show_as_launcher_app" ||
+ p_option == "apk_expansion/enable" ||
+ p_option == "apk_expansion/SALT" ||
+ p_option == "apk_expansion/public_key") {
+ return advanced_options_enabled;
+ }
+ if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {
+ return advanced_options_enabled && bool(p_preset->get("gradle_build/use_gradle_build"));
+ }
+ if (p_option == "custom_template/debug" || p_option == "custom_template/release") {
+ // The APK templates are ignored if Gradle build is enabled.
+ return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build"));
+ }
+
+ // Hide .NET embedding option (always enabled).
+ if (p_option == "dotnet/embed_build_outputs") {
+ return false;
+ }
+
+ return true;
+}
+
String EditorExportPlatformAndroid::get_name() const {
return "Android";
}
@@ -1933,6 +2084,12 @@ String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const {
return s;
}
+String EditorExportPlatformAndroid::get_device_architecture(int p_index) const {
+ ERR_FAIL_INDEX_V(p_index, devices.size(), "");
+ MutexLock lock(device_lock);
+ return devices[p_index].architecture;
+}
+
Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
@@ -2112,8 +2269,26 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {
return run_icon;
}
+String EditorExportPlatformAndroid::get_java_path() {
+ String exe_ext;
+ if (OS::get_singleton()->get_name() == "Windows") {
+ exe_ext = ".exe";
+ }
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ return java_sdk_path.path_join("bin/java" + exe_ext);
+}
+
+String EditorExportPlatformAndroid::get_keytool_path() {
+ String exe_ext;
+ if (OS::get_singleton()->get_name() == "Windows") {
+ exe_ext = ".exe";
+ }
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ return java_sdk_path.path_join("bin/keytool" + exe_ext);
+}
+
String EditorExportPlatformAndroid::get_adb_path() {
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".exe";
}
@@ -2125,13 +2300,13 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_
if (p_target_sdk == -1) {
p_target_sdk = DEFAULT_TARGET_SDK_VERSION;
}
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".bat";
}
String apksigner_command_name = "apksigner" + exe_ext;
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
- String apksigner_path = "";
+ String apksigner_path;
Error errn;
String build_tools_dir = sdk_path.path_join("build-tools");
@@ -2230,6 +2405,54 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_
return apksigner_path;
}
+static bool has_valid_keystore_credentials(String &r_error_str, const String &p_keystore, const String &p_username, const String &p_password, const String &p_type) {
+ String output;
+ List<String> args;
+ args.push_back("-list");
+ args.push_back("-keystore");
+ args.push_back(p_keystore);
+ args.push_back("-storepass");
+ args.push_back(p_password);
+ args.push_back("-alias");
+ args.push_back(p_username);
+ Error error = OS::get_singleton()->execute("keytool", args, &output, nullptr, true);
+ String keytool_error = "keytool error:";
+ bool valid = output.substr(0, keytool_error.length()) != keytool_error;
+
+ if (error != OK) {
+ r_error_str = TTR("Error: There was a problem validating the keystore username and password");
+ return false;
+ }
+ if (!valid) {
+ r_error_str = TTR(p_type + " Username and/or Password is invalid for the given " + p_type + " Keystore");
+ return false;
+ }
+ r_error_str = "";
+ return true;
+}
+
+bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) {
+ String dk = _get_keystore_path(p_preset, true);
+ String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
+ String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
+ String rk = _get_keystore_path(p_preset, false);
+ String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
+ String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
+
+ bool valid = true;
+ if (!dk.is_empty() && !dk_user.is_empty() && !dk_password.is_empty()) {
+ String err = "";
+ valid = has_valid_keystore_credentials(err, dk, dk_user, dk_password, "Debug");
+ r_error += err;
+ }
+ if (!rk.is_empty() && !rk_user.is_empty() && !rk_password.is_empty()) {
+ String err = "";
+ valid = has_valid_keystore_credentials(err, rk, rk_user, rk_password, "Release");
+ r_error += err;
+ }
+ return valid;
+}
+
bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
String err;
bool valid = false;
@@ -2285,9 +2508,22 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += template_err;
}
} else {
- bool installed_android_build_template = FileAccess::exists("res://android/build/build.gradle");
+ // Validate the custom gradle android source template.
+ bool android_source_template_valid = false;
+ const String android_source_template = p_preset->get("gradle_build/android_source_template");
+ if (!android_source_template.is_empty()) {
+ android_source_template_valid = FileAccess::exists(android_source_template);
+ if (!android_source_template_valid) {
+ err += TTR("Custom Android source template not found.") + "\n";
+ }
+ }
+
+ // Validate the installed build template.
+ bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle"));
if (!installed_android_build_template) {
- r_missing_templates = !exists_export_template("android_source.zip", &err);
+ if (!android_source_template_valid) {
+ r_missing_templates = !exists_export_template("android_source.zip", &err);
+ }
err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";
} else {
r_missing_templates = false;
@@ -2298,7 +2534,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
// Validate the rest of the export configuration.
- String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ String dk = _get_keystore_path(p_preset, true);
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
@@ -2316,7 +2552,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
}
}
- String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String rk = _get_keystore_path(p_preset, false);
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
@@ -2330,6 +2566,32 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
}
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
+ valid = false;
+ } else {
+ // Validate the given path by checking that `java` is present under the `bin` directory.
+ Error errn;
+ // Check for the bin directory.
+ Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
+ if (errn != OK) {
+ err += TTR("Invalid Java SDK path in Editor Settings.");
+ err += TTR("Missing 'bin' directory!");
+ err += "\n";
+ valid = false;
+ } else {
+ // Check for the `java` command.
+ String java_path = get_java_path();
+ if (!FileAccess::exists(java_path)) {
+ err += TTR("Unable to find 'java' command using the Java SDK path.");
+ err += TTR("Please check the Java SDK directory specified in Editor Settings.");
+ err += "\n";
+ valid = false;
+ }
+ }
+ }
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
if (sdk_path.is_empty()) {
err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
@@ -2406,6 +2668,19 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit
valid = false;
}
+ if (p_preset->get("gradle_build/use_gradle_build")) {
+ String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version");
+ Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ);
+ if (f.is_valid()) {
+ String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
+ String installed_version = f->get_line().strip_edges();
+ if (current_version != installed_version) {
+ err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version);
+ err += "\n";
+ }
+ }
+ }
+
String min_sdk_str = p_preset->get("gradle_build/min_sdk");
int min_sdk_int = VULKAN_MIN_SDK_VERSION;
if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
@@ -2440,6 +2715,13 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit
err += "\n";
}
+ String package_name = p_preset->get("package/unique_name");
+ if (package_name.find("$genname") >= 0 && !is_project_name_valid()) {
+ // Warning only, so don't override `valid`.
+ err += vformat(TTR("The project name does not meet the requirement for the package name format and will be updated to \"%s\". Please explicitly specify the package name if needed."), get_valid_basename());
+ err += "\n";
+ }
+
r_error = err;
return valid;
}
@@ -2527,7 +2809,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
int export_format = int(p_preset->get("gradle_build/export_format"));
String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
- String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String release_keystore = _get_keystore_path(p_preset, false);
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
@@ -2549,7 +2831,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
String password;
String user;
if (p_debug) {
- keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ keystore = _get_keystore_path(p_preset, true);
password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
@@ -2641,34 +2923,37 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
return OK;
}
-void EditorExportPlatformAndroid::_clear_assets_directory() {
+void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) {
Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
// Clear the APK assets directory
- if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) {
+ String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY);
+ if (da_res->dir_exists(apk_assets_directory)) {
print_verbose("Clearing APK assets directory...");
- Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY);
+ Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory);
ERR_FAIL_COND(da_assets.is_null());
da_assets->erase_contents_recursive();
- da_res->remove(APK_ASSETS_DIRECTORY);
+ da_res->remove(apk_assets_directory);
}
// Clear the AAB assets directory
- if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) {
+ String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY);
+ if (da_res->dir_exists(aab_assets_directory)) {
print_verbose("Clearing AAB assets directory...");
- Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY);
+ Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory);
ERR_FAIL_COND(da_assets.is_null());
da_assets->erase_contents_recursive();
- da_res->remove(AAB_ASSETS_DIRECTORY);
+ da_res->remove(aab_assets_directory);
}
}
-void EditorExportPlatformAndroid::_remove_copied_libs() {
+void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) {
print_verbose("Removing previously installed libraries...");
Error error;
- String libs_json = FileAccess::get_file_as_string(GDEXTENSION_LIBS_PATH, &error);
+ String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error);
if (error || libs_json.is_empty()) {
print_verbose("No previously installed libraries found");
return;
@@ -2684,7 +2969,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() {
print_verbose("Removing previously installed library " + libs[i]);
da->remove(libs[i]);
}
- da->remove(GDEXTENSION_LIBS_PATH);
+ da->remove(p_gdextension_libs_path);
}
String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {
@@ -2743,6 +3028,8 @@ String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(
bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {
bool first_build = last_gradle_build_time == 0;
bool have_plugins_changed = false;
+ String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
+ bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir;
String plugin_names = _get_plugins_names(p_preset);
@@ -2762,9 +3049,10 @@ bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExpor
}
last_gradle_build_time = OS::get_singleton()->get_unix_time();
+ last_gradle_build_dir = gradle_build_dir;
last_plugin_names = plugin_names;
- return have_plugins_changed || first_build;
+ return have_plugins_changed || has_build_dir_changed || first_build;
}
Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
@@ -2776,12 +3064,19 @@ Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset>
Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags) {
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
+ const String base_dir = p_path.get_base_dir();
+ if (!DirAccess::exists(base_dir)) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));
+ return ERR_FILE_BAD_PATH;
+ }
+
String src_apk;
Error err;
EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
+ String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";
bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG);
bool apk_expansion = p_preset->get("apk_expansion/enable");
Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
@@ -2830,34 +3125,51 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));
return ERR_UNCONFIGURED;
}
+ String err_string;
+ if (!has_valid_username_and_password(p_preset, err_string)) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string));
+ return ERR_UNCONFIGURED;
+ }
if (use_gradle_build) {
print_verbose("Starting gradle build...");
//test that installed build version is alright
{
print_verbose("Checking build version...");
- Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ);
+ String gradle_base_directory = gradle_build_directory.get_base_dir();
+ Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ);
if (f.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu."));
return ERR_UNCONFIGURED;
}
- String version = f->get_line().strip_edges();
- print_verbose("- build version: " + version);
- if (version != VERSION_FULL_CONFIG) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG));
+ String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
+ String installed_version = f->get_line().strip_edges();
+ print_verbose("- build version: " + installed_version);
+ if (installed_version != current_version) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version));
return ERR_UNCONFIGURED;
}
}
const String assets_directory = get_assets_directory(p_preset, export_format);
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
+ return ERR_UNCONFIGURED;
+ }
+ print_verbose("Java sdk path: " + java_sdk_path);
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
- ERR_FAIL_COND_V_MSG(sdk_path.is_empty(), ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'.");
+ if (sdk_path.is_empty()) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));
+ return ERR_UNCONFIGURED;
+ }
print_verbose("Android sdk path: " + sdk_path);
// TODO: should we use "package/name" or "application/config/name"?
String project_name = get_project_name(p_preset->get("package/name"));
- err = _create_project_name_strings_files(p_preset, project_name); //project name localization.
+ err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory); //project name localization.
if (err != OK) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));
}
// Copies the project icon files into the appropriate Gradle project directory.
_copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background);
@@ -2865,12 +3177,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
_write_tmp_manifest(p_preset, p_give_internet, p_debug);
//stores all the project files inside the Gradle project directory. Also includes all ABIs
- _clear_assets_directory();
- _remove_copied_libs();
+ _clear_assets_directory(p_preset);
+ String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);
+ _remove_copied_libs(gdextension_libs_path);
if (!apk_expansion) {
print_verbose("Exporting project files...");
CustomExportData user_data;
user_data.assets_directory = assets_directory;
+ user_data.libs_directory = gradle_build_directory.path_join("libs");
user_data.debug = p_debug;
if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so);
@@ -2882,7 +3196,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
return err;
}
if (user_data.libs.size() > 0) {
- Ref<FileAccess> fa = FileAccess::open(GDEXTENSION_LIBS_PATH, FileAccess::WRITE);
+ Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
fa->store_string(JSON::stringify(user_data.libs, "\t"));
}
} else {
@@ -2896,8 +3210,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Storing command line flags...");
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
+ print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
+ OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
+
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
- OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required
+ OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
String build_command;
#ifdef WINDOWS_ENABLED
@@ -2906,7 +3223,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
build_command = "gradlew";
#endif
- String build_path = ProjectSettings::get_singleton()->get_resource_path().path_join("android/build");
+ String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory);
build_command = build_path.path_join(build_command);
String package_name = get_package_name(p_preset->get("package/unique_name"));
@@ -2923,6 +3240,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String enabled_abi_string = join_abis(enabled_abis, "|", false);
String sign_flag = should_sign ? "true" : "false";
String zipalign_flag = "true";
+ String compress_native_libraries_flag = bool(p_preset->get("gradle_build/compress_native_libraries")) ? "true" : "false";
Vector<String> android_libraries;
Vector<String> android_dependencies;
@@ -2960,6 +3278,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
List<String> cmdline;
+ cmdline.push_back("validateJavaVersion");
if (clean_build_required) {
cmdline.push_back("clean");
}
@@ -2986,6 +3305,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
cmdline.push_back("-Pplugins_maven_repos=" + combined_android_dependencies_maven_repos); // argument to specify the list of maven repos for android dependencies provided by plugins.
cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned.
cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed.
+ cmdline.push_back("-Pcompress_native_libraries=" + compress_native_libraries_flag); // argument to specify whether the build should compress native libraries.
cmdline.push_back("-Pgodot_editor_version=" + String(VERSION_FULL_CONFIG));
// NOTE: The release keystore is not included in the verbose logging
@@ -2996,7 +3316,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (should_sign) {
if (p_debug) {
- String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
+ String debug_keystore = _get_keystore_path(p_preset, true);
String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
@@ -3018,7 +3338,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.
} else {
// Pass the release keystore info as well
- String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
+ String release_keystore = _get_keystore_path(p_preset, false);
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
if (release_keystore.is_relative_path()) {
@@ -3035,10 +3355,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
- int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline);
+ String build_project_output;
+ int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);
if (result != 0) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);
return ERR_CANT_CREATE;
+ } else {
+ print_verbose(build_project_output);
}
List<String> copy_args;
@@ -3065,10 +3388,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
copy_args.push_back("-Pexport_filename=" + export_filename);
print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
- int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args);
+ String copy_binary_output;
+ int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, &copy_binary_output);
if (copy_result != 0) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output);
return ERR_CANT_CREATE;
+ } else {
+ print_verbose(copy_binary_output);
}
print_verbose("Successfully completed Android gradle build.");
@@ -3094,10 +3420,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
- if (!DirAccess::exists(p_path.get_base_dir())) {
- return ERR_FILE_BAD_PATH;
- }
-
Ref<FileAccess> io_fa;
zlib_filefunc_def io = zipio_create_io(&io_fa);
@@ -3290,10 +3612,6 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
zipClose(unaligned_apk, nullptr);
unzClose(pkg);
- if (err != OK) {
- CLEANUP_AND_RETURN(err);
- }
-
// Let's zip-align (must be done before signing)
static const int ZIP_ALIGNMENT = 4;
@@ -3380,6 +3698,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
// file will invalidate the signature.
err = sign_apk(p_preset, p_debug, p_path, ep);
if (err != OK) {
+ // Message is supplied by the subroutine method.
CLEANUP_AND_RETURN(err);
}
}
@@ -3413,6 +3732,8 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
android_plugins_changed.set();
#endif // DISABLE_DEPRECATED
#ifndef ANDROID_ENABLED
+ _create_editor_debug_keystore_if_needed();
+ _update_preset_status();
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
#endif
}
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index a2d0417c5d..679afdc50f 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -60,6 +60,9 @@ const String ENV_ANDROID_KEYSTORE_RELEASE_PATH = "GODOT_ANDROID_KEYSTORE_RELEASE
const String ENV_ANDROID_KEYSTORE_RELEASE_USER = "GODOT_ANDROID_KEYSTORE_RELEASE_USER";
const String ENV_ANDROID_KEYSTORE_RELEASE_PASS = "GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD";
+const String DEFAULT_ANDROID_KEYSTORE_DEBUG_USER = "androiddebugkey";
+const String DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD = "android";
+
struct LauncherIcon {
const char *export_path;
int dimensions = 0;
@@ -76,6 +79,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
String name;
String description;
int api_level = 0;
+ String architecture;
};
struct APKExportData {
@@ -90,6 +94,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
#endif // DISABLE_DEPRECATED
String last_plugin_names;
uint64_t last_gradle_build_time = 0;
+ String last_gradle_build_dir;
Vector<Device> devices;
SafeFlag devices_changed;
@@ -97,8 +102,10 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
#ifndef ANDROID_ENABLED
Thread check_for_changes_thread;
SafeFlag quit_request;
+ SafeFlag has_runnable_preset;
static void _check_for_changes_poll_thread(void *ud);
+ void _update_preset_status();
#endif
String get_project_name(const String &p_name) const;
@@ -162,6 +169,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet);
+ static String _get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug);
+
static String _parse_string(const uint8_t *p_bytes, bool p_utf8);
void _fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest);
@@ -174,10 +183,6 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background);
- void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data);
-
- void store_image(const String &export_path, const Vector<uint8_t> &data);
-
void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
const String &processed_splash_config_xml,
const Ref<Image> &splash_image,
@@ -186,18 +191,24 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
const Ref<Image> &foreground,
const Ref<Image> &background);
+ static void _create_editor_debug_keystore_if_needed();
+
static Vector<ABI> get_enabled_abis(const Ref<EditorExportPreset> &p_preset);
static bool _uses_vulkan();
+protected:
+ void _notification(int p_what);
+
public:
typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
-public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
virtual void get_export_options(List<ExportOption> *r_options) const override;
+ virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override;
+
virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override;
virtual String get_name() const override;
@@ -218,6 +229,8 @@ public:
virtual String get_option_tooltip(int p_index) const override;
+ virtual String get_device_architecture(int p_index) const override;
+
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override;
virtual Ref<Texture2D> get_run_icon() const override;
@@ -226,8 +239,13 @@ public:
static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false);
+ static String get_java_path();
+
+ static String get_keytool_path();
+
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override;
+ static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error);
virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
@@ -245,9 +263,9 @@ public:
Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep);
- void _clear_assets_directory();
+ void _clear_assets_directory(const Ref<EditorExportPreset> &p_preset);
- void _remove_copied_libs();
+ void _remove_copied_libs(String p_gdextension_libs_path);
static String join_list(const List<String> &p_parts, const String &p_separator);
static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch);
diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp
index 0915009235..9eddef6a4c 100644
--- a/platform/android/export/gradle_export_util.cpp
+++ b/platform/android/export/gradle_export_util.cpp
@@ -191,14 +191,14 @@ String _android_xml_escape(const String &p_string) {
}
// Creates strings.xml files inside the gradle project for different locales.
-Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name) {
+Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir) {
print_verbose("Creating strings resources for supported locales for project " + project_name);
// Stores the string into the default values directory.
String processed_default_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(project_name));
- store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string);
+ store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string);
// Searches the Gradle project res/ directory to find all supported locales
- Ref<DirAccess> da = DirAccess::open("res://android/build/res");
+ Ref<DirAccess> da = DirAccess::open(p_gradle_build_dir.path_join("res"));
if (da.is_null()) {
if (OS::get_singleton()->is_stdout_verbose()) {
print_error("Unable to open Android resources directory.");
@@ -217,7 +217,7 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
continue;
}
String locale = file.replace("values-", "").replace("-r", "_");
- String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml";
+ String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
if (appnames.has(locale)) {
String locale_project_name = appnames[locale];
String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name));
diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h
index 2498394add..9f8e476f73 100644
--- a/platform/android/export/gradle_export_util.h
+++ b/platform/android/export/gradle_export_util.h
@@ -63,6 +63,7 @@ static const int XR_MODE_OPENXR = 1;
struct CustomExportData {
String assets_directory;
+ String libs_directory;
bool debug;
Vector<String> libs;
};
@@ -94,7 +95,7 @@ Error store_string_at_path(const String &p_path, const String &p_data);
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
// Creates strings.xml files inside the gradle project for different locales.
-Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name);
+Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir);
String bool_to_string(bool v);
diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp
index 1249f2219f..ae336d6f9d 100644
--- a/platform/android/file_access_android.cpp
+++ b/platform/android/file_access_android.cpp
@@ -31,8 +31,12 @@
#include "file_access_android.h"
#include "core/string/print_string.h"
+#include "thread_jandroid.h"
+
+#include <android/asset_manager_jni.h>
AAssetManager *FileAccessAndroid::asset_manager = nullptr;
+jobject FileAccessAndroid::j_asset_manager = nullptr;
String FileAccessAndroid::get_path() const {
return path_src;
@@ -121,6 +125,75 @@ uint8_t FileAccessAndroid::get_8() const {
return byte;
}
+uint16_t FileAccessAndroid::get_16() const {
+ if (pos >= len) {
+ eof = true;
+ return 0;
+ }
+
+ uint16_t bytes = 0;
+ int r = AAsset_read(asset, &bytes, 2);
+
+ if (r >= 0) {
+ pos += r;
+ if (pos >= len) {
+ eof = true;
+ }
+ }
+
+ if (big_endian) {
+ bytes = BSWAP16(bytes);
+ }
+
+ return bytes;
+}
+
+uint32_t FileAccessAndroid::get_32() const {
+ if (pos >= len) {
+ eof = true;
+ return 0;
+ }
+
+ uint32_t bytes = 0;
+ int r = AAsset_read(asset, &bytes, 4);
+
+ if (r >= 0) {
+ pos += r;
+ if (pos >= len) {
+ eof = true;
+ }
+ }
+
+ if (big_endian) {
+ bytes = BSWAP32(bytes);
+ }
+
+ return bytes;
+}
+
+uint64_t FileAccessAndroid::get_64() const {
+ if (pos >= len) {
+ eof = true;
+ return 0;
+ }
+
+ uint64_t bytes = 0;
+ int r = AAsset_read(asset, &bytes, 8);
+
+ if (r >= 0) {
+ pos += r;
+ if (pos >= len) {
+ eof = true;
+ }
+ }
+
+ if (big_endian) {
+ bytes = BSWAP64(bytes);
+ }
+
+ return bytes;
+}
+
uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
@@ -151,6 +224,18 @@ void FileAccessAndroid::store_8(uint8_t p_dest) {
ERR_FAIL();
}
+void FileAccessAndroid::store_16(uint16_t p_dest) {
+ ERR_FAIL();
+}
+
+void FileAccessAndroid::store_32(uint32_t p_dest) {
+ ERR_FAIL();
+}
+
+void FileAccessAndroid::store_64(uint64_t p_dest) {
+ ERR_FAIL();
+}
+
bool FileAccessAndroid::file_exists(const String &p_path) {
String path = fix_path(p_path).simplify_path();
if (path.begins_with("/")) {
@@ -176,3 +261,16 @@ void FileAccessAndroid::close() {
FileAccessAndroid::~FileAccessAndroid() {
_close();
}
+
+void FileAccessAndroid::setup(jobject p_asset_manager) {
+ JNIEnv *env = get_jni_env();
+ j_asset_manager = env->NewGlobalRef(p_asset_manager);
+ asset_manager = AAssetManager_fromJava(env, j_asset_manager);
+}
+
+void FileAccessAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(j_asset_manager);
+}
diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h
index 3aa4ca98fc..e79daeafb3 100644
--- a/platform/android/file_access_android.h
+++ b/platform/android/file_access_android.h
@@ -35,9 +35,13 @@
#include <android/asset_manager.h>
#include <android/log.h>
+#include <jni.h>
#include <stdio.h>
class FileAccessAndroid : public FileAccess {
+ static AAssetManager *asset_manager;
+ static jobject j_asset_manager;
+
mutable AAsset *asset = nullptr;
mutable uint64_t len = 0;
mutable uint64_t pos = 0;
@@ -48,8 +52,6 @@ class FileAccessAndroid : public FileAccess {
void _close();
public:
- static AAssetManager *asset_manager;
-
virtual Error open_internal(const String &p_path, int p_mode_flags) override; // open a file
virtual bool is_open() const override; // true when file is open
@@ -65,13 +67,20 @@ public:
virtual bool eof_reached() const override; // reading passed EOF
+ virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; }
virtual uint8_t get_8() const override; // get a byte
+ virtual uint16_t get_16() const override;
+ virtual uint32_t get_32() const override;
+ virtual uint64_t get_64() const override;
virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
virtual Error get_error() const override; // get last error
virtual void flush() override;
virtual void store_8(uint8_t p_dest) override; // store a byte
+ virtual void store_16(uint16_t p_dest) override;
+ virtual void store_32(uint32_t p_dest) override;
+ virtual void store_64(uint64_t p_dest) override;
virtual bool file_exists(const String &p_path) override; // return true if a file exists
@@ -86,6 +95,10 @@ public:
virtual void close() override;
+ static void setup(jobject p_asset_manager);
+
+ static void terminate();
+
~FileAccessAndroid();
};
diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp
index beea73fd61..f28d469d07 100644
--- a/platform/android/file_access_filesystem_jandroid.cpp
+++ b/platform/android/file_access_filesystem_jandroid.cpp
@@ -53,6 +53,7 @@ jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr;
jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_resize = nullptr;
String FileAccessFilesystemJAndroid::get_path() const {
return path_src;
@@ -82,7 +83,7 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo
default:
return ERR_FILE_CANT_OPEN;
- case -1:
+ case -2:
return ERR_FILE_NOT_FOUND;
}
}
@@ -181,6 +182,36 @@ uint8_t FileAccessFilesystemJAndroid::get_8() const {
return byte;
}
+uint16_t FileAccessFilesystemJAndroid::get_16() const {
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ uint16_t bytes = 0;
+ get_buffer(reinterpret_cast<uint8_t *>(&bytes), 2);
+ if (big_endian) {
+ bytes = BSWAP16(bytes);
+ }
+ return bytes;
+}
+
+uint32_t FileAccessFilesystemJAndroid::get_32() const {
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ uint32_t bytes = 0;
+ get_buffer(reinterpret_cast<uint8_t *>(&bytes), 4);
+ if (big_endian) {
+ bytes = BSWAP32(bytes);
+ }
+ return bytes;
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_64() const {
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ uint64_t bytes = 0;
+ get_buffer(reinterpret_cast<uint8_t *>(&bytes), 8);
+ if (big_endian) {
+ bytes = BSWAP64(bytes);
+ }
+ return bytes;
+}
+
String FileAccessFilesystemJAndroid::get_line() const {
ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use.");
@@ -250,6 +281,27 @@ void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) {
store_buffer(&p_dest, 1);
}
+void FileAccessFilesystemJAndroid::store_16(uint16_t p_dest) {
+ if (big_endian) {
+ p_dest = BSWAP16(p_dest);
+ }
+ store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 2);
+}
+
+void FileAccessFilesystemJAndroid::store_32(uint32_t p_dest) {
+ if (big_endian) {
+ p_dest = BSWAP32(p_dest);
+ }
+ store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 4);
+}
+
+void FileAccessFilesystemJAndroid::store_64(uint64_t p_dest) {
+ if (big_endian) {
+ p_dest = BSWAP64(p_dest);
+ }
+ store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 8);
+}
+
void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
if (_file_write) {
ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
@@ -273,6 +325,30 @@ Error FileAccessFilesystemJAndroid::get_error() const {
return OK;
}
+Error FileAccessFilesystemJAndroid::resize(int64_t p_length) {
+ if (_file_resize) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, FAILED);
+ ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use.");
+ int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length);
+ switch (res) {
+ case 0:
+ return OK;
+ case -4:
+ return ERR_INVALID_PARAMETER;
+ case -3:
+ return ERR_FILE_CANT_OPEN;
+ case -2:
+ return ERR_FILE_NOT_FOUND;
+ case -1:
+ default:
+ return FAILED;
+ }
+ } else {
+ return ERR_UNAVAILABLE;
+ }
+}
+
void FileAccessFilesystemJAndroid::flush() {
if (_file_flush) {
JNIEnv *env = get_jni_env();
@@ -332,6 +408,15 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) {
_file_flush = env->GetMethodID(cls, "fileFlush", "(I)V");
_file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z");
_file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J");
+ _file_resize = env->GetMethodID(cls, "fileResize", "(IJ)I");
+}
+
+void FileAccessFilesystemJAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(file_access_handler);
}
void FileAccessFilesystemJAndroid::close() {
diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h
index 0c3f8d7259..6a8fc524b7 100644
--- a/platform/android/file_access_filesystem_jandroid.h
+++ b/platform/android/file_access_filesystem_jandroid.h
@@ -52,6 +52,7 @@ class FileAccessFilesystemJAndroid : public FileAccess {
static jmethodID _file_close;
static jmethodID _file_exists;
static jmethodID _file_last_modified;
+ static jmethodID _file_resize;
int id;
String absolute_path;
@@ -76,7 +77,11 @@ public:
virtual bool eof_reached() const override; ///< reading passed EOF
+ virtual Error resize(int64_t p_length) override;
virtual uint8_t get_8() const override; ///< get a byte
+ virtual uint16_t get_16() const override;
+ virtual uint32_t get_32() const override;
+ virtual uint64_t get_64() const override;
virtual String get_line() const override; ///< get a line
virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
@@ -84,11 +89,15 @@ public:
virtual void flush() override;
virtual void store_8(uint8_t p_dest) override; ///< store a byte
+ virtual void store_16(uint16_t p_dest) override;
+ virtual void store_32(uint32_t p_dest) override;
+ virtual void store_64(uint64_t p_dest) override;
virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override;
virtual bool file_exists(const String &p_path) override; ///< return true if a file exists
static void setup(jobject p_file_access_handler);
+ static void terminate();
virtual uint64_t _get_modified_time(const String &p_file) override;
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml
index 079f629b12..4abc6548bf 100644
--- a/platform/android/java/app/AndroidManifest.xml
+++ b/platform/android/java/app/AndroidManifest.xml
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="com.godot.game"
android:versionCode="1"
android:versionName="1.0"
android:installLocation="auto" >
diff --git a/platform/android/java/app/assetPacks/installTime/build.gradle b/platform/android/java/app/assetPacks/installTime/build.gradle
index b06faac374..46fa046ed4 100644
--- a/platform/android/java/app/assetPacks/installTime/build.gradle
+++ b/platform/android/java/app/assetPacks/installTime/build.gradle
@@ -1,4 +1,6 @@
-apply plugin: 'com.android.asset-pack'
+plugins {
+ id 'com.android.asset-pack'
+}
assetPack {
packName = "installTime" // Directory name for the asset pack
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 01b148aeef..b83ef1471c 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -1,17 +1,4 @@
// Gradle build config for Godot Engine's Android port.
-buildscript {
- apply from: 'config.gradle'
-
- repositories {
- google()
- mavenCentral()
- }
- dependencies {
- classpath libraries.androidGradlePlugin
- classpath libraries.kotlinGradlePlugin
- }
-}
-
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
@@ -23,6 +10,8 @@ allprojects {
repositories {
google()
mavenCentral()
+ gradlePluginPortal()
+ maven { url "https://plugins.gradle.org/m2/" }
// Godot user plugins custom maven repos
String[] mavenRepos = getGodotPluginsMavenRepos()
@@ -42,8 +31,7 @@ configurations {
}
dependencies {
- implementation libraries.kotlinStdLib
- implementation libraries.androidxFragment
+ implementation "androidx.fragment:fragment:$versions.fragmentVersion"
if (rootProject.findProject(":lib")) {
implementation project(":lib")
@@ -88,6 +76,8 @@ android {
assetPacks = [":assetPacks:installTime"]
+ namespace = 'com.godot.game'
+
defaultConfig {
// The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects.
aaptOptions {
@@ -126,6 +116,14 @@ android {
if (shouldNotStrip()) {
doNotStrip '**/*.so'
}
+
+ jniLibs {
+ // Setting this to true causes AGP to package compressed native libraries when building the app
+ // For more background, see:
+ // - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
+ // - https://stackoverflow.com/a/44704840
+ useLegacyPackaging shouldUseLegacyPackaging()
+ }
}
signingConfigs {
@@ -207,37 +205,90 @@ android {
}
task copyAndRenameDebugApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/debug/android_debug.apk"
into getExportPath()
rename "android_debug.apk", getExportFilename()
}
task copyAndRenameDevApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/dev/android_dev.apk"
into getExportPath()
rename "android_dev.apk", getExportFilename()
}
task copyAndRenameReleaseApk(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/apk/release/android_release.apk"
into getExportPath()
rename "android_release.apk", getExportFilename()
}
task copyAndRenameDebugAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/debug/build-debug.aab"
into getExportPath()
rename "build-debug.aab", getExportFilename()
}
task copyAndRenameDevAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/dev/build-dev.aab"
into getExportPath()
rename "build-dev.aab", getExportFilename()
}
task copyAndRenameReleaseAab(type: Copy) {
+ // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
+ // and directories. Otherwise this check may cause permissions access failures on Windows
+ // machines.
+ doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+
from "$buildDir/outputs/bundle/release/build-release.aab"
into getExportPath()
rename "build-release.aab", getExportFilename()
}
+
+/**
+ * Used to validate the version of the Java SDK used for the Godot gradle builds.
+ */
+task validateJavaVersion {
+ if (JavaVersion.current() != versions.javaVersion) {
+ throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the required Java version for Godot gradle builds.")
+ }
+}
+
+/*
+When they're scheduled to run, the copy*AARToAppModule tasks generate dependencies for the 'app'
+module, so we're ensuring the ':app:preBuild' task is set to run after those tasks.
+ */
+if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) {
+ preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule"))
+}
+if (rootProject.tasks.findByPath("copyDevAARToAppModule") != null) {
+ preBuild.mustRunAfter(rootProject.tasks.named("copyDevAARToAppModule"))
+}
+if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) {
+ preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule"))
+}
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index a91e7bc7ce..d27e75b07a 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -1,27 +1,20 @@
ext.versions = [
- androidGradlePlugin: '7.2.1',
- compileSdk : 33,
+ androidGradlePlugin: '8.2.0',
+ compileSdk : 34,
// Also update 'platform/android/export/export_plugin.cpp#OPENGL_MIN_SDK_VERSION'
minSdk : 21,
// Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
- targetSdk : 33,
- buildTools : '33.0.2',
- kotlinVersion : '1.7.0',
- fragmentVersion : '1.3.6',
- nexusPublishVersion: '1.1.0',
- javaVersion : 17,
+ targetSdk : 34,
+ buildTools : '34.0.0',
+ kotlinVersion : '1.9.20',
+ fragmentVersion : '1.6.2',
+ nexusPublishVersion: '1.3.0',
+ javaVersion : JavaVersion.VERSION_17,
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
ndkVersion : '23.2.8568313'
]
-ext.libraries = [
- androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin",
- kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion",
- kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion",
- androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion",
-]
-
ext.getExportPackageName = { ->
// Retrieve the app id from the project property set by the Godot build command.
String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : ""
@@ -201,17 +194,17 @@ final String VALUE_SEPARATOR_REGEX = "\\|"
// get the list of ABIs the project should be exported to
ext.getExportEnabledABIs = { ->
- String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : "";
+ String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""
if (enabledABIs == null || enabledABIs.isEmpty()) {
enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|"
}
- Set<String> exportAbiFilter = [];
+ Set<String> exportAbiFilter = []
for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) {
if (!abi_name.trim().isEmpty()){
- exportAbiFilter.add(abi_name);
+ exportAbiFilter.add(abi_name)
}
}
- return exportAbiFilter;
+ return exportAbiFilter
}
ext.getExportPath = {
@@ -368,3 +361,26 @@ ext.shouldSign = { ->
ext.shouldNotStrip = { ->
return isAndroidStudio() || project.hasProperty("doNotStrip")
}
+
+/**
+ * Whether to use the legacy convention of compressing all .so files in the APK.
+ *
+ * For more background, see:
+ * - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
+ * - https://stackoverflow.com/a/44704840
+ */
+ext.shouldUseLegacyPackaging = { ->
+ int minSdk = getExportMinSdkVersion()
+ if (minSdk < 23) {
+ // Enforce the default behavior for compatibility with device running api < 23
+ return true
+ }
+
+ String legacyPackagingFlag = project.hasProperty("compress_native_libraries") ? project.property("compress_native_libraries") : ""
+ if (legacyPackagingFlag != null && !legacyPackagingFlag.isEmpty()) {
+ return Boolean.parseBoolean(legacyPackagingFlag)
+ }
+
+ // Default behavior for minSdk >= 23
+ return false
+}
diff --git a/platform/android/java/app/gradle.properties b/platform/android/java/app/gradle.properties
index d9f79b6818..2b6468c95e 100644
--- a/platform/android/java/app/gradle.properties
+++ b/platform/android/java/app/gradle.properties
@@ -23,3 +23,6 @@ org.gradle.warning.mode=all
# Enable resource optimizations for release build.
# NOTE: This is turned off for template release build in order to support the build legacy process.
android.enableResourceOptimizations=true
+
+# Fix gradle build errors when the build path contains non-ASCII characters
+android.overridePathCheck=true
diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle
index b4524a3f60..dcac44e393 100644
--- a/platform/android/java/app/settings.gradle
+++ b/platform/android/java/app/settings.gradle
@@ -7,8 +7,10 @@ pluginManagement {
id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
}
repositories {
- gradlePluginPortal()
google()
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url "https://plugins.gradle.org/m2/" }
}
}
diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle
index f94454e2a7..c609b33ef4 100644
--- a/platform/android/java/build.gradle
+++ b/platform/android/java/build.gradle
@@ -1,18 +1,3 @@
-buildscript {
- apply from: 'app/config.gradle'
-
- repositories {
- google()
- mavenCentral()
- maven { url "https://plugins.gradle.org/m2/" }
- }
- dependencies {
- classpath libraries.androidGradlePlugin
- classpath libraries.kotlinGradlePlugin
- classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
- }
-}
-
plugins {
id 'io.github.gradle-nexus.publish-plugin'
}
@@ -31,6 +16,8 @@ allprojects {
repositories {
google()
mavenCentral()
+ gradlePluginPortal()
+ maven { url "https://plugins.gradle.org/m2/" }
}
}
@@ -173,10 +160,21 @@ task zipGradleBuild(type: Zip) {
destinationDirectory = file(binDir)
}
+/**
+ * Returns true if the scons build tasks responsible for generating the Godot native shared
+ * libraries should be excluded.
+ */
+def excludeSconsBuildTasks() {
+ return !isAndroidStudio() && !project.hasProperty("generateNativeLibs")
+}
+
+/**
+ * Generates the list of build tasks that should be excluded from the build process.\
+ */
def templateExcludedBuildTask() {
// We exclude these gradle tasks so we can run the scons command manually.
def excludedTasks = []
- if (!isAndroidStudio()) {
+ if (excludeSconsBuildTasks()) {
logger.lifecycle("Excluding Android studio build tasks")
for (String flavor : supportedFlavors) {
String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor]
@@ -190,23 +188,42 @@ def templateExcludedBuildTask() {
return excludedTasks
}
-def templateBuildTasks() {
+/**
+ * Generates the build tasks for the given flavor
+ * @param flavor Must be one of the supported flavors ('template' / 'editor')
+ */
+def generateBuildTasks(String flavor = "template") {
+ if (!supportedFlavors.contains(flavor)) {
+ throw new GradleException("Invalid build flavor: $flavor")
+ }
+
def tasks = []
- // Only build the apks and aar files for which we have native shared libraries.
- for (String target : supportedFlavorsBuildTypes["template"]) {
- File targetLibs = new File("lib/libs/" + target)
- if (targetLibs != null
+ // Only build the apks and aar files for which we have native shared libraries unless we intend
+ // to run the scons build tasks.
+ boolean excludeSconsBuildTasks = excludeSconsBuildTasks()
+ boolean isTemplate = flavor == "template"
+ String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/"
+ for (String target : supportedFlavorsBuildTypes[flavor]) {
+ File targetLibs = new File(libsDir + target)
+ if (!excludeSconsBuildTasks || (targetLibs != null
&& targetLibs.isDirectory()
&& targetLibs.listFiles() != null
- && targetLibs.listFiles().length > 0) {
+ && targetLibs.listFiles().length > 0)) {
String capitalizedTarget = target.capitalize()
- // Copy the generated aar library files to the build directory.
- tasks += "copy" + capitalizedTarget + "AARToAppModule"
- // Copy the generated aar library files to the bin directory.
- tasks += "copy" + capitalizedTarget + "AARToBin"
- // Copy the prebuilt binary templates to the bin directory.
- tasks += "copy" + capitalizedTarget + "BinaryToBin"
+ if (isTemplate) {
+ // Copy the generated aar library files to the build directory.
+ tasks += "copy${capitalizedTarget}AARToAppModule"
+ // Copy the generated aar library files to the bin directory.
+ tasks += "copy${capitalizedTarget}AARToBin"
+ // Copy the prebuilt binary templates to the bin directory.
+ tasks += "copy${capitalizedTarget}BinaryToBin"
+ } else {
+ // Copy the generated editor apk to the bin directory.
+ tasks += "copyEditor${capitalizedTarget}ApkToBin"
+ // Copy the generated editor aab to the bin directory.
+ tasks += "copyEditor${capitalizedTarget}AabToBin"
+ }
} else {
logger.lifecycle("No native shared libs for target $target. Skipping build.")
}
@@ -265,27 +282,13 @@ task copyEditorDevAabToBin(type: Copy) {
/**
* Generate the Godot Editor Android apk.
*
- * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running
- * this gradle task. The task will only build the apk(s) for which the shared libraries is
- * available.
+ * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
+ * must have been generated (via scons) prior to running this gradle task.
+ * The task will only build the apk(s) for which the shared libraries is available.
*/
task generateGodotEditor {
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
-
- def tasks = []
-
- for (String target : supportedFlavorsBuildTypes["editor"]) {
- File targetLibs = new File("lib/libs/tools/" + target)
- if (targetLibs != null
- && targetLibs.isDirectory()
- && targetLibs.listFiles() != null
- && targetLibs.listFiles().length > 0) {
- tasks += "copyEditor${target.capitalize()}ApkToBin"
- tasks += "copyEditor${target.capitalize()}AabToBin"
- }
- }
-
- dependsOn = tasks
+ dependsOn = generateBuildTasks("editor")
}
/**
@@ -293,7 +296,7 @@ task generateGodotEditor {
*/
task generateGodotTemplates {
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
- dependsOn = templateBuildTasks()
+ dependsOn = generateBuildTasks("template")
finalizedBy 'zipGradleBuild'
}
@@ -303,10 +306,10 @@ task generateGodotTemplates {
*/
task generateDevTemplate {
// add parameter to set symbols to true
- gradle.startParameter.projectProperties += [doNotStrip: true]
+ gradle.startParameter.projectProperties += [doNotStrip: "true"]
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
- dependsOn = templateBuildTasks()
+ dependsOn = generateBuildTasks("template")
finalizedBy 'zipGradleBuild'
}
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 38034aa47c..c5ef086152 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -2,14 +2,14 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
+ id 'base'
}
dependencies {
- implementation libraries.kotlinStdLib
- implementation libraries.androidxFragment
+ implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib")
- implementation "androidx.window:window:1.0.0"
+ implementation "androidx.window:window:1.2.0"
}
ext {
@@ -20,7 +20,7 @@ ext {
String versionStatus = System.getenv("GODOT_VERSION_STATUS")
if (versionStatus != null && !versionStatus.isEmpty()) {
try {
- buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""));
+ buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""))
} catch (NumberFormatException ignored) {
buildNumber = 0
}
@@ -81,6 +81,8 @@ android {
buildToolsVersion versions.buildTools
ndkVersion versions.ndkVersion
+ namespace = "org.godotengine.editor"
+
defaultConfig {
// The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device
applicationId "org.godotengine.editor.v4"
@@ -90,7 +92,10 @@ android {
targetSdkVersion versions.targetSdk
missingDimensionStrategy 'products', 'editor'
- setProperty("archivesBaseName", "android_editor")
+ }
+
+ base {
+ archivesName = "android_editor"
}
compileOptions {
@@ -111,6 +116,10 @@ android {
}
}
+ buildFeatures {
+ buildConfig = true
+ }
+
buildTypes {
dev {
initWith debug
diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml
index cb89d6e1b0..78dcddac0e 100644
--- a/platform/android/java/editor/src/main/AndroidManifest.xml
+++ b/platform/android/java/editor/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="org.godotengine.editor"
android:installLocation="auto">
<supports-screens
@@ -35,7 +34,7 @@
<activity
android:name=".GodotProjectManager"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
- android:launchMode="singleInstance"
+ android:launchMode="singleTask"
android:screenOrientation="userLandscape"
android:exported="true"
android:process=":GodotProjectManager">
@@ -54,7 +53,7 @@
android:name=".GodotEditor"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotEditor"
- android:launchMode="singleInstance"
+ android:launchMode="singleTask"
android:screenOrientation="userLandscape"
android:exported="false">
<layout android:defaultHeight="@dimen/editor_default_window_height"
@@ -66,7 +65,7 @@
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:label="@string/godot_project_name_string"
android:process=":GodotGame"
- android:launchMode="singleInstance"
+ android:launchMode="singleTask"
android:exported="false"
android:screenOrientation="userLandscape">
<layout android:defaultHeight="@dimen/editor_default_window_height"
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 02709d4dc5..c9a62d24b7 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -91,6 +91,10 @@ open class GodotEditor : GodotActivity() {
private val commandLineParams = ArrayList<String>()
override fun onCreate(savedInstanceState: Bundle?) {
+ // We exclude certain permissions from the set we request at startup, as they'll be
+ // requested on demand based on use-cases.
+ PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO))
+
val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
Log.d(TAG, "Received parameters ${params.contentToString()}")
updateCommandLineParams(params)
@@ -123,7 +127,7 @@ open class GodotEditor : GodotActivity() {
*/
protected open fun checkForProjectPermissionsToEnable() {
// Check for RECORD_AUDIO permission
- val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"));
+ val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
if (audioInputEnabled) {
PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this)
}
@@ -221,16 +225,9 @@ open class GodotEditor : GodotActivity() {
val runningProcesses = activityManager.runningAppProcesses
for (runningProcess in runningProcesses) {
if (runningProcess.processName.endsWith(processNameSuffix)) {
- if (targetClass == null) {
- // Killing process directly
- Log.v(TAG, "Killing Godot process ${runningProcess.processName}")
- Process.killProcess(runningProcess.pid)
- } else {
- // Activity is running; sending a request for self termination.
- Log.v(TAG, "Sending force quit request to $targetClass running on process ${runningProcess.processName}")
- val forceQuitIntent = Intent(this, targetClass).putExtra(EXTRA_FORCE_QUIT, true)
- startActivity(forceQuitIntent)
- }
+ // Killing process directly
+ Log.v(TAG, "Killing Godot process ${runningProcess.processName}")
+ Process.killProcess(runningProcess.pid)
return true
}
}
diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties
index 39a0dcda16..c8abb52614 100644
--- a/platform/android/java/gradle.properties
+++ b/platform/android/java/gradle.properties
@@ -26,3 +26,6 @@ org.gradle.warning.mode=all
# Disable resource optimizations for template release build.
# NOTE: This is turned on for Godot Editor's gradle builds in order to improve the release build.
android.enableResourceOptimizations=false
+
+# Fix gradle build errors when the build path contains non-ASCII characters
+android.overridePathCheck=true
diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties
index aa991fceae..471fefaf90 100644
--- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties
+++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Wed Jan 17 12:08:26 PST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml
index f03a1dd47a..8240843876 100644
--- a/platform/android/java/lib/AndroidManifest.xml
+++ b/platform/android/java/lib/AndroidManifest.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="org.godotengine.godot"
android:versionCode="1"
android:versionName="1.0">
diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle
index 4340250ad3..81ab598b90 100644
--- a/platform/android/java/lib/build.gradle
+++ b/platform/android/java/lib/build.gradle
@@ -10,8 +10,9 @@ ext {
apply from: "../scripts/publish-module.gradle"
dependencies {
- implementation libraries.kotlinStdLib
- implementation libraries.androidxFragment
+ implementation "androidx.fragment:fragment:$versions.fragmentVersion"
+
+ testImplementation "junit:junit:4.13.2"
}
def pathToRootDir = "../../../../"
@@ -39,6 +40,11 @@ android {
jvmTarget = versions.javaVersion
}
+ buildFeatures {
+ aidl = true
+ buildConfig = true
+ }
+
buildTypes {
dev {
initWith debug
@@ -70,6 +76,7 @@ android {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
+ test.java.srcDirs = ['srcTest/java']
res.srcDirs = ['res']
aidl.srcDirs = ['aidl']
assets.srcDirs = ['assets']
@@ -97,6 +104,7 @@ android {
}
boolean devBuild = buildType == "dev"
+ boolean debugSymbols = devBuild || isAndroidStudio()
boolean runTests = devBuild
boolean productionBuild = !devBuild
boolean storeRelease = buildType == "release"
@@ -113,7 +121,7 @@ android {
case "dev":
default:
sconsTarget += "_debug"
- break;
+ break
}
}
@@ -164,7 +172,7 @@ android {
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
tasks.create(name: taskName, type: Exec) {
executable sconsExecutableFile.absolutePath
- args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
+ args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "debug_symbols=${debugSymbols}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
}
// Schedule the tasks so the generated libs are present before the aar file is packaged.
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index 38961bcda8..ce53aeebcb 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -37,6 +37,7 @@ import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
+import android.graphics.Color
import android.graphics.Rect
import android.hardware.Sensor
import android.hardware.SensorEvent
@@ -55,6 +56,7 @@ 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.CommandLineFileParser
import org.godotengine.godot.utils.GodotNetUtils
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
@@ -67,7 +69,7 @@ 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.lang.Exception
import java.security.MessageDigest
import java.util.*
@@ -83,6 +85,9 @@ class Godot(private val context: Context) : SensorEventListener {
private val TAG = Godot::class.java.simpleName
}
+ private val windowManager: WindowManager by lazy {
+ requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ }
private val pluginRegistry: GodotPluginRegistry by lazy {
GodotPluginRegistry.getPluginRegistry()
}
@@ -119,6 +124,7 @@ class Godot(private val context: Context) : SensorEventListener {
val directoryAccessHandler = DirectoryAccessHandler(context)
val fileAccessHandler = FileAccessHandler(context)
val netUtils = GodotNetUtils(context)
+ private val commandLineFileParser = CommandLineFileParser()
/**
* Tracks whether [onCreate] was completed successfully.
@@ -149,6 +155,7 @@ class Godot(private val context: Context) : SensorEventListener {
private var useApkExpansion = false
private var useImmersive = false
private var useDebugOpengl = false
+ private var darkMode = false
private var containerLayout: FrameLayout? = null
var renderView: GodotRenderView? = null
@@ -184,7 +191,9 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- beginBenchmarkMeasure("Godot::onCreate")
+ darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+
+ beginBenchmarkMeasure("Startup", "Godot::onCreate")
try {
this.primaryHost = primaryHost
val activity = requireActivity()
@@ -286,7 +295,7 @@ class Godot(private val context: Context) : SensorEventListener {
initializationStarted = false
throw e
} finally {
- endBenchmarkMeasure("Godot::onCreate");
+ endBenchmarkMeasure("Startup", "Godot::onCreate")
}
}
@@ -376,6 +385,8 @@ class Godot(private val context: Context) : SensorEventListener {
ViewGroup.LayoutParams.MATCH_PARENT,
activity.resources.getDimension(R.dimen.text_edit_height).toInt()
)
+ // Prevent GodotEditText from showing on splash screen on devices with Android 14 or newer.
+ editText.setBackgroundColor(Color.TRANSPARENT)
// ...add to FrameLayout
containerLayout?.addView(editText)
renderView = if (usesVulkan()) {
@@ -390,16 +401,19 @@ class Godot(private val context: Context) : SensorEventListener {
}
if (host == primaryHost) {
- renderView!!.startRenderer()
+ renderView?.startRenderer()
}
- val view: View = renderView!!.view
- containerLayout?.addView(
- view,
+
+ renderView?.let {
+ containerLayout?.addView(
+ it.view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
- )
+ )
+ }
+
editText.setView(renderView)
io?.setEdit(editText)
@@ -442,20 +456,23 @@ class Godot(private val context: Context) : SensorEventListener {
})
} else {
// Infer the virtual keyboard height using visible area.
- view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+ renderView?.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)
+ renderView?.let {
+ val surfaceView = it.view
+
+ surfaceView.getWindowVisibleDisplayFrame(visibleSize)
+ val keyboardHeight = surfaceView.height - visibleSize.bottom
+ GodotLib.setVirtualKeyboardHeight(keyboardHeight)
+ }
}
})
}
if (host == primaryHost) {
- renderView!!.queueOnRenderThread {
+ renderView?.queueOnRenderThread {
for (plugin in pluginRegistry.allPlugins) {
plugin.onRegisterPluginWithGodotNative()
}
@@ -484,12 +501,20 @@ class Godot(private val context: Context) : SensorEventListener {
return containerLayout
}
+ fun onStart(host: GodotHost) {
+ if (host != primaryHost) {
+ return
+ }
+
+ renderView?.onActivityStarted()
+ }
+
fun onResume(host: GodotHost) {
if (host != primaryHost) {
return
}
- renderView!!.onActivityResumed()
+ renderView?.onActivityResumed()
if (mAccelerometer != null) {
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
}
@@ -521,13 +546,21 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
- renderView!!.onActivityPaused()
+ renderView?.onActivityPaused()
mSensorManager.unregisterListener(this)
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainPause()
}
}
+ fun onStop(host: GodotHost) {
+ if (host != primaryHost) {
+ return
+ }
+
+ renderView?.onActivityStopped()
+ }
+
fun onDestroy(primaryHost: GodotHost) {
if (this.primaryHost != primaryHost) {
return
@@ -536,8 +569,22 @@ class Godot(private val context: Context) : SensorEventListener {
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainDestroy()
}
- GodotLib.ondestroy()
- forceQuit()
+
+ runOnRenderThread {
+ GodotLib.ondestroy()
+ forceQuit()
+ }
+ }
+
+ /**
+ * Configuration change callback
+ */
+ fun onConfigurationChanged(newConfig: Configuration) {
+ val newDarkMode = newConfig.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+ if (darkMode != newDarkMode) {
+ darkMode = newDarkMode
+ GodotLib.onNightModeChanged()
+ }
}
/**
@@ -577,11 +624,13 @@ class Godot(private val context: Context) : SensorEventListener {
// These properties are defined after Godot setup completion, so we retrieve them here.
val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
+ val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis"))
runOnUiThread {
renderView?.inputHandler?.apply {
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
+ setRotaryInputAxis(rotaryInputAxis)
}
}
@@ -648,9 +697,7 @@ class Godot(private val context: Context) : SensorEventListener {
* This must be called after the render thread has started.
*/
fun runOnRenderThread(action: Runnable) {
- if (renderView != null) {
- renderView!!.queueOnRenderThread(action)
- }
+ renderView?.queueOnRenderThread(action)
}
/**
@@ -712,7 +759,7 @@ class Godot(private val context: Context) : SensorEventListener {
*/
@Keep
private fun isDarkModeSupported(): Boolean {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+ return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED
}
/**
@@ -720,17 +767,14 @@ class Godot(private val context: Context) : SensorEventListener {
*/
@Keep
private fun isDarkMode(): Boolean {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
- }
- return false
+ return darkMode
}
fun hasClipboard(): Boolean {
return mClipboard.hasPrimaryClip()
}
- fun getClipboard(): String? {
+ fun getClipboard(): String {
val clipData = mClipboard.primaryClip ?: return ""
val text = clipData.getItemAt(0).text ?: return ""
return text.toString()
@@ -747,15 +791,14 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun forceQuit(instanceId: Int): Boolean {
- if (primaryHost == null) {
- return false
- }
- return if (instanceId == 0) {
- primaryHost!!.onGodotForceQuit(this)
- true
- } else {
- primaryHost!!.onGodotForceQuit(instanceId)
- }
+ primaryHost?.let {
+ if (instanceId == 0) {
+ it.onGodotForceQuit(this)
+ return true
+ } else {
+ return it.onGodotForceQuit(instanceId)
+ }
+ } ?: return false
}
fun onBackPressed(host: GodotHost) {
@@ -769,20 +812,17 @@ class Godot(private val context: Context) : SensorEventListener {
shouldQuit = false
}
}
- if (shouldQuit && renderView != null) {
- renderView!!.queueOnRenderThread { GodotLib.back() }
+ if (shouldQuit) {
+ renderView?.queueOnRenderThread { GodotLib.back() }
}
}
private fun getRotatedValues(values: FloatArray?): FloatArray? {
if (values == null || values.size != 3) {
- return values
+ return null
}
- val display =
- (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
- val displayRotation = display.rotation
val rotatedValues = FloatArray(3)
- when (displayRotation) {
+ when (windowManager.defaultDisplay.rotation) {
Surface.ROTATION_0 -> {
rotatedValues[0] = values[0]
rotatedValues[1] = values[1]
@@ -811,37 +851,36 @@ class Godot(private val context: Context) : SensorEventListener {
if (renderView == null) {
return
}
+
+ val rotatedValues = getRotatedValues(event.values)
+
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.accelerometer(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.accelerometer(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_GRAVITY -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.gravity(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.gravity(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_MAGNETIC_FIELD -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.magnetometer(
- -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.magnetometer(-it[0], -it[1], -it[2])
+ }
}
}
Sensor.TYPE_GYROSCOPE -> {
- val rotatedValues = getRotatedValues(event.values)
- renderView!!.queueOnRenderThread {
- GodotLib.gyroscope(
- rotatedValues!![0], rotatedValues[1], rotatedValues[2]
- )
+ rotatedValues?.let {
+ renderView?.queueOnRenderThread {
+ GodotLib.gyroscope(it[0], it[1], it[2])
+ }
}
}
}
@@ -873,47 +912,18 @@ class Godot(private val context: Context) : SensorEventListener {
}
private fun getCommandLine(): MutableList<String> {
- val original: MutableList<String> = parseCommandLine()
+ val commandLine = try {
+ commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
+ } catch (ignored: Exception) {
+ mutableListOf()
+ }
+
val hostCommandLine = primaryHost?.commandLine
if (!hostCommandLine.isNullOrEmpty()) {
- original.addAll(hostCommandLine)
+ commandLine.addAll(hostCommandLine)
}
- return original
- }
- private fun parseCommandLine(): MutableList<String> {
- val inputStream: InputStream
- return try {
- inputStream = requireActivity().assets.open("_cl_")
- val len = ByteArray(4)
- var r = inputStream.read(len)
- if (r < 4) {
- return mutableListOf()
- }
- val argc =
- (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
- val cmdline = ArrayList<String>(argc)
- for (i in 0 until argc) {
- r = inputStream.read(len)
- if (r < 4) {
- return mutableListOf()
- }
- val strlen =
- (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
- if (strlen > 65535) {
- return mutableListOf()
- }
- val arg = ByteArray(strlen)
- r = inputStream.read(arg)
- if (r == strlen) {
- cmdline.add(String(arg, StandardCharsets.UTF_8))
- }
- }
- cmdline
- } catch (e: Exception) {
- // The _cl_ file can be missing with no adverse effect
- mutableListOf()
- }
+ return commandLine
}
/**
@@ -1004,7 +1014,7 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun initInputDevices() {
- renderView!!.initInputDevices()
+ renderView?.initInputDevices()
}
@Keep
@@ -1013,13 +1023,13 @@ class Godot(private val context: Context) : SensorEventListener {
}
@Keep
- private fun nativeBeginBenchmarkMeasure(label: String) {
- beginBenchmarkMeasure(label)
+ private fun nativeBeginBenchmarkMeasure(scope: String, label: String) {
+ beginBenchmarkMeasure(scope, label)
}
@Keep
- private fun nativeEndBenchmarkMeasure(label: String) {
- endBenchmarkMeasure(label)
+ private fun nativeEndBenchmarkMeasure(scope: String, label: String) {
+ endBenchmarkMeasure(scope, label)
}
@Keep
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
index a60f6e997e..7b8fad8952 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
@@ -30,7 +30,6 @@
package org.godotengine.godot
-import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
@@ -65,10 +64,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
private set
override fun onCreate(savedInstanceState: Bundle?) {
- // We exclude certain permissions from the set we request at startup, as they'll be
- // requested on demand based on use-cases.
- PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO))
-
super.onCreate(savedInstanceState)
setContentView(R.layout.godot_app_layout)
@@ -88,8 +83,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
override fun onDestroy() {
Log.v(TAG, "Destroying Godot app...")
super.onDestroy()
- if (godotFragment != null) {
- terminateGodotInstance(godotFragment!!.godot)
+
+ godotFragment?.let {
+ terminateGodotInstance(it.godot)
}
}
@@ -98,22 +94,26 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
}
private fun terminateGodotInstance(instance: Godot) {
- if (godotFragment != null && instance === godotFragment!!.godot) {
- Log.v(TAG, "Force quitting Godot instance")
- ProcessPhoenix.forceQuit(this)
+ godotFragment?.let {
+ if (instance === it.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)
+ godotFragment?.let {
+ if (instance === it.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)
+ }
}
}
}
@@ -156,7 +156,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
godotFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) {
+ // Logging the result of permission requests
+ if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE || requestCode == PermissionsUtil.REQUEST_SINGLE_PERMISSION_REQ_CODE) {
Log.d(TAG, "Received permissions request result..")
for (i in permissions.indices) {
val permissionGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
index 120e1722e5..a323045e1b 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
@@ -38,6 +38,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Messenger;
@@ -147,6 +148,13 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
@CallSuper
@Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ godot.onConfigurationChanged(newConfig);
+ }
+
+ @CallSuper
+ @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCallback != null) {
@@ -172,7 +180,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
@Override
public void onCreate(Bundle icicle) {
- BenchmarkUtils.beginBenchmarkMeasure("GodotFragment::onCreate");
+ BenchmarkUtils.beginBenchmarkMeasure("Startup", "GodotFragment::onCreate");
super.onCreate(icicle);
final Activity activity = getActivity();
@@ -180,7 +188,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
godot = new Godot(requireContext());
performEngineInitialization();
- BenchmarkUtils.endBenchmarkMeasure("GodotFragment::onCreate");
+ BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
}
private void performEngineInitialization() {
@@ -271,6 +279,32 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
}
@Override
+ public void onStop() {
+ super.onStop();
+ if (!godot.isInitialized()) {
+ if (null != mDownloaderClientStub) {
+ mDownloaderClientStub.disconnect(getActivity());
+ }
+ return;
+ }
+
+ godot.onStop(this);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (!godot.isInitialized()) {
+ if (null != mDownloaderClientStub) {
+ mDownloaderClientStub.connect(getActivity());
+ }
+ return;
+ }
+
+ godot.onStart(this);
+ }
+
+ @Override
public void onResume() {
super.onResume();
if (!godot.isInitialized()) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
index 52350c12a6..81043ce782 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
@@ -114,12 +114,30 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
@Override
public void onActivityPaused() {
- onPause();
+ queueEvent(() -> {
+ GodotLib.focusout();
+ // Pause the renderer
+ godotRenderer.onActivityPaused();
+ });
+ }
+
+ @Override
+ public void onActivityStopped() {
+ pauseGLThread();
}
@Override
public void onActivityResumed() {
- onResume();
+ queueEvent(() -> {
+ // Resume the renderer
+ godotRenderer.onActivityResumed();
+ GodotLib.focusin();
+ });
+ }
+
+ @Override
+ public void onActivityStarted() {
+ resumeGLThread();
}
@Override
@@ -283,26 +301,4 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
/* Set the renderer responsible for frame rendering */
setRenderer(godotRenderer);
}
-
- @Override
- public void onResume() {
- super.onResume();
-
- queueEvent(() -> {
- // Resume the renderer
- godotRenderer.onActivityResumed();
- GodotLib.focusin();
- });
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- queueEvent(() -> {
- GodotLib.focusout();
- // Pause the renderer
- godotRenderer.onActivityPaused();
- });
- }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
index edcd9c4d1f..4b51bd778d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
@@ -178,12 +178,10 @@ public class GodotIO {
}
public int[] getDisplaySafeArea() {
- DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
- Display display = activity.getWindowManager().getDefaultDisplay();
- Point size = new Point();
- display.getRealSize(size);
+ Rect rect = new Rect();
+ activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
- int[] result = { 0, 0, size.x, size.y };
+ int[] result = { rect.left, rect.top, rect.right, rect.bottom };
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
DisplayCutout cutout = insets.getDisplayCutout();
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
index fee50e93c2..d0c3d4a687 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -220,6 +220,11 @@ public class GodotLib {
public static native void requestPermissionResult(String p_permission, boolean p_result);
/**
+ * Invoked on the theme light/dark mode change.
+ */
+ public static native void onNightModeChanged();
+
+ /**
* Invoked on the GL thread to configure the height of the virtual keyboard.
*/
public static native void setVirtualKeyboardHeight(int p_height);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
index ebf3a6b2fb..5b2f9f57c7 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
@@ -47,8 +47,13 @@ public interface GodotRenderView {
void queueOnRenderThread(Runnable event);
void onActivityPaused();
+
+ void onActivityStopped();
+
void onActivityResumed();
+ void onActivityStarted();
+
void onBackPressed();
GodotInputHandler getInputHandler();
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
index 48708152be..a1ee9bd6b4 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
@@ -92,12 +92,30 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
@Override
public void onActivityPaused() {
- onPause();
+ queueOnVkThread(() -> {
+ GodotLib.focusout();
+ // Pause the renderer
+ mRenderer.onVkPause();
+ });
+ }
+
+ @Override
+ public void onActivityStopped() {
+ pauseRenderThread();
+ }
+
+ @Override
+ public void onActivityStarted() {
+ resumeRenderThread();
}
@Override
public void onActivityResumed() {
- onResume();
+ queueOnVkThread(() -> {
+ // Resume the renderer
+ mRenderer.onVkResume();
+ GodotLib.focusin();
+ });
}
@Override
@@ -211,26 +229,4 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
}
return super.onResolvePointerIcon(me, pointerIndex);
}
-
- @Override
- public void onResume() {
- super.onResume();
-
- queueOnVkThread(() -> {
- // Resume the renderer
- mRenderer.onVkResume();
- GodotLib.focusin();
- });
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- queueOnVkThread(() -> {
- GodotLib.focusout();
- // Pause the renderer
- mRenderer.onVkPause();
- });
- }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
index 56397bb2c2..bd8c58ad69 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
@@ -122,8 +122,8 @@ import javax.microedition.khronos.opengles.GL10;
* <p>
* <h3>Activity Life-cycle</h3>
* A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients
- * are required to call {@link #onPause()} when the activity stops and
- * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to
+ * are required to call {@link #pauseGLThread()} when the activity stops and
+ * {@link #resumeGLThread()} when the activity starts. These calls allow GLSurfaceView to
* pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate
* the OpenGL display.
* <p>
@@ -339,8 +339,8 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
* setRenderer is called:
* <ul>
* <li>{@link #getRenderMode()}
- * <li>{@link #onPause()}
- * <li>{@link #onResume()}
+ * <li>{@link #pauseGLThread()}
+ * <li>{@link #resumeGLThread()}
* <li>{@link #queueEvent(Runnable)}
* <li>{@link #requestRender()}
* <li>{@link #setRenderMode(int)}
@@ -568,6 +568,7 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
}
+ // -- GODOT start --
/**
* Pause the rendering thread, optionally tearing down the EGL context
* depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}.
@@ -578,22 +579,23 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
*
* Must not be called before a renderer has been set.
*/
- public void onPause() {
+ protected final void pauseGLThread() {
mGLThread.onPause();
}
/**
* Resumes the rendering thread, re-creating the OpenGL context if necessary. It
- * is the counterpart to {@link #onPause()}.
+ * is the counterpart to {@link #pauseGLThread()}.
*
* This method should typically be called in
* {@link android.app.Activity#onStart Activity.onStart}.
*
* Must not be called before a renderer has been set.
*/
- public void onResume() {
+ protected final void resumeGLThread() {
mGLThread.onResume();
}
+ // -- GODOT end --
/**
* Queue a runnable to be run on the GL rendering thread. This can be used
@@ -1671,7 +1673,24 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
mWantRenderNotification = true;
mRequestRender = true;
mRenderComplete = false;
- mFinishDrawingRunnable = finishDrawing;
+
+ // fix lost old callback when continuous call requestRenderAndNotify
+ //
+ // If continuous call requestRenderAndNotify before trigger old
+ // callback, old callback will lose, cause VRI will wait for SV's
+ // draw to finish forever not calling finishDraw.
+ // https://android.googlesource.com/platform/frameworks/base/+/044fce0b826f2da3a192aac56785b5089143e693%5E%21/
+ //+++++++++++++++++++++++++++++++++++++++++++++++++++
+ final Runnable oldCallback = mFinishDrawingRunnable;
+ mFinishDrawingRunnable = () -> {
+ if (oldCallback != null) {
+ oldCallback.run();
+ }
+ if (finishDrawing != null) {
+ finishDrawing.run();
+ }
+ };
+ //----------------------------------------------------
sGLThreadManager.notifyAll();
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
index 3070a8a207..dc8a0e54bb 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
@@ -34,10 +34,13 @@ import org.godotengine.godot.*;
import android.content.Context;
import android.content.res.Configuration;
+import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.text.InputFilter;
import android.text.InputType;
+import android.text.TextUtils;
+import android.text.method.DigitsKeyListener;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
@@ -45,6 +48,7 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import java.lang.ref.WeakReference;
+import java.util.Locale;
public class GodotEditText extends EditText {
// ===========================================================
@@ -137,6 +141,7 @@ public class GodotEditText extends EditText {
}
int inputType = InputType.TYPE_CLASS_TEXT;
+ String acceptCharacters = null;
switch (edit.getKeyboardType()) {
case KEYBOARD_TYPE_DEFAULT:
inputType = InputType.TYPE_CLASS_TEXT;
@@ -148,7 +153,8 @@ public class GodotEditText extends EditText {
inputType = InputType.TYPE_CLASS_NUMBER;
break;
case KEYBOARD_TYPE_NUMBER_DECIMAL:
- inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED;
+ inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ acceptCharacters = "0123456789,.- ";
break;
case KEYBOARD_TYPE_PHONE:
inputType = InputType.TYPE_CLASS_PHONE;
@@ -165,6 +171,14 @@ public class GodotEditText extends EditText {
}
edit.setInputType(inputType);
+ if (!TextUtils.isEmpty(acceptCharacters)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ edit.setKeyListener(DigitsKeyListener.getInstance(Locale.getDefault()));
+ } else {
+ edit.setKeyListener(DigitsKeyListener.getInstance(acceptCharacters));
+ }
+ }
+
edit.mInputWrapper.setOriginText(text);
edit.addTextChangedListener(edit.mInputWrapper);
final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
index e26c9d39c2..89fbb9f580 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
@@ -210,21 +210,23 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
}
override fun onScroll(
- originEvent: MotionEvent,
+ originEvent: MotionEvent?,
terminusEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (scaleInProgress) {
if (dragInProgress) {
- // Cancel the drag
- GodotInputHandler.handleMotionEvent(
- originEvent.source,
- MotionEvent.ACTION_CANCEL,
- originEvent.buttonState,
- originEvent.x,
- originEvent.y
- )
+ if (originEvent != null) {
+ // Cancel the drag
+ GodotInputHandler.handleMotionEvent(
+ originEvent.source,
+ MotionEvent.ACTION_CANCEL,
+ originEvent.buttonState,
+ originEvent.x,
+ originEvent.y
+ )
+ }
dragInProgress = false
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
index 38c115ad7f..fe971cf442 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
@@ -57,6 +57,9 @@ import java.util.Set;
public class GodotInputHandler implements InputManager.InputDeviceListener {
private static final String TAG = GodotInputHandler.class.getSimpleName();
+ private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
+ private static final int ROTARY_INPUT_HORIZONTAL_AXIS = 0;
+
private final SparseIntArray mJoystickIds = new SparseIntArray(4);
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
@@ -71,6 +74,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
*/
private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
+
public GodotInputHandler(GodotRenderView godotView) {
final Context context = godotView.getView().getContext();
mRenderView = godotView;
@@ -102,6 +107,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
this.godotGestureHandler.setPanningAndScalingEnabled(enable);
}
+ /**
+ * On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default.
+ */
+ public void setRotaryInputAxis(int axis) {
+ rotaryInputAxis = axis;
+ }
+
private boolean isKeyEventGameDevice(int source) {
// Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD)
if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD))
@@ -484,8 +496,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
final float tiltX = (float)-Math.sin(orientation) * tiltMult;
final float tiltY = (float)Math.cos(orientation) * tiltMult;
- final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
- final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ float verticalFactor = 0;
+ float horizontalFactor = 0;
+
+ // If event came from RotaryEncoder (Bezel or Crown rotate event on Wear OS smart watches),
+ // convert it to mouse wheel event.
+ if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+ if (rotaryInputAxis == ROTARY_INPUT_HORIZONTAL_AXIS) {
+ horizontalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ } else {
+ // If rotaryInputAxis is not ROTARY_INPUT_HORIZONTAL_AXIS then use default ROTARY_INPUT_VERTICAL_AXIS axis.
+ verticalFactor = -event.getAxisValue(MotionEvent.AXIS_SCROLL);
+ }
+ } else {
+ verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ }
boolean sourceMouseRelative = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE);
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
index 0f447f0b05..11cf7b3566 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
@@ -36,7 +36,9 @@ import android.util.Log
import org.godotengine.godot.io.StorageScope
import java.io.IOException
import java.nio.ByteBuffer
+import java.nio.channels.ClosedChannelException
import java.nio.channels.FileChannel
+import java.nio.channels.NonWritableChannelException
import kotlin.math.max
/**
@@ -135,6 +137,21 @@ internal abstract class DataAccess(private val filePath: String) {
seek(positionFromBeginning)
}
+ fun resize(length: Long): Int {
+ return try {
+ fileChannel.truncate(length)
+ FileErrors.OK.nativeValue
+ } catch (e: NonWritableChannelException) {
+ FileErrors.FILE_CANT_OPEN.nativeValue
+ } catch (e: ClosedChannelException) {
+ FileErrors.FILE_CANT_OPEN.nativeValue
+ } catch (e: IllegalArgumentException) {
+ FileErrors.INVALID_PARAMETER.nativeValue
+ } catch (e: IOException) {
+ FileErrors.FAILED.nativeValue
+ }
+ }
+
fun position(): Long {
return try {
fileChannel.position()
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
index 984bf607d0..1d773467e8 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -45,7 +45,6 @@ class FileAccessHandler(val context: Context) {
companion object {
private val TAG = FileAccessHandler::class.java.simpleName
- private const val FILE_NOT_FOUND_ERROR_ID = -1
internal const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
@@ -56,7 +55,9 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.fileExists(storageScope, context, path!!)
+ path?.let {
+ DataAccess.fileExists(storageScope, context, it)
+ } ?: false
} catch (e: SecurityException) {
false
}
@@ -69,20 +70,22 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.removeFile(storageScope, context, path!!)
+ path?.let {
+ DataAccess.removeFile(storageScope, context, it)
+ } ?: false
} catch (e: Exception) {
false
}
}
- internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean {
+ internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean {
val storageScope = storageScopeIdentifier.identifyStorageScope(from)
if (storageScope == StorageScope.UNKNOWN) {
return false
}
return try {
- DataAccess.renameFile(storageScope, context, from!!, to!!)
+ DataAccess.renameFile(storageScope, context, from, to)
} catch (e: Exception) {
false
}
@@ -106,16 +109,18 @@ class FileAccessHandler(val context: Context) {
return INVALID_FILE_ID
}
- try {
- val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
+ return try {
+ path?.let {
+ val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID
- files.put(++lastFileId, dataAccess)
- return lastFileId
+ files.put(++lastFileId, dataAccess)
+ lastFileId
+ } ?: INVALID_FILE_ID
} catch (e: FileNotFoundException) {
- return FILE_NOT_FOUND_ERROR_ID
+ FileErrors.FILE_NOT_FOUND.nativeValue
} catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e)
- return INVALID_FILE_ID
+ INVALID_FILE_ID
}
}
@@ -176,12 +181,22 @@ class FileAccessHandler(val context: Context) {
}
return try {
- DataAccess.fileLastModified(storageScope, context, filepath!!)
+ filepath?.let {
+ DataAccess.fileLastModified(storageScope, context, it)
+ } ?: 0L
} catch (e: SecurityException) {
0L
}
}
+ fun fileResize(fileId: Int, length: Long): Int {
+ if (!hasFileId(fileId)) {
+ return FileErrors.FAILED.nativeValue
+ }
+
+ return files[fileId].resize(length)
+ }
+
fun fileGetPosition(fileId: Int): Long {
if (!hasFileId(fileId)) {
return 0L
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
new file mode 100644
index 0000000000..2df0195de7
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
@@ -0,0 +1,53 @@
+/**************************************************************************/
+/* FileErrors.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.file
+
+/**
+ * Set of errors that may occur when performing data access.
+ */
+internal enum class FileErrors(val nativeValue: Int) {
+ OK(0),
+ FAILED(-1),
+ FILE_NOT_FOUND(-2),
+ FILE_CANT_OPEN(-3),
+ INVALID_PARAMETER(-4);
+
+ companion object {
+ fun fromNativeError(error: Int): FileErrors? {
+ for (fileError in entries) {
+ if (fileError.nativeValue == error) {
+ return fileError
+ }
+ }
+ return null
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
index 1552c8f082..69748c0a8d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
@@ -41,7 +41,7 @@ import org.godotengine.godot.io.file.FileAccessFlags
import org.godotengine.godot.io.file.FileAccessHandler
import org.json.JSONObject
import java.nio.ByteBuffer
-import java.util.concurrent.ConcurrentSkipListMap
+import java.util.Collections
/**
* Contains benchmark related utilities methods
@@ -51,44 +51,47 @@ private const val TAG = "GodotBenchmark"
var useBenchmark = false
var benchmarkFile = ""
-private val startBenchmarkFrom = ConcurrentSkipListMap<String, Long>()
-private val benchmarkTracker = ConcurrentSkipListMap<String, Double>()
+private val startBenchmarkFrom = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Long>())
+private val benchmarkTracker = Collections.synchronizedMap(LinkedHashMap<Pair<String, String>, Double>())
/**
- * Start measuring and tracing the execution of a given section of code using the given label.
+ * Start measuring and tracing the execution of a given section of code using the given label
+ * within the given scope.
*
* Must be followed by a call to [endBenchmarkMeasure].
*
* Note: Only enabled on 'editorDev' build variant.
*/
-fun beginBenchmarkMeasure(label: String) {
+fun beginBenchmarkMeasure(scope: String, label: String) {
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
return
}
- startBenchmarkFrom[label] = SystemClock.elapsedRealtime()
+ val key = Pair(scope, label)
+ startBenchmarkFrom[key] = SystemClock.elapsedRealtime()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- Trace.beginAsyncSection(label, 0)
+ Trace.beginAsyncSection("[$scope] $label", 0)
}
}
/**
- * End measuring and tracing of the section of code with the given label.
+ * End measuring and tracing of the section of code with the given label within the given scope.
*
* Must be preceded by a call [beginBenchmarkMeasure]
*
* * Note: Only enabled on 'editorDev' build variant.
*/
-fun endBenchmarkMeasure(label: String) {
+fun endBenchmarkMeasure(scope: String, label: String) {
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
return
}
- val startTime = startBenchmarkFrom[label] ?: return
+ val key = Pair(scope, label)
+ val startTime = startBenchmarkFrom[key] ?: return
val total = SystemClock.elapsedRealtime() - startTime
- benchmarkTracker[label] = total / 1000.0
+ benchmarkTracker[key] = total / 1_000.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- Trace.endAsyncSection(label, 0)
+ Trace.endAsyncSection("[$scope] $label", 0)
}
}
@@ -107,8 +110,16 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = ben
return
}
+ val results = LinkedHashMap<String, String>()
+ for (entry in benchmarkTracker) {
+ if (!results.containsKey(entry.key.first)) {
+ results[entry.key.first] = ""
+ }
+ results[entry.key.first] += "\t\t- ${entry.key.second}: ${entry.value * 1_000.0} msec.\n"
+ }
+
val printOut =
- benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n")
+ results.map { "\t- [${it.key}]\n ${it.value}" }.joinToString("\n")
Log.i(TAG, "BENCHMARK:\n$printOut")
if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt
new file mode 100644
index 0000000000..ce5c5b6714
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt
@@ -0,0 +1,83 @@
+/**************************************************************************/
+/* CommandLineFileParser.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.utils
+
+import java.io.InputStream
+import java.nio.charset.StandardCharsets
+import java.util.ArrayList
+
+/**
+ * A class that parses the content of file storing command line params. Usually, this file is saved
+ * in `assets/_cl_` on exporting an apk
+ *
+ * Returns a mutable list of command lines
+ */
+internal class CommandLineFileParser {
+ fun parseCommandLine(inputStream: InputStream): MutableList<String> {
+ return try {
+ val headerBytes = ByteArray(4)
+ var argBytes = inputStream.read(headerBytes)
+ if (argBytes < 4) {
+ return mutableListOf()
+ }
+ val argc = decodeHeaderIntValue(headerBytes)
+
+ val cmdline = ArrayList<String>(argc)
+ for (i in 0 until argc) {
+ argBytes = inputStream.read(headerBytes)
+ if (argBytes < 4) {
+ return mutableListOf()
+ }
+ val strlen = decodeHeaderIntValue(headerBytes)
+
+ if (strlen > 65535) {
+ return mutableListOf()
+ }
+
+ val arg = ByteArray(strlen)
+ argBytes = inputStream.read(arg)
+ if (argBytes == strlen) {
+ cmdline.add(String(arg, StandardCharsets.UTF_8))
+ }
+ }
+ cmdline
+ } catch (e: Exception) {
+ // The _cl_ file can be missing with no adverse effect
+ mutableListOf()
+ }
+ }
+
+ private fun decodeHeaderIntValue(headerBytes: ByteArray): Int =
+ (headerBytes[3].toInt() and 0xFF) shl 24 or
+ ((headerBytes[2].toInt() and 0xFF) shl 16) or
+ ((headerBytes[1].toInt() and 0xFF) shl 8) or
+ (headerBytes[0].toInt() and 0xFF)
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
index 9a82204467..9df890e6bd 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
@@ -41,12 +41,16 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
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;
@@ -56,9 +60,9 @@ import java.util.Set;
public final class PermissionsUtil {
private static final String TAG = PermissionsUtil.class.getSimpleName();
- static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
- static final int REQUEST_CAMERA_PERMISSION = 2;
- static final int REQUEST_VIBRATE_PERMISSION = 3;
+ public static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
+ public static final int REQUEST_CAMERA_PERMISSION = 2;
+ public static final int REQUEST_VIBRATE_PERMISSION = 3;
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
@@ -67,12 +71,74 @@ public final class PermissionsUtil {
}
/**
- * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a>
- * @param permissionName the name of the requested permission.
+ * Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest
+ * @param permissions list of the permissions to request.
* @param activity the caller activity for this method.
- * @return true/false. "true" if permission was granted otherwise returns "false".
+ * @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched.
+ */
+ public static boolean requestPermissions(Activity activity, List<String> permissions) {
+ if (activity == null) {
+ return false;
+ }
+
+ if (permissions == null || permissions.isEmpty()) {
+ return true;
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Not necessary, asked on install already
+ return true;
+ }
+
+ Set<String> requestedPermissions = new HashSet<>();
+ for (String permission : permissions) {
+ try {
+ if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ Log.d(TAG, "Requesting permission " + permission);
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ } catch (Exception ignored) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ }
+ }
+ } 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) {
+ Log.d(TAG, "Requesting permission " + permission);
+ requestedPermissions.add(permission);
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Skip this permission and continue.
+ Log.w(TAG, "Unable to identify permission " + permission, e);
+ }
+ }
+
+ if (requestedPermissions.isEmpty()) {
+ // If list is empty, all of dangerous permissions were granted.
+ return true;
+ }
+
+ activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
+ return true;
+ }
+
+ /**
+ * Request a dangerous permission. The requested permission must be included in the app's AndroidManifest
+ * @param permissionName the name of the permission to request.
+ * @param activity the caller activity for this method.
+ * @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched.
*/
public static boolean requestPermission(String permissionName, Activity activity) {
+ if (activity == null || TextUtils.isEmpty(permissionName)) {
+ return false;
+ }
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Not necessary, asked on install already
return true;
@@ -124,7 +190,7 @@ public final class PermissionsUtil {
/**
* Request dangerous permissions which are defined in the Android manifest file from the user.
* @param activity the caller activity for this method.
- * @return true/false. "true" if all permissions were granted otherwise returns "false".
+ * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
*/
public static boolean requestManifestPermissions(Activity activity) {
return requestManifestPermissions(activity, null);
@@ -134,14 +200,18 @@ public final class PermissionsUtil {
* Request dangerous permissions which are defined in the Android manifest file from the user.
* @param activity the caller activity for this method.
* @param excludes Set of permissions to exclude from the request
- * @return true/false. "true" if all permissions were granted otherwise returns "false".
+ * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched.
*/
public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) {
+ if (activity == null) {
+ return false;
+ }
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
- String[] manifestPermissions;
+ List<String> manifestPermissions;
try {
manifestPermissions = getManifestPermissions(activity);
} catch (PackageManager.NameNotFoundException e) {
@@ -149,48 +219,17 @@ public final class PermissionsUtil {
return false;
}
- if (manifestPermissions.length == 0)
+ if (manifestPermissions.isEmpty()) {
return true;
-
- List<String> requestedPermissions = new ArrayList<>();
- for (String manifestPermission : manifestPermissions) {
- if (excludes != null && excludes.contains(manifestPermission)) {
- continue;
- }
- try {
- if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
- Log.d(TAG, "Requesting permission " + manifestPermission);
- try {
- Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
- intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
- activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
- } catch (Exception ignored) {
- Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
- activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
- }
- }
- } else {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
- Log.d(TAG, "Requesting permission " + manifestPermission);
- requestedPermissions.add(manifestPermission);
- }
- }
- } catch (PackageManager.NameNotFoundException e) {
- // Skip this permission and continue.
- Log.w(TAG, "Unable to identify permission " + manifestPermission, e);
- }
}
- if (requestedPermissions.isEmpty()) {
- // If list is empty, all of dangerous permissions were granted.
- return true;
+ if (excludes != null && !excludes.isEmpty()) {
+ for (String excludedPermission : excludes) {
+ manifestPermissions.remove(excludedPermission);
+ }
}
- activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
- return false;
+ return requestPermissions(activity, manifestPermissions);
}
/**
@@ -199,15 +238,16 @@ public final class PermissionsUtil {
* @return granted permissions list
*/
public static String[] getGrantedPermissions(Context context) {
- String[] manifestPermissions;
+ List<String> manifestPermissions;
try {
manifestPermissions = getManifestPermissions(context);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return new String[0];
}
- if (manifestPermissions.length == 0)
- return manifestPermissions;
+ if (manifestPermissions.isEmpty()) {
+ return new String[0];
+ }
List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
@@ -235,7 +275,7 @@ public final class PermissionsUtil {
/**
* Check if the given permission is in the AndroidManifest.xml file.
* @param context the caller context for this method.
- * @param permission the permession to look for in the manifest file.
+ * @param permission the permission to look for in the manifest file.
* @return "true" if the permission is in the manifest file of the activity, "false" otherwise.
*/
public static boolean hasManifestPermission(Context context, String permission) {
@@ -253,15 +293,15 @@ public final class PermissionsUtil {
/**
* Returns the permissions defined in the AndroidManifest.xml file.
* @param context the caller context for this method.
- * @return manifest permissions list
+ * @return mutable copy of 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(Context context) throws PackageManager.NameNotFoundException {
+ public static ArrayList<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;
+ return new ArrayList<String>();
+ return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions));
}
/**
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
index 3828004198..791b425444 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
@@ -99,7 +99,7 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf
*
* Must not be called before a [VkRenderer] has been set.
*/
- open fun onResume() {
+ protected fun resumeRenderThread() {
vkThread.onResume()
}
@@ -108,7 +108,7 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf
*
* Must not be called before a [VkRenderer] has been set.
*/
- open fun onPause() {
+ protected fun pauseRenderThread() {
vkThread.onPause()
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
index 4aba0c370d..8c0065b31e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
@@ -142,7 +142,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
fun onSurfaceChanged(width: Int, height: Int) {
lock.withLock {
hasSurface = true
- surfaceChanged = true;
+ surfaceChanged = true
this.width = width
this.height = height
@@ -179,7 +179,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
// blocking the thread lifecycle by holding onto the lock.
if (eventQueue.isNotEmpty()) {
event = eventQueue.removeAt(0)
- break;
+ break
}
if (readyToDraw) {
@@ -199,7 +199,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
}
// Break out of the loop so drawing can occur without holding onto the lock.
- break;
+ break
} else if (rendererResumed) {
// If we aren't ready to draw but are resumed, that means we either lost a surface
// or the app was paused.
diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
index 01ee41e30b..1f0d8592b3 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java
@@ -66,11 +66,15 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {
GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);
EGLContext context;
+ int[] debug_attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
+ int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE };
if (mUseDebugOpengl) {
- int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
- context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+ context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, debug_attrib_list);
+ if (context == null || context == EGL10.EGL_NO_CONTEXT) {
+ Log.w(TAG, "creating 'OpenGL Debug' context failed");
+ context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
+ }
} else {
- int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE };
context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
}
GLUtils.checkEglError(TAG, "After eglCreateContext", egl);
diff --git a/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt
new file mode 100644
index 0000000000..8b0466848a
--- /dev/null
+++ b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt
@@ -0,0 +1,104 @@
+/**************************************************************************/
+/* CommandLineFileParserTest.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.utils
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+
+// Godot saves command line params in the `assets/_cl_` file on exporting an apk. By default,
+// without any other commands specified in `command_line/extra_args` in Export window, the content
+// of that _cl_ file consists of only the `--xr_mode_regular` and `--use_immersive` flags.
+// The `CL_` prefix here refers to that file
+private val CL_DEFAULT_NO_EXTRA_ARGS = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_ONE_EXTRA_ARG = byteArrayOf(3, 0, 0, 0, 15, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_TWO_EXTRA_ARGS = byteArrayOf(4, 0, 0, 0, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 49, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 50, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_EMPTY = byteArrayOf()
+private val CL_HEADER_TOO_SHORT = byteArrayOf(0, 0, 0)
+private val CL_INCOMPLETE_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0)
+private val CL_LENGTH_TOO_LONG_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG = byteArrayOf(2, 0, 0, 0, 10, 0, 0, 0, 45, 45, 120, 114)
+private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
+
+@RunWith(Parameterized::class)
+class CommandLineFileParserTest(
+ private val inputStreamArg: InputStream,
+ private val expectedResult: List<String>,
+) {
+
+ private val commandLineFileParser = CommandLineFileParser()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data() = listOf(
+ arrayOf(ByteArrayInputStream(CL_EMPTY), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_HEADER_TOO_SHORT), listOf<String>()),
+
+ arrayOf(ByteArrayInputStream(CL_DEFAULT_NO_EXTRA_ARGS), listOf(
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_ONE_EXTRA_ARG), listOf(
+ "--unit_test_arg",
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_TWO_EXTRA_ARGS), listOf(
+ "--unit_test_arg1",
+ "--unit_test_arg2",
+ "--xr_mode_regular",
+ "--use_immersive",
+ )),
+
+ arrayOf(ByteArrayInputStream(CL_INCOMPLETE_FIRST_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_LENGTH_TOO_LONG_IN_FIRST_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG), listOf<String>()),
+ arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG), listOf<String>()),
+ )
+ }
+
+ @Test
+ fun `Given inputStream, When parsing command line, Then a correct list is returned`() {
+ // given
+ val inputStream = inputStreamArg
+
+ // when
+ val result = commandLineFileParser.parseCommandLine(inputStream)
+
+ // then
+ assert(result == expectedResult) { "Expected: $expectedResult Actual: $result" }
+ }
+}
diff --git a/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml
index dc180375d5..8072ee00db 100644
--- a/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml
+++ b/platform/android/java/nativeSrcsConfigs/AndroidManifest.xml
@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest package="org.godotengine.godot" />
+<manifest />
diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle
index 5e810ae1ba..a728241181 100644
--- a/platform/android/java/nativeSrcsConfigs/build.gradle
+++ b/platform/android/java/nativeSrcsConfigs/build.gradle
@@ -9,6 +9,8 @@ android {
buildToolsVersion versions.buildTools
ndkVersion versions.ndkVersion
+ namespace = "org.godotengine.godot"
+
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle
index 466ffebf22..3137e74244 100644
--- a/platform/android/java/settings.gradle
+++ b/platform/android/java/settings.gradle
@@ -5,12 +5,15 @@ pluginManagement {
plugins {
id 'com.android.application' version versions.androidGradlePlugin
id 'com.android.library' version versions.androidGradlePlugin
+ id 'com.android.asset-pack' version versions.androidGradlePlugin
id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
id 'io.github.gradle-nexus.publish-plugin' version versions.nexusPublishVersion
}
repositories {
- gradlePluginPortal()
google()
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url "https://plugins.gradle.org/m2/" }
}
}
diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp
index d6455cbf1c..a309a6ab74 100644
--- a/platform/android/java_class_wrapper.cpp
+++ b/platform/android/java_class_wrapper.cpp
@@ -1157,50 +1157,54 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
- jclass activity = env->FindClass("android/app/Activity");
- jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;");
- classLoader = env->CallObjectMethod(p_activity, getClassLoader);
- classLoader = (jclass)env->NewGlobalRef(classLoader);
- jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
- findClass = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
-
jclass bclass = env->FindClass("java/lang/Class");
getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;");
Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/reflect/Method");
getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;");
getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;");
getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/reflect/Field");
Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Boolean");
Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Byte");
Byte_byteValue = env->GetMethodID(bclass, "byteValue", "()B");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Character");
Character_characterValue = env->GetMethodID(bclass, "charValue", "()C");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Short");
Short_shortValue = env->GetMethodID(bclass, "shortValue", "()S");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Integer");
Integer_integerValue = env->GetMethodID(bclass, "intValue", "()I");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Long");
Long_longValue = env->GetMethodID(bclass, "longValue", "()J");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Float");
Float_floatValue = env->GetMethodID(bclass, "floatValue", "()F");
+ env->DeleteLocalRef(bclass);
bclass = env->FindClass("java/lang/Double");
Double_doubleValue = env->GetMethodID(bclass, "doubleValue", "()D");
+ env->DeleteLocalRef(bclass);
}
diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp
index 10716a5c79..49913b9c30 100644
--- a/platform/android/java_godot_io_wrapper.cpp
+++ b/platform/android/java_godot_io_wrapper.cpp
@@ -70,7 +70,11 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc
}
GodotIOJavaWrapper::~GodotIOJavaWrapper() {
- // nothing to do here for now
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(godot_io_instance);
}
jobject GodotIOJavaWrapper::get_instance() {
@@ -82,7 +86,9 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE);
jstring jStr = env->NewStringUTF(p_uri.utf8().get_data());
- return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK;
+ Error result = env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK;
+ env->DeleteLocalRef(jStr);
+ return result;
} else {
return ERR_UNAVAILABLE;
}
@@ -220,6 +226,7 @@ void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_type, int p_max
ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_existing.utf8().get_data());
env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_type, p_max_input_length, p_cursor_start, p_cursor_end);
+ env->DeleteLocalRef(jStr);
}
}
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 50075ed3f5..6cab7e74fd 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -95,6 +95,13 @@ static void _terminate(JNIEnv *env, bool p_restart = false) {
if (godot_io_java) {
delete godot_io_java;
}
+
+ TTS_Android::terminate();
+ FileAccessAndroid::terminate();
+ DirAccessJAndroid::terminate();
+ FileAccessFilesystemJAndroid::terminate();
+ NetSocketAndroid::terminate();
+
if (godot_java) {
if (!restart_on_cleanup) {
if (p_restart) {
@@ -125,10 +132,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv
init_thread_jandroid(jvm, env);
- jobject amgr = env->NewGlobalRef(p_asset_manager);
-
- FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr);
-
+ FileAccessAndroid::setup(p_asset_manager);
DirAccessJAndroid::setup(p_directory_access_handler);
FileAccessFilesystemJAndroid::setup(p_file_access_handler);
NetSocketAndroid::setup(p_net_utils);
@@ -250,7 +254,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env,
}
if (step.get() == 1) {
- if (!Main::start()) {
+ if (Main::start() != EXIT_SUCCESS) {
return true; // should exit instead and print the error
}
@@ -484,7 +488,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *
env->DeleteLocalRef(jobj);
}
- MessageQueue::get_singleton()->push_callp(obj, str_method, argptrs, count);
+ Callable(obj, str_method).call_deferredp(argptrs, count);
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
+ DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
+ if (ds) {
+ ds->emit_system_theme_changed();
+ }
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) {
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index 1ddda6c1c8..f32ffc291a 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -66,6 +66,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
}
diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp
index a95f762e01..04424c1179 100644
--- a/platform/android/java_godot_view_wrapper.cpp
+++ b/platform/android/java_godot_view_wrapper.cpp
@@ -95,6 +95,7 @@ void GodotJavaViewWrapper::configure_pointer_icon(int pointer_type, const String
jstring jImagePath = env->NewStringUTF(image_path.utf8().get_data());
env->CallVoidMethod(_godot_view, _configure_pointer_icon, pointer_type, jImagePath, p_hotspot.x, p_hotspot.y);
+ env->DeleteLocalRef(jImagePath);
}
}
diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h
index e5b04e4866..5f554aa2d6 100644
--- a/platform/android/java_godot_view_wrapper.h
+++ b/platform/android/java_godot_view_wrapper.h
@@ -38,7 +38,7 @@
#include <android/log.h>
#include <jni.h>
-// Class that makes functions in java/src/org/godotengine/godot/GodotView.java callable from C++
+// Class that makes functions in java/src/org/godotengine/godot/GodotRenderView.java callable from C++
class GodotJavaViewWrapper {
private:
jclass _cls;
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index cb6ebf14a8..61be6fc5db 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -78,8 +78,8 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
- _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V");
- _end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V");
+ _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V");
+ _end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V");
_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
_get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;");
_has_feature = p_env->GetMethodID(godot_class, "hasFeature", "(Ljava/lang/String;)Z");
@@ -172,6 +172,8 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data());
jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data());
env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle);
+ env->DeleteLocalRef(jStrMessage);
+ env->DeleteLocalRef(jStrTitle);
}
}
@@ -231,6 +233,7 @@ void GodotJavaWrapper::set_clipboard(const String &p_text) {
ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_text.utf8().get_data());
env->CallVoidMethod(godot_instance, _set_clipboard, jStr);
+ env->DeleteLocalRef(jStr);
}
}
@@ -253,7 +256,9 @@ bool GodotJavaWrapper::request_permission(const String &p_name) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
jstring jStrName = env->NewStringUTF(p_name.utf8().get_data());
- return env->CallBooleanMethod(godot_instance, _request_permission, jStrName);
+ bool result = env->CallBooleanMethod(godot_instance, _request_permission, jStrName);
+ env->DeleteLocalRef(jStrName);
+ return result;
} else {
return false;
}
@@ -334,13 +339,15 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) {
}
}
-int GodotJavaWrapper::create_new_godot_instance(List<String> args) {
+int GodotJavaWrapper::create_new_godot_instance(const List<String> &args) {
if (_create_new_godot_instance) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, 0);
jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
for (int i = 0; i < args.size(); i++) {
- env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
+ jstring j_arg = env->NewStringUTF(args[i].utf8().get_data());
+ env->SetObjectArrayElement(jargs, i, j_arg);
+ env->DeleteLocalRef(j_arg);
}
return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs);
} else {
@@ -348,21 +355,27 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> args) {
}
}
-void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) {
+void GodotJavaWrapper::begin_benchmark_measure(const String &p_context, const String &p_label) {
if (_begin_benchmark_measure) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
+ jstring j_context = env->NewStringUTF(p_context.utf8().get_data());
jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
- env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label);
+ env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_context, j_label);
+ env->DeleteLocalRef(j_context);
+ env->DeleteLocalRef(j_label);
}
}
-void GodotJavaWrapper::end_benchmark_measure(const String &p_label) {
+void GodotJavaWrapper::end_benchmark_measure(const String &p_context, const String &p_label) {
if (_end_benchmark_measure) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
+ jstring j_context = env->NewStringUTF(p_context.utf8().get_data());
jstring j_label = env->NewStringUTF(p_label.utf8().get_data());
- env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label);
+ env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_context, j_label);
+ env->DeleteLocalRef(j_context);
+ env->DeleteLocalRef(j_label);
}
}
@@ -372,6 +385,7 @@ void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) {
ERR_FAIL_NULL(env);
jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data());
env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file);
+ env->DeleteLocalRef(j_benchmark_file);
}
}
@@ -381,7 +395,9 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const {
ERR_FAIL_NULL_V(env, false);
jstring j_feature = env->NewStringUTF(p_feature.utf8().get_data());
- return env->CallBooleanMethod(godot_instance, _has_feature, j_feature);
+ bool result = env->CallBooleanMethod(godot_instance, _has_feature, j_feature);
+ env->DeleteLocalRef(j_feature);
+ return result;
} else {
return false;
}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 7c6327c9e1..93998021a9 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -39,7 +39,7 @@
#include <android/log.h>
#include <jni.h>
-// Class that makes functions in java/src/org/godotengine/godot/Godot.java callable from C++
+// Class that makes functions in java/src/org/godotengine/godot/Godot.kt callable from C++
class GodotJavaWrapper {
private:
jobject godot_instance;
@@ -104,9 +104,9 @@ public:
void init_input_devices();
void vibrate(int p_duration_ms);
String get_input_fallback_mapping();
- int create_new_godot_instance(List<String> args);
- void begin_benchmark_measure(const String &p_label);
- void end_benchmark_measure(const String &p_label);
+ int create_new_godot_instance(const List<String> &args);
+ void begin_benchmark_measure(const String &p_context, const String &p_label);
+ void end_benchmark_measure(const String &p_context, const String &p_label);
void dump_benchmark(const String &benchmark_file);
// Return the list of gdextensions config file.
diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp
index 1299ad032a..8f0ee51fac 100644
--- a/platform/android/net_socket_android.cpp
+++ b/platform/android/net_socket_android.cpp
@@ -49,6 +49,14 @@ void NetSocketAndroid::setup(jobject p_net_utils) {
_multicast_lock_release = env->GetMethodID(cls, "multicastLockRelease", "()V");
}
+void NetSocketAndroid::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(net_utils);
+}
+
void NetSocketAndroid::multicast_lock_acquire() {
if (_multicast_lock_acquire) {
JNIEnv *env = get_jni_env();
@@ -106,7 +114,7 @@ Error NetSocketAndroid::set_broadcasting_enabled(bool p_enabled) {
return OK;
}
-Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, String p_if_name) {
+Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) {
Error err = NetSocketPosix::join_multicast_group(p_multi_address, p_if_name);
if (err != OK) {
return err;
@@ -120,7 +128,7 @@ Error NetSocketAndroid::join_multicast_group(const IPAddress &p_multi_address, S
return OK;
}
-Error NetSocketAndroid::leave_multicast_group(const IPAddress &p_multi_address, String p_if_name) {
+Error NetSocketAndroid::leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) {
Error err = NetSocketPosix::leave_multicast_group(p_multi_address, p_if_name);
if (err != OK) {
return err;
diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h
index f0c2b70529..26cb2d4e3d 100644
--- a/platform/android/net_socket_android.h
+++ b/platform/android/net_socket_android.h
@@ -63,12 +63,13 @@ protected:
public:
static void make_default();
static void setup(jobject p_net_utils);
+ static void terminate();
virtual void close();
virtual Error set_broadcasting_enabled(bool p_enabled);
- virtual Error join_multicast_group(const IPAddress &p_multi_address, String p_if_name);
- virtual Error leave_multicast_group(const IPAddress &p_multi_address, String p_if_name);
+ virtual Error join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name);
+ virtual Error leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name);
NetSocketAndroid() {}
~NetSocketAndroid();
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 92dc5f909f..463a307854 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -162,7 +162,39 @@ Vector<String> OS_Android::get_granted_permissions() const {
return godot_java->get_granted_permissions();
}
-Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
+bool OS_Android::copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path) {
+ if (!FileAccess::exists(p_library_path)) {
+ return false;
+ }
+
+ Ref<DirAccess> da_ref = DirAccess::create_for_path(p_library_path);
+ if (!da_ref.is_valid()) {
+ return false;
+ }
+
+ String copy_path = p_target_dir.path_join(p_library_path.get_file());
+ bool copy_exists = FileAccess::exists(copy_path);
+ if (copy_exists) {
+ print_verbose("Deleting existing library copy " + copy_path);
+ if (da_ref->remove(copy_path) != OK) {
+ print_verbose("Unable to delete " + copy_path);
+ }
+ }
+
+ print_verbose("Copying " + p_library_path + " to " + p_target_dir);
+ Error create_dir_result = da_ref->make_dir_recursive(p_target_dir);
+ if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) {
+ copy_exists = da_ref->copy(p_library_path, copy_path) == OK;
+ }
+
+ if (copy_exists && r_copy_path != nullptr) {
+ *r_copy_path = copy_path;
+ }
+
+ return copy_exists;
+}
+
+Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) {
String path = p_path;
bool so_file_exists = true;
if (!FileAccess::exists(path)) {
@@ -172,24 +204,32 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han
p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
if (!p_library_handle && so_file_exists) {
- // The library may be on the sdcard and thus inaccessible. Try to copy it to the internal
- // directory.
- uint64_t so_modified_time = FileAccess::get_modified_time(p_path);
- String dynamic_library_path = get_dynamic_libraries_path().path_join(String::num_uint64(so_modified_time));
- String internal_path = dynamic_library_path.path_join(p_path.get_file());
-
- bool internal_so_file_exists = FileAccess::exists(internal_path);
- if (!internal_so_file_exists) {
- Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path);
- if (da_ref.is_valid()) {
- Error create_dir_result = da_ref->make_dir_recursive(dynamic_library_path);
- if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) {
- internal_so_file_exists = da_ref->copy(path, internal_path) == OK;
+ // The library (and its dependencies) may be on the sdcard and thus inaccessible.
+ // Try to copy to the internal directory for access.
+ const String dynamic_library_path = get_dynamic_libraries_path();
+
+ if (p_data != nullptr && p_data->library_dependencies != nullptr && !p_data->library_dependencies->is_empty()) {
+ // Copy the library dependencies
+ print_verbose("Copying library dependencies..");
+ for (const String &library_dependency_path : *p_data->library_dependencies) {
+ String internal_library_dependency_path;
+ if (!copy_dynamic_library(library_dependency_path, dynamic_library_path.path_join(library_dependency_path.get_base_dir()), &internal_library_dependency_path)) {
+ ERR_PRINT(vformat("Unable to copy library dependency %s", library_dependency_path));
+ } else {
+ void *lib_dependency_handle = dlopen(internal_library_dependency_path.utf8().get_data(), RTLD_NOW);
+ if (!lib_dependency_handle) {
+ ERR_PRINT(vformat("Can't open dynamic library dependency: %s. Error: %s.", internal_library_dependency_path, dlerror()));
+ }
}
}
}
+ String internal_path;
+ print_verbose("Copying library " + p_path);
+ const bool internal_so_file_exists = copy_dynamic_library(p_path, dynamic_library_path.path_join(p_path.get_base_dir()), &internal_path);
+
if (internal_so_file_exists) {
+ print_verbose("Opening library " + internal_path);
p_library_handle = dlopen(internal_path.utf8().get_data(), RTLD_NOW);
if (p_library_handle) {
path = internal_path;
@@ -199,8 +239,8 @@ Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_han
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;
+ if (p_data != nullptr && p_data->r_resolved_path != nullptr) {
+ *p_data->r_resolved_path = path;
}
return OK;
@@ -324,15 +364,21 @@ void OS_Android::main_loop_end() {
void OS_Android::main_loop_focusout() {
DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
+ if (OS::get_singleton()->get_main_loop()) {
+ OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT);
+ }
audio_driver_android.set_pause(true);
}
void OS_Android::main_loop_focusin() {
DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_IN);
+ if (OS::get_singleton()->get_main_loop()) {
+ OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN);
+ }
audio_driver_android.set_pause(false);
}
-Error OS_Android::shell_open(String p_uri) {
+Error OS_Android::shell_open(const String &p_uri) {
return godot_io_java->open_uri(p_uri);
}
@@ -708,15 +754,15 @@ String OS_Android::get_config_path() const {
return get_user_data_dir().path_join("config");
}
-void OS_Android::benchmark_begin_measure(const String &p_what) {
+void OS_Android::benchmark_begin_measure(const String &p_context, const String &p_what) {
#ifdef TOOLS_ENABLED
- godot_java->begin_benchmark_measure(p_what);
+ godot_java->begin_benchmark_measure(p_context, p_what);
#endif
}
-void OS_Android::benchmark_end_measure(const String &p_what) {
+void OS_Android::benchmark_end_measure(const String &p_context, const String &p_what) {
#ifdef TOOLS_ENABLED
- godot_java->end_benchmark_measure(p_what);
+ godot_java->end_benchmark_measure(p_context, p_what);
#endif
}
@@ -730,6 +776,10 @@ void OS_Android::benchmark_dump() {
}
bool OS_Android::_check_internal_feature_support(const String &p_feature) {
+ if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") {
+ return false;
+ }
+
if (p_feature == "system_fonts") {
return true;
}
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index f88f3e0518..7bdbeef77a 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -113,7 +113,7 @@ public:
virtual void alert(const String &p_alert, const String &p_title) override;
- virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override;
+ virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override;
virtual String get_name() const override;
virtual String get_distribution_name() const override;
@@ -134,7 +134,7 @@ public:
void set_native_window(ANativeWindow *p_native_window);
ANativeWindow *get_native_window() const;
- virtual Error shell_open(String p_uri) override;
+ virtual Error shell_open(const String &p_uri) override;
virtual Vector<String> get_system_fonts() const override;
virtual String get_system_font_path(const String &p_font_name, int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override;
@@ -165,8 +165,8 @@ public:
virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override;
- virtual void benchmark_begin_measure(const String &p_what) override;
- virtual void benchmark_end_measure(const String &p_what) override;
+ virtual void benchmark_begin_measure(const String &p_context, const String &p_what) override;
+ virtual void benchmark_end_measure(const String &p_context, const String &p_what) override;
virtual void benchmark_dump() override;
virtual void load_platform_gdextensions() const override;
@@ -178,6 +178,8 @@ public:
private:
// Location where we relocate external dynamic libraries to make them accessible.
String get_dynamic_libraries_path() const;
+ // Copy a dynamic library to the given location to make it accessible for loading.
+ bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr);
};
#endif // OS_ANDROID_H
diff --git a/platform/android/vulkan_context_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp
index 01e6d14438..9232126b04 100644
--- a/platform/android/vulkan_context_android.cpp
+++ b/platform/android/rendering_context_driver_vulkan_android.cpp
@@ -1,5 +1,5 @@
/**************************************************************************/
-/* vulkan_context_android.cpp */
+/* rendering_context_driver_vulkan_android.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,7 +28,7 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-#include "vulkan_context_android.h"
+#include "rendering_context_driver_vulkan_android.h"
#ifdef VULKAN_ENABLED
@@ -38,32 +38,32 @@
#include <vulkan/vulkan.h>
#endif
-const char *VulkanContextAndroid::_get_platform_surface_extension() const {
+const char *RenderingContextDriverVulkanAndroid::_get_platform_surface_extension() const {
return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME;
}
-Error VulkanContextAndroid::window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height) {
- VkAndroidSurfaceCreateInfoKHR createInfo;
- createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
- createInfo.pNext = nullptr;
- createInfo.flags = 0;
- createInfo.window = p_window;
+RenderingContextDriver::SurfaceID RenderingContextDriverVulkanAndroid::surface_create(const void *p_platform_data) {
+ const WindowPlatformData *wpd = (const WindowPlatformData *)(p_platform_data);
- VkSurfaceKHR surface;
- VkResult err = vkCreateAndroidSurfaceKHR(get_instance(), &createInfo, nullptr, &surface);
- if (err != VK_SUCCESS) {
- ERR_FAIL_V_MSG(ERR_CANT_CREATE, "vkCreateAndroidSurfaceKHR failed with error " + itos(err));
- }
+ VkAndroidSurfaceCreateInfoKHR create_info = {};
+ create_info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
+ create_info.window = wpd->window;
- return _window_create(DisplayServer::MAIN_WINDOW_ID, p_vsync_mode, surface, p_width, p_height);
+ VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
+ VkResult err = vkCreateAndroidSurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface);
+ ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
+
+ Surface *surface = memnew(Surface);
+ surface->vk_surface = vk_surface;
+ return SurfaceID(surface);
}
-bool VulkanContextAndroid::_use_validation_layers() {
- uint32_t count = 0;
- _get_preferred_validation_layers(&count, nullptr);
+bool RenderingContextDriverVulkanAndroid::_use_validation_layers() const {
+ TightLocalVector<const char *> layer_names;
+ Error err = _find_validation_layers(layer_names);
// On Android, we use validation layers automatically if they were explicitly linked with the app.
- return count > 0;
+ return (err == OK) && !layer_names.is_empty();
}
#endif // VULKAN_ENABLED
diff --git a/platform/android/vulkan_context_android.h b/platform/android/rendering_context_driver_vulkan_android.h
index f253149ef6..a2a42eef24 100644
--- a/platform/android/vulkan_context_android.h
+++ b/platform/android/rendering_context_driver_vulkan_android.h
@@ -1,5 +1,5 @@
/**************************************************************************/
-/* vulkan_context_android.h */
+/* rendering_context_driver_vulkan_android.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,28 +28,32 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-#ifndef VULKAN_CONTEXT_ANDROID_H
-#define VULKAN_CONTEXT_ANDROID_H
+#ifndef RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H
+#define RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H
#ifdef VULKAN_ENABLED
-#include "drivers/vulkan/vulkan_context.h"
+#include "drivers/vulkan/rendering_context_driver_vulkan.h"
struct ANativeWindow;
-class VulkanContextAndroid : public VulkanContext {
- virtual const char *_get_platform_surface_extension() const override;
+class RenderingContextDriverVulkanAndroid : public RenderingContextDriverVulkan {
+private:
+ virtual const char *_get_platform_surface_extension() const override final;
-public:
- Error window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height);
+protected:
+ SurfaceID surface_create(const void *p_platform_data) override final;
+ bool _use_validation_layers() const override final;
- VulkanContextAndroid() = default;
- ~VulkanContextAndroid() override = default;
+public:
+ struct WindowPlatformData {
+ ANativeWindow *window;
+ };
-protected:
- bool _use_validation_layers() override;
+ RenderingContextDriverVulkanAndroid() = default;
+ ~RenderingContextDriverVulkanAndroid() override = default;
};
#endif // VULKAN_ENABLED
-#endif // VULKAN_CONTEXT_ANDROID_H
+#endif // RENDERING_CONTEXT_DRIVER_VULKAN_ANDROID_H
diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp
index 93517d8045..be85e47972 100644
--- a/platform/android/tts_android.cpp
+++ b/platform/android/tts_android.cpp
@@ -77,6 +77,14 @@ void TTS_Android::setup(jobject p_tts) {
}
}
+void TTS_Android::terminate() {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL(env);
+
+ env->DeleteGlobalRef(cls);
+ env->DeleteGlobalRef(tts);
+}
+
void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) {
ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech.");
if (ids.has(p_id)) {
@@ -170,6 +178,8 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum
jstring jStrT = env->NewStringUTF(p_text.utf8().get_data());
jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data());
env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt);
+ env->DeleteLocalRef(jStrT);
+ env->DeleteLocalRef(jStrV);
}
}
diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h
index 39efef6ed1..4cc7c12846 100644
--- a/platform/android/tts_android.h
+++ b/platform/android/tts_android.h
@@ -57,6 +57,7 @@ class TTS_Android {
public:
static void setup(jobject p_tts);
+ static void terminate();
static void _java_utterance_callback(int p_event, int p_id, int p_pos);
static bool is_speaking();