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