summaryrefslogtreecommitdiff
path: root/platform/android
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android')
-rw-r--r--platform/android/README.md7
-rw-r--r--platform/android/SCsub9
-rw-r--r--platform/android/android_input_handler.cpp280
-rw-r--r--platform/android/android_input_handler.h28
-rw-r--r--platform/android/android_keys_utils.cpp47
-rw-r--r--platform/android/android_keys_utils.h251
-rw-r--r--platform/android/api/jni_singleton.h2
-rw-r--r--platform/android/audio_driver_opensl.cpp10
-rw-r--r--platform/android/audio_driver_opensl.h2
-rw-r--r--platform/android/detect.py371
-rw-r--r--platform/android/dir_access_jandroid.cpp322
-rw-r--r--platform/android/dir_access_jandroid.h78
-rw-r--r--platform/android/display_server_android.cpp42
-rw-r--r--platform/android/display_server_android.h6
-rw-r--r--platform/android/export/export.cpp11
-rw-r--r--platform/android/export/export_plugin.cpp311
-rw-r--r--platform/android/export/export_plugin.h22
-rw-r--r--platform/android/export/godot_plugin_config.cpp3
-rw-r--r--platform/android/export/godot_plugin_config.h2
-rw-r--r--platform/android/export/gradle_export_util.cpp21
-rw-r--r--platform/android/export/gradle_export_util.h12
-rw-r--r--platform/android/file_access_android.cpp40
-rw-r--r--platform/android/file_access_android.h43
-rw-r--r--platform/android/file_access_filesystem_jandroid.cpp344
-rw-r--r--platform/android/file_access_filesystem_jandroid.h100
-rw-r--r--platform/android/java/app/AndroidManifest.xml1
-rw-r--r--platform/android/java/app/build.gradle13
-rw-r--r--platform/android/java/app/config.gradle39
-rw-r--r--platform/android/java/app/res/values/themes.xml4
-rw-r--r--platform/android/java/app/settings.gradle13
-rw-r--r--platform/android/java/build.gradle15
-rw-r--r--platform/android/java/editor/build.gradle37
-rw-r--r--platform/android/java/editor/src/main/AndroidManifest.xml19
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java124
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt212
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt (renamed from platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java)14
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt (renamed from platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java)9
-rw-r--r--platform/android/java/editor/src/main/res/values/dimens.xml5
-rw-r--r--platform/android/java/editor/src/main/res/values/strings.xml2
-rw-r--r--platform/android/java/lib/AndroidManifest.xml15
-rw-r--r--platform/android/java/lib/build.gradle12
-rw-r--r--platform/android/java/lib/res/values/strings.xml2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java20
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.java69
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java23
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java127
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java59
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java23
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java49
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt289
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java301
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java22
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt113
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt177
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt224
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt231
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt183
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt (renamed from platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java)105
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt208
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt (renamed from platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java)71
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt284
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java55
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java141
-rw-r--r--platform/android/java/nativeSrcsConfigs/build.gradle5
-rw-r--r--platform/android/java/settings.gradle15
-rw-r--r--platform/android/java_class_wrapper.cpp26
-rw-r--r--platform/android/java_godot_io_wrapper.cpp38
-rw-r--r--platform/android/java_godot_io_wrapper.h11
-rw-r--r--platform/android/java_godot_lib_jni.cpp115
-rw-r--r--platform/android/java_godot_lib_jni.h19
-rw-r--r--platform/android/java_godot_view_wrapper.cpp16
-rw-r--r--platform/android/java_godot_view_wrapper.h6
-rw-r--r--platform/android/java_godot_wrapper.cpp83
-rw-r--r--platform/android/java_godot_wrapper.h49
-rw-r--r--platform/android/jni_utils.cpp10
-rw-r--r--platform/android/net_socket_android.cpp8
-rw-r--r--platform/android/net_socket_android.h2
-rw-r--r--platform/android/os_android.cpp123
-rw-r--r--platform/android/os_android.h8
-rw-r--r--platform/android/plugin/godot_plugin_jni.cpp2
-rw-r--r--platform/android/string_android.h1
-rw-r--r--platform/android/thread_jandroid.h2
-rw-r--r--platform/android/tts_android.cpp20
83 files changed, 4509 insertions, 1724 deletions
diff --git a/platform/android/README.md b/platform/android/README.md
index 343e588553..f6aabab708 100644
--- a/platform/android/README.md
+++ b/platform/android/README.md
@@ -3,6 +3,13 @@
This folder contains the Java and C++ (JNI) code for the Android platform port,
using [Gradle](https://gradle.org/) as a build system.
+## Documentation
+
+- [Compiling for Android](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_android.html)
+ - Instructions on building this platform port from source.
+- [Exporting for Android](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_android.html)
+ - Instructions on using the compiled export templates to export a project.
+
## Artwork license
[`logo.png`](logo.png) and [`run_icon.png`](run_icon.png) are licensed under
diff --git a/platform/android/SCsub b/platform/android/SCsub
index ad226255bc..344ca036de 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -6,6 +6,7 @@ android_files = [
"os_android.cpp",
"android_input_handler.cpp",
"file_access_android.cpp",
+ "file_access_filesystem_jandroid.cpp",
"audio_driver_opensl.cpp",
"dir_access_jandroid.cpp",
"tts_android.cpp",
@@ -40,13 +41,13 @@ lib = env_android.add_shared_library("#bin/libgodot", [android_objects], SHLIBSU
env.Depends(lib, thirdparty_obj)
lib_arch_dir = ""
-if env["android_arch"] == "armv7":
+if env["arch"] == "arm32":
lib_arch_dir = "armeabi-v7a"
-elif env["android_arch"] == "arm64v8":
+elif env["arch"] == "arm64":
lib_arch_dir = "arm64-v8a"
-elif env["android_arch"] == "x86":
+elif env["arch"] == "x86_32":
lib_arch_dir = "x86"
-elif env["android_arch"] == "x86_64":
+elif env["arch"] == "x86_64":
lib_arch_dir = "x86_64"
else:
print("WARN: Architecture not suitable for embedding into APK; keeping .so at \\bin")
diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp
index 10f23b320b..454bcd2eda 100644
--- a/platform/android/android_input_handler.cpp
+++ b/platform/android/android_input_handler.cpp
@@ -56,10 +56,10 @@ void AndroidInputHandler::_set_key_modifier_state(Ref<InputEventWithModifiers> e
ev->set_ctrl_pressed(control_mem);
}
-void AndroidInputHandler::process_key_event(int p_keycode, int p_scancode, int p_unicode_char, bool p_pressed) {
+void AndroidInputHandler::process_key_event(int p_keycode, int p_physical_keycode, int p_unicode, bool p_pressed) {
static char32_t prev_wc = 0;
- char32_t unicode = p_unicode_char;
- if ((p_unicode_char & 0xfffffc00) == 0xd800) {
+ char32_t unicode = p_unicode;
+ if ((p_unicode & 0xfffffc00) == 0xd800) {
if (prev_wc != 0) {
ERR_PRINT("invalid utf16 surrogate input");
}
@@ -78,39 +78,38 @@ void AndroidInputHandler::process_key_event(int p_keycode, int p_scancode, int p
Ref<InputEventKey> ev;
ev.instantiate();
- int val = unicode;
- Key keycode = android_get_keysym(p_keycode);
- Key phy_keycode = android_get_keysym(p_scancode);
- if (keycode == Key::SHIFT) {
- shift_mem = p_pressed;
+ Key physical_keycode = godot_code_from_android_code(p_physical_keycode);
+ Key keycode = physical_keycode;
+ if (p_keycode != 0) {
+ keycode = godot_code_from_unicode(p_keycode);
}
- if (keycode == Key::ALT) {
- alt_mem = p_pressed;
- }
- if (keycode == Key::CTRL) {
- control_mem = p_pressed;
- }
- if (keycode == Key::META) {
- meta_mem = p_pressed;
+
+ switch (physical_keycode) {
+ case Key::SHIFT: {
+ shift_mem = p_pressed;
+ } break;
+ case Key::ALT: {
+ alt_mem = p_pressed;
+ } break;
+ case Key::CTRL: {
+ control_mem = p_pressed;
+ } break;
+ case Key::META: {
+ meta_mem = p_pressed;
+ } break;
+ default:
+ break;
}
ev->set_keycode(keycode);
- ev->set_physical_keycode(phy_keycode);
- ev->set_unicode(val);
+ ev->set_physical_keycode(physical_keycode);
+ ev->set_unicode(unicode);
ev->set_pressed(p_pressed);
_set_key_modifier_state(ev);
- if (val == '\n') {
- ev->set_keycode(Key::ENTER);
- } else if (val == 61448) {
- ev->set_keycode(Key::BACKSPACE);
- ev->set_unicode((char32_t)Key::BACKSPACE);
- } else if (val == 61453) {
- ev->set_keycode(Key::ENTER);
- ev->set_unicode((char32_t)Key::ENTER);
- } else if (p_keycode == 4) {
+ if (p_physical_keycode == AKEYCODE_BACK) {
if (DisplayServerAndroid *dsa = Object::cast_to<DisplayServerAndroid>(DisplayServer::get_singleton())) {
dsa->send_window_event(DisplayServer::WINDOW_EVENT_GO_BACK_REQUEST, true);
}
@@ -119,20 +118,31 @@ void AndroidInputHandler::process_key_event(int p_keycode, int p_scancode, int p
Input::get_singleton()->parse_input_event(ev);
}
-void AndroidInputHandler::process_touch(int p_event, int p_pointer, const Vector<AndroidInputHandler::TouchPos> &p_points) {
+void AndroidInputHandler::_parse_all_touch(bool p_pressed) {
+ if (touch.size()) {
+ //end all if exist
+ for (int i = 0; i < touch.size(); i++) {
+ Ref<InputEventScreenTouch> ev;
+ ev.instantiate();
+ ev->set_index(touch[i].id);
+ ev->set_pressed(p_pressed);
+ ev->set_position(touch[i].pos);
+ Input::get_singleton()->parse_input_event(ev);
+ }
+ }
+}
+
+void AndroidInputHandler::_release_all_touch() {
+ _parse_all_touch(false);
+ touch.clear();
+}
+
+void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points) {
switch (p_event) {
case AMOTION_EVENT_ACTION_DOWN: { //gesture begin
- if (touch.size()) {
- //end all if exist
- for (int i = 0; i < touch.size(); i++) {
- Ref<InputEventScreenTouch> ev;
- ev.instantiate();
- ev->set_index(touch[i].id);
- ev->set_pressed(false);
- ev->set_position(touch[i].pos);
- Input::get_singleton()->parse_input_event(ev);
- }
- }
+ // Release any remaining touches or mouse event
+ _release_mouse_event_info();
+ _release_all_touch();
touch.resize(p_points.size());
for (int i = 0; i < p_points.size(); i++) {
@@ -141,18 +151,13 @@ void AndroidInputHandler::process_touch(int p_event, int p_pointer, const Vector
}
//send touch
- for (int i = 0; i < touch.size(); i++) {
- Ref<InputEventScreenTouch> ev;
- ev.instantiate();
- ev->set_index(touch[i].id);
- ev->set_pressed(true);
- ev->set_position(touch[i].pos);
- Input::get_singleton()->parse_input_event(ev);
- }
+ _parse_all_touch(true);
} break;
case AMOTION_EVENT_ACTION_MOVE: { //motion
- ERR_FAIL_COND(touch.size() != p_points.size());
+ if (touch.size() != p_points.size()) {
+ return;
+ }
for (int i = 0; i < touch.size(); i++) {
int idx = -1;
@@ -181,18 +186,7 @@ void AndroidInputHandler::process_touch(int p_event, int p_pointer, const Vector
} break;
case AMOTION_EVENT_ACTION_CANCEL:
case AMOTION_EVENT_ACTION_UP: { //release
- if (touch.size()) {
- //end all if exist
- for (int i = 0; i < touch.size(); i++) {
- Ref<InputEventScreenTouch> ev;
- ev.instantiate();
- ev->set_index(touch[i].id);
- ev->set_pressed(false);
- ev->set_position(touch[i].pos);
- Input::get_singleton()->parse_input_event(ev);
- }
- touch.clear();
- }
+ _release_all_touch();
} break;
case AMOTION_EVENT_ACTION_POINTER_DOWN: { // add touch
for (int i = 0; i < p_points.size(); i++) {
@@ -230,88 +224,118 @@ void AndroidInputHandler::process_touch(int p_event, int p_pointer, const Vector
}
}
-void AndroidInputHandler::process_hover(int p_type, Point2 p_pos) {
- // https://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER
- switch (p_type) {
+void AndroidInputHandler::_parse_mouse_event_info(MouseButton event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) {
+ if (!mouse_event_info.valid) {
+ return;
+ }
+
+ Ref<InputEventMouseButton> ev;
+ ev.instantiate();
+ _set_key_modifier_state(ev);
+ if (p_source_mouse_relative) {
+ ev->set_position(hover_prev_pos);
+ ev->set_global_position(hover_prev_pos);
+ } else {
+ ev->set_position(mouse_event_info.pos);
+ ev->set_global_position(mouse_event_info.pos);
+ hover_prev_pos = mouse_event_info.pos;
+ }
+ ev->set_pressed(p_pressed);
+ MouseButton changed_button_mask = MouseButton(buttons_state ^ event_buttons_mask);
+
+ buttons_state = event_buttons_mask;
+
+ ev->set_button_index(_button_index_from_mask(changed_button_mask));
+ ev->set_button_mask(event_buttons_mask);
+ ev->set_double_click(p_double_click);
+ Input::get_singleton()->parse_input_event(ev);
+}
+
+void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative) {
+ _parse_mouse_event_info(MouseButton::NONE, false, false, p_source_mouse_relative);
+ mouse_event_info.valid = false;
+}
+
+void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative) {
+ MouseButton event_buttons_mask = _android_button_mask_to_godot_button_mask(p_event_android_buttons_mask);
+ switch (p_event_action) {
case AMOTION_EVENT_ACTION_HOVER_MOVE: // hover move
case AMOTION_EVENT_ACTION_HOVER_ENTER: // hover enter
case AMOTION_EVENT_ACTION_HOVER_EXIT: { // hover exit
+ // https://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER
Ref<InputEventMouseMotion> ev;
ev.instantiate();
_set_key_modifier_state(ev);
- ev->set_position(p_pos);
- ev->set_global_position(p_pos);
- ev->set_relative(p_pos - hover_prev_pos);
+ ev->set_position(p_event_pos);
+ ev->set_global_position(p_event_pos);
+ ev->set_relative(p_event_pos - hover_prev_pos);
Input::get_singleton()->parse_input_event(ev);
- hover_prev_pos = p_pos;
+ hover_prev_pos = p_event_pos;
} break;
- }
-}
-void AndroidInputHandler::process_mouse_event(int input_device, int event_action, int event_android_buttons_mask, Point2 event_pos, float event_vertical_factor, float event_horizontal_factor) {
- MouseButton event_buttons_mask = _android_button_mask_to_godot_button_mask(event_android_buttons_mask);
- switch (event_action) {
- case AMOTION_EVENT_ACTION_BUTTON_PRESS:
- case AMOTION_EVENT_ACTION_BUTTON_RELEASE: {
- Ref<InputEventMouseButton> ev;
- ev.instantiate();
- _set_key_modifier_state(ev);
- if ((input_device & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE) {
- ev->set_position(event_pos);
- ev->set_global_position(event_pos);
- } else {
- ev->set_position(hover_prev_pos);
- ev->set_global_position(hover_prev_pos);
- }
- ev->set_pressed(event_action == AMOTION_EVENT_ACTION_BUTTON_PRESS);
- MouseButton changed_button_mask = MouseButton(buttons_state ^ event_buttons_mask);
+ case AMOTION_EVENT_ACTION_DOWN:
+ case AMOTION_EVENT_ACTION_BUTTON_PRESS: {
+ // Release any remaining touches or mouse event
+ _release_mouse_event_info();
+ _release_all_touch();
- buttons_state = event_buttons_mask;
+ 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);
+ } break;
- ev->set_button_index(_button_index_from_mask(changed_button_mask));
- ev->set_button_mask(event_buttons_mask);
- Input::get_singleton()->parse_input_event(ev);
+ case AMOTION_EVENT_ACTION_UP:
+ case AMOTION_EVENT_ACTION_CANCEL:
+ case AMOTION_EVENT_ACTION_BUTTON_RELEASE: {
+ _release_mouse_event_info(p_source_mouse_relative);
} break;
case AMOTION_EVENT_ACTION_MOVE: {
+ if (!mouse_event_info.valid) {
+ return;
+ }
+
Ref<InputEventMouseMotion> ev;
ev.instantiate();
_set_key_modifier_state(ev);
- if ((input_device & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE) {
- ev->set_position(event_pos);
- ev->set_global_position(event_pos);
- ev->set_relative(event_pos - hover_prev_pos);
- hover_prev_pos = event_pos;
- } else {
+ if (p_source_mouse_relative) {
ev->set_position(hover_prev_pos);
ev->set_global_position(hover_prev_pos);
- ev->set_relative(event_pos);
+ ev->set_relative(p_event_pos);
+ } else {
+ ev->set_position(p_event_pos);
+ ev->set_global_position(p_event_pos);
+ ev->set_relative(p_event_pos - hover_prev_pos);
+ mouse_event_info.pos = p_event_pos;
+ hover_prev_pos = p_event_pos;
}
ev->set_button_mask(event_buttons_mask);
Input::get_singleton()->parse_input_event(ev);
} break;
+
case AMOTION_EVENT_ACTION_SCROLL: {
Ref<InputEventMouseButton> ev;
ev.instantiate();
- if ((input_device & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE) {
- ev->set_position(event_pos);
- ev->set_global_position(event_pos);
- } else {
+ _set_key_modifier_state(ev);
+ if (p_source_mouse_relative) {
ev->set_position(hover_prev_pos);
ev->set_global_position(hover_prev_pos);
+ } else {
+ ev->set_position(p_event_pos);
+ ev->set_global_position(p_event_pos);
}
ev->set_pressed(true);
buttons_state = event_buttons_mask;
- if (event_vertical_factor > 0) {
- _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_UP, event_vertical_factor);
- } else if (event_vertical_factor < 0) {
- _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_DOWN, -event_vertical_factor);
+ if (p_delta.y > 0) {
+ _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_UP, p_delta.y);
+ } else if (p_delta.y < 0) {
+ _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_DOWN, -p_delta.y);
}
- if (event_horizontal_factor > 0) {
- _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_RIGHT, event_horizontal_factor);
- } else if (event_horizontal_factor < 0) {
- _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_LEFT, -event_horizontal_factor);
+ if (p_delta.x > 0) {
+ _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_RIGHT, p_delta.x);
+ } else if (p_delta.x < 0) {
+ _wheel_button_click(event_buttons_mask, ev, MouseButton::WHEEL_LEFT, -p_delta.x);
}
} break;
}
@@ -330,18 +354,22 @@ void AndroidInputHandler::_wheel_button_click(MouseButton event_buttons_mask, co
Input::get_singleton()->parse_input_event(evdd);
}
-void AndroidInputHandler::process_double_tap(int event_android_button_mask, Point2 p_pos) {
- MouseButton event_button_mask = _android_button_mask_to_godot_button_mask(event_android_button_mask);
- Ref<InputEventMouseButton> ev;
- ev.instantiate();
- _set_key_modifier_state(ev);
- ev->set_position(p_pos);
- ev->set_global_position(p_pos);
- ev->set_pressed(event_button_mask != MouseButton::NONE);
- ev->set_button_index(_button_index_from_mask(event_button_mask));
- ev->set_button_mask(event_button_mask);
- ev->set_double_click(true);
- Input::get_singleton()->parse_input_event(ev);
+void AndroidInputHandler::process_magnify(Point2 p_pos, float p_factor) {
+ Ref<InputEventMagnifyGesture> magnify_event;
+ magnify_event.instantiate();
+ _set_key_modifier_state(magnify_event);
+ magnify_event->set_position(p_pos);
+ magnify_event->set_factor(p_factor);
+ Input::get_singleton()->parse_input_event(magnify_event);
+}
+
+void AndroidInputHandler::process_pan(Point2 p_pos, Vector2 p_delta) {
+ Ref<InputEventPanGesture> pan_event;
+ pan_event.instantiate();
+ _set_key_modifier_state(pan_event);
+ pan_event->set_position(p_pos);
+ pan_event->set_delta(p_delta);
+ Input::get_singleton()->parse_input_event(pan_event);
}
MouseButton AndroidInputHandler::_button_index_from_mask(MouseButton button_mask) {
@@ -381,13 +409,3 @@ MouseButton AndroidInputHandler::_android_button_mask_to_godot_button_mask(int a
return godot_button_mask;
}
-
-void AndroidInputHandler::process_scroll(Point2 p_pos) {
- Ref<InputEventPanGesture> ev;
- ev.instantiate();
- _set_key_modifier_state(ev);
- ev->set_position(p_pos);
- ev->set_delta(p_pos - scroll_prev_pos);
- Input::get_singleton()->parse_input_event(ev);
- scroll_prev_pos = p_pos;
-}
diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h
index 2b237c4006..88490f0407 100644
--- a/platform/android/android_input_handler.h
+++ b/platform/android/android_input_handler.h
@@ -44,6 +44,11 @@ public:
Point2 pos;
};
+ struct MouseEventInfo {
+ bool valid = false;
+ Point2 pos;
+ };
+
enum {
JOY_EVENT_BUTTON = 0,
JOY_EVENT_AXIS = 1,
@@ -68,8 +73,8 @@ private:
MouseButton buttons_state = MouseButton::NONE;
Vector<TouchPos> touch;
+ MouseEventInfo mouse_event_info;
Point2 hover_prev_pos; // needed to calculate the relative position on hover events
- Point2 scroll_prev_pos; // needed to calculate the relative position on scroll events
void _set_key_modifier_state(Ref<InputEventWithModifiers> ev);
@@ -78,14 +83,21 @@ private:
void _wheel_button_click(MouseButton event_buttons_mask, const Ref<InputEventMouseButton> &ev, MouseButton wheel_button, float factor);
+ void _parse_mouse_event_info(MouseButton event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative);
+
+ void _release_mouse_event_info(bool p_source_mouse_relative = false);
+
+ void _parse_all_touch(bool p_pressed);
+
+ void _release_all_touch();
+
public:
- void process_touch(int p_event, int p_pointer, const Vector<TouchPos> &p_points);
- void process_hover(int p_type, Point2 p_pos);
- void process_mouse_event(int input_device, int event_action, int event_android_buttons_mask, Point2 event_pos, float event_vertical_factor = 0, float event_horizontal_factor = 0);
- void process_double_tap(int event_android_button_mask, Point2 p_pos);
- void process_scroll(Point2 p_pos);
+ void process_mouse_event(int p_event_action, int p_event_android_buttons_mask, Point2 p_event_pos, Vector2 p_delta, bool p_double_click, bool p_source_mouse_relative);
+ void process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points);
+ 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_keycode, int p_scancode, int p_unicode_char, bool p_pressed);
+ void process_key_event(int p_keycode, int p_physical_keycode, int p_unicode, bool p_pressed);
};
-#endif
+#endif // ANDROID_INPUT_HANDLER_H
diff --git a/platform/android/android_keys_utils.cpp b/platform/android/android_keys_utils.cpp
index 885e4ff145..d2c5fdfd6c 100644
--- a/platform/android/android_keys_utils.cpp
+++ b/platform/android/android_keys_utils.cpp
@@ -30,12 +30,49 @@
#include "android_keys_utils.h"
-Key android_get_keysym(unsigned int p_code) {
- for (int i = 0; _ak_to_keycode[i].keysym != Key::UNKNOWN; i++) {
- if (_ak_to_keycode[i].keycode == p_code) {
- return _ak_to_keycode[i].keysym;
+Key godot_code_from_android_code(unsigned int p_code) {
+ for (int i = 0; android_godot_code_pairs[i].android_code != AKEYCODE_MAX; i++) {
+ if (android_godot_code_pairs[i].android_code == p_code) {
+ return android_godot_code_pairs[i].godot_code;
}
}
-
return Key::UNKNOWN;
}
+
+Key godot_code_from_unicode(unsigned int p_code) {
+ unsigned int code = p_code;
+ if (code > 0xFF) {
+ return Key::UNKNOWN;
+ }
+ // Known control codes.
+ if (code == '\b') { // 0x08
+ return Key::BACKSPACE;
+ }
+ if (code == '\t') { // 0x09
+ return Key::TAB;
+ }
+ if (code == '\n') { // 0x0A
+ return Key::ENTER;
+ }
+ if (code == 0x1B) {
+ return Key::ESCAPE;
+ }
+ if (code == 0x1F) {
+ return Key::KEY_DELETE;
+ }
+ // Unknown control codes.
+ if (code <= 0x1F || (code >= 0x80 && code <= 0x9F)) {
+ return Key::UNKNOWN;
+ }
+ // Convert to uppercase.
+ if (code >= 'a' && code <= 'z') { // 0x61 - 0x7A
+ code -= ('a' - 'A');
+ }
+ if (code >= u'à' && code <= u'ö') { // 0xE0 - 0xF6
+ code -= (u'à' - u'À'); // 0xE0 - 0xC0
+ }
+ if (code >= u'ø' && code <= u'þ') { // 0xF8 - 0xFF
+ code -= (u'ø' - u'Ø'); // 0xF8 - 0xD8
+ }
+ return Key(code);
+}
diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h
index 24a6589fba..5ec3ee17aa 100644
--- a/platform/android/android_keys_utils.h
+++ b/platform/android/android_keys_utils.h
@@ -34,129 +34,140 @@
#include <android/input.h>
#include <core/os/keyboard.h>
-struct _WinTranslatePair {
- Key keysym = Key::NONE;
- unsigned int keycode = 0;
+#define AKEYCODE_MAX 0xFFFF
+
+struct AndroidGodotCodePair {
+ unsigned int android_code = 0;
+ Key godot_code = Key::NONE;
};
-static _WinTranslatePair _ak_to_keycode[] = {
- { Key::TAB, AKEYCODE_TAB },
- { Key::ENTER, AKEYCODE_ENTER },
- { Key::SHIFT, AKEYCODE_SHIFT_LEFT },
- { Key::SHIFT, AKEYCODE_SHIFT_RIGHT },
- { Key::ALT, AKEYCODE_ALT_LEFT },
- { Key::ALT, AKEYCODE_ALT_RIGHT },
- { Key::MENU, AKEYCODE_MENU },
- { Key::PAUSE, AKEYCODE_MEDIA_PLAY_PAUSE },
- { Key::ESCAPE, AKEYCODE_BACK },
- { Key::SPACE, AKEYCODE_SPACE },
- { Key::PAGEUP, AKEYCODE_PAGE_UP },
- { Key::PAGEDOWN, AKEYCODE_PAGE_DOWN },
- { Key::HOME, AKEYCODE_HOME }, //(0x24)
- { Key::LEFT, AKEYCODE_DPAD_LEFT },
- { Key::UP, AKEYCODE_DPAD_UP },
- { Key::RIGHT, AKEYCODE_DPAD_RIGHT },
- { Key::DOWN, AKEYCODE_DPAD_DOWN },
- { Key::PERIODCENTERED, AKEYCODE_DPAD_CENTER },
- { Key::BACKSPACE, AKEYCODE_DEL },
- { Key::KEY_0, AKEYCODE_0 },
- { Key::KEY_1, AKEYCODE_1 },
- { Key::KEY_2, AKEYCODE_2 },
- { Key::KEY_3, AKEYCODE_3 },
- { Key::KEY_4, AKEYCODE_4 },
- { Key::KEY_5, AKEYCODE_5 },
- { Key::KEY_6, AKEYCODE_6 },
- { Key::KEY_7, AKEYCODE_7 },
- { Key::KEY_8, AKEYCODE_8 },
- { Key::KEY_9, AKEYCODE_9 },
- { Key::A, AKEYCODE_A },
- { Key::B, AKEYCODE_B },
- { Key::C, AKEYCODE_C },
- { Key::D, AKEYCODE_D },
- { Key::E, AKEYCODE_E },
- { Key::F, AKEYCODE_F },
- { Key::G, AKEYCODE_G },
- { Key::H, AKEYCODE_H },
- { Key::I, AKEYCODE_I },
- { Key::J, AKEYCODE_J },
- { Key::K, AKEYCODE_K },
- { Key::L, AKEYCODE_L },
- { Key::M, AKEYCODE_M },
- { Key::N, AKEYCODE_N },
- { Key::O, AKEYCODE_O },
- { Key::P, AKEYCODE_P },
- { Key::Q, AKEYCODE_Q },
- { Key::R, AKEYCODE_R },
- { Key::S, AKEYCODE_S },
- { Key::T, AKEYCODE_T },
- { Key::U, AKEYCODE_U },
- { Key::V, AKEYCODE_V },
- { Key::W, AKEYCODE_W },
- { Key::X, AKEYCODE_X },
- { Key::Y, AKEYCODE_Y },
- { Key::Z, AKEYCODE_Z },
- { Key::HOMEPAGE, AKEYCODE_EXPLORER },
- { Key::LAUNCH0, AKEYCODE_BUTTON_A },
- { Key::LAUNCH1, AKEYCODE_BUTTON_B },
- { Key::LAUNCH2, AKEYCODE_BUTTON_C },
- { Key::LAUNCH3, AKEYCODE_BUTTON_X },
- { Key::LAUNCH4, AKEYCODE_BUTTON_Y },
- { Key::LAUNCH5, AKEYCODE_BUTTON_Z },
- { Key::LAUNCH6, AKEYCODE_BUTTON_L1 },
- { Key::LAUNCH7, AKEYCODE_BUTTON_R1 },
- { Key::LAUNCH8, AKEYCODE_BUTTON_L2 },
- { Key::LAUNCH9, AKEYCODE_BUTTON_R2 },
- { Key::LAUNCHA, AKEYCODE_BUTTON_THUMBL },
- { Key::LAUNCHB, AKEYCODE_BUTTON_THUMBR },
- { Key::LAUNCHC, AKEYCODE_BUTTON_START },
- { Key::LAUNCHD, AKEYCODE_BUTTON_SELECT },
- { Key::LAUNCHE, AKEYCODE_BUTTON_MODE },
- { Key::VOLUMEMUTE, AKEYCODE_MUTE },
- { Key::VOLUMEDOWN, AKEYCODE_VOLUME_DOWN },
- { Key::VOLUMEUP, AKEYCODE_VOLUME_UP },
- { Key::BACK, AKEYCODE_MEDIA_REWIND },
- { Key::FORWARD, AKEYCODE_MEDIA_FAST_FORWARD },
- { Key::MEDIANEXT, AKEYCODE_MEDIA_NEXT },
- { Key::MEDIAPREVIOUS, AKEYCODE_MEDIA_PREVIOUS },
- { Key::MEDIASTOP, AKEYCODE_MEDIA_STOP },
- { Key::PLUS, AKEYCODE_PLUS },
- { Key::EQUAL, AKEYCODE_EQUALS }, // the '+' key
- { Key::COMMA, AKEYCODE_COMMA }, // the ',' key
- { Key::MINUS, AKEYCODE_MINUS }, // the '-' key
- { Key::SLASH, AKEYCODE_SLASH }, // the '/?' key
- { Key::BACKSLASH, AKEYCODE_BACKSLASH },
- { Key::BRACKETLEFT, AKEYCODE_LEFT_BRACKET },
- { Key::BRACKETRIGHT, AKEYCODE_RIGHT_BRACKET },
- { Key::CTRL, AKEYCODE_CTRL_LEFT },
- { Key::CTRL, AKEYCODE_CTRL_RIGHT },
- { Key::UNKNOWN, 0 }
+static AndroidGodotCodePair android_godot_code_pairs[] = {
+ { AKEYCODE_UNKNOWN, Key::UNKNOWN }, // (0) Unknown key code.
+ { AKEYCODE_HOME, Key::HOME }, // (3) Home key.
+ { AKEYCODE_BACK, Key::BACK }, // (4) Back key.
+ { AKEYCODE_0, Key::KEY_0 }, // (7) '0' key.
+ { AKEYCODE_1, Key::KEY_1 }, // (8) '1' key.
+ { AKEYCODE_2, Key::KEY_2 }, // (9) '2' key.
+ { AKEYCODE_3, Key::KEY_3 }, // (10) '3' key.
+ { AKEYCODE_4, Key::KEY_4 }, // (11) '4' key.
+ { AKEYCODE_5, Key::KEY_5 }, // (12) '5' key.
+ { AKEYCODE_6, Key::KEY_6 }, // (13) '6' key.
+ { AKEYCODE_7, Key::KEY_7 }, // (14) '7' key.
+ { AKEYCODE_8, Key::KEY_8 }, // (15) '8' key.
+ { AKEYCODE_9, Key::KEY_9 }, // (16) '9' key.
+ { AKEYCODE_STAR, Key::ASTERISK }, // (17) '*' key.
+ { AKEYCODE_POUND, Key::NUMBERSIGN }, // (18) '#' key.
+ { AKEYCODE_DPAD_UP, Key::UP }, // (19) Directional Pad Up key.
+ { 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_VOLUME_UP, Key::VOLUMEUP }, // (24) Volume Up key.
+ { AKEYCODE_VOLUME_DOWN, Key::VOLUMEDOWN }, // (25) Volume Down key.
+ { AKEYCODE_CLEAR, Key::CLEAR }, // (28) Clear key.
+ { AKEYCODE_A, Key::A }, // (29) 'A' key.
+ { AKEYCODE_B, Key::B }, // (30) 'B' key.
+ { AKEYCODE_C, Key::C }, // (31) 'C' key.
+ { AKEYCODE_D, Key::D }, // (32) 'D' key.
+ { AKEYCODE_E, Key::E }, // (33) 'E' key.
+ { AKEYCODE_F, Key::F }, // (34) 'F' key.
+ { AKEYCODE_G, Key::G }, // (35) 'G' key.
+ { AKEYCODE_H, Key::H }, // (36) 'H' key.
+ { AKEYCODE_I, Key::I }, // (37) 'I' key.
+ { AKEYCODE_J, Key::J }, // (38) 'J' key.
+ { AKEYCODE_K, Key::K }, // (39) 'K' key.
+ { AKEYCODE_L, Key::L }, // (40) 'L' key.
+ { AKEYCODE_M, Key::M }, // (41) 'M' key.
+ { AKEYCODE_N, Key::N }, // (42) 'N' key.
+ { AKEYCODE_O, Key::O }, // (43) 'O' key.
+ { AKEYCODE_P, Key::P }, // (44) 'P' key.
+ { AKEYCODE_Q, Key::Q }, // (45) 'Q' key.
+ { AKEYCODE_R, Key::R }, // (46) 'R' key.
+ { AKEYCODE_S, Key::S }, // (47) 'S' key.
+ { AKEYCODE_T, Key::T }, // (48) 'T' key.
+ { AKEYCODE_U, Key::U }, // (49) 'U' key.
+ { AKEYCODE_V, Key::V }, // (50) 'V' key.
+ { AKEYCODE_W, Key::W }, // (51) 'W' key.
+ { AKEYCODE_X, Key::X }, // (52) 'X' key.
+ { AKEYCODE_Y, Key::Y }, // (53) 'Y' key.
+ { AKEYCODE_Z, Key::Z }, // (54) 'Z' key.
+ { AKEYCODE_COMMA, Key::COMMA }, // (55) ',’ key.
+ { AKEYCODE_PERIOD, Key::PERIOD }, // (56) '.' key.
+ { AKEYCODE_ALT_LEFT, Key::ALT }, // (57) Left Alt modifier key.
+ { AKEYCODE_ALT_RIGHT, Key::ALT }, // (58) Right Alt modifier key.
+ { AKEYCODE_SHIFT_LEFT, Key::SHIFT }, // (59) Left Shift modifier key.
+ { AKEYCODE_SHIFT_RIGHT, Key::SHIFT }, // (60) Right Shift modifier key.
+ { AKEYCODE_TAB, Key::TAB }, // (61) Tab key.
+ { AKEYCODE_SPACE, Key::SPACE }, // (62) Space key.
+ { AKEYCODE_ENTER, Key::ENTER }, // (66) Enter key.
+ { AKEYCODE_DEL, Key::BACKSPACE }, // (67) Backspace key.
+ { AKEYCODE_GRAVE, Key::QUOTELEFT }, // (68) '`' (backtick) key.
+ { AKEYCODE_MINUS, Key::MINUS }, // (69) '-'.
+ { AKEYCODE_EQUALS, Key::EQUAL }, // (70) '=' key.
+ { AKEYCODE_LEFT_BRACKET, Key::BRACKETLEFT }, // (71) '[' key.
+ { AKEYCODE_RIGHT_BRACKET, Key::BRACKETRIGHT }, // (72) ']' key.
+ { AKEYCODE_BACKSLASH, Key::BACKSLASH }, // (73) '\' key.
+ { AKEYCODE_SEMICOLON, Key::SEMICOLON }, // (74) ';' key.
+ { AKEYCODE_APOSTROPHE, Key::APOSTROPHE }, // (75) ''' (apostrophe) key.
+ { AKEYCODE_SLASH, Key::SLASH }, // (76) '/' key.
+ { AKEYCODE_AT, Key::AT }, // (77) '@' key.
+ { AKEYCODE_PLUS, Key::PLUS }, // (81) '+' key.
+ { AKEYCODE_MENU, Key::MENU }, // (82) Menu key.
+ { AKEYCODE_SEARCH, Key::SEARCH }, // (84) Search key.
+ { AKEYCODE_MEDIA_STOP, Key::MEDIASTOP }, // (86) Stop media key.
+ { AKEYCODE_MEDIA_PREVIOUS, Key::MEDIAPREVIOUS }, // (88) Play Previous media key.
+ { AKEYCODE_PAGE_UP, Key::PAGEUP }, // (92) Page Up key.
+ { AKEYCODE_PAGE_DOWN, Key::PAGEDOWN }, // (93) Page Down key.
+ { AKEYCODE_ESCAPE, Key::ESCAPE }, // (111) Escape key.
+ { AKEYCODE_FORWARD_DEL, Key::KEY_DELETE }, // (112) Forward Delete key.
+ { AKEYCODE_CTRL_LEFT, Key::CTRL }, // (113) Left Control modifier key.
+ { AKEYCODE_CTRL_RIGHT, Key::CTRL }, // (114) Right Control modifier key.
+ { AKEYCODE_CAPS_LOCK, Key::CAPSLOCK }, // (115) Caps Lock key.
+ { AKEYCODE_SCROLL_LOCK, Key::SCROLLLOCK }, // (116) Scroll Lock key.
+ { AKEYCODE_META_LEFT, Key::META }, // (117) Left Meta modifier key.
+ { AKEYCODE_META_RIGHT, Key::META }, // (118) Right Meta modifier key.
+ { AKEYCODE_SYSRQ, Key::PRINT }, // (120) System Request / Print Screen key.
+ { AKEYCODE_BREAK, Key::PAUSE }, // (121) Break / Pause key.
+ { AKEYCODE_INSERT, Key::INSERT }, // (124) Insert key.
+ { AKEYCODE_FORWARD, Key::FORWARD }, // (125) Forward key.
+ { AKEYCODE_MEDIA_PLAY, Key::MEDIAPLAY }, // (126) Play media key.
+ { AKEYCODE_MEDIA_RECORD, Key::MEDIARECORD }, // (130) Record media key.
+ { AKEYCODE_F1, Key::F1 }, // (131) F1 key.
+ { AKEYCODE_F2, Key::F2 }, // (132) F2 key.
+ { AKEYCODE_F3, Key::F3 }, // (133) F3 key.
+ { AKEYCODE_F4, Key::F4 }, // (134) F4 key.
+ { AKEYCODE_F5, Key::F5 }, // (135) F5 key.
+ { AKEYCODE_F6, Key::F6 }, // (136) F6 key.
+ { AKEYCODE_F7, Key::F7 }, // (137) F7 key.
+ { AKEYCODE_F8, Key::F8 }, // (138) F8 key.
+ { AKEYCODE_F9, Key::F9 }, // (139) F9 key.
+ { AKEYCODE_F10, Key::F10 }, // (140) F10 key.
+ { AKEYCODE_F11, Key::F11 }, // (141) F11 key.
+ { AKEYCODE_F12, Key::F12 }, // (142) F12 key.
+ { AKEYCODE_NUM_LOCK, Key::NUMLOCK }, // (143) Num Lock key.
+ { AKEYCODE_NUMPAD_0, Key::KP_0 }, // (144) Numeric keypad '0' key.
+ { AKEYCODE_NUMPAD_1, Key::KP_1 }, // (145) Numeric keypad '1' key.
+ { AKEYCODE_NUMPAD_2, Key::KP_2 }, // (146) Numeric keypad '2' key.
+ { AKEYCODE_NUMPAD_3, Key::KP_3 }, // (147) Numeric keypad '3' key.
+ { AKEYCODE_NUMPAD_4, Key::KP_4 }, // (148) Numeric keypad '4' key.
+ { AKEYCODE_NUMPAD_5, Key::KP_5 }, // (149) Numeric keypad '5' key.
+ { AKEYCODE_NUMPAD_6, Key::KP_6 }, // (150) Numeric keypad '6' key.
+ { AKEYCODE_NUMPAD_7, Key::KP_7 }, // (151) Numeric keypad '7' key.
+ { AKEYCODE_NUMPAD_8, Key::KP_8 }, // (152) Numeric keypad '8' key.
+ { AKEYCODE_NUMPAD_9, Key::KP_9 }, // (153) Numeric keypad '9' key.
+ { AKEYCODE_NUMPAD_DIVIDE, Key::KP_DIVIDE }, // (154) Numeric keypad '/' key (for division).
+ { AKEYCODE_NUMPAD_MULTIPLY, Key::KP_MULTIPLY }, // (155) Numeric keypad '*' key (for multiplication).
+ { AKEYCODE_NUMPAD_SUBTRACT, Key::KP_SUBTRACT }, // (156) Numeric keypad '-' key (for subtraction).
+ { AKEYCODE_NUMPAD_ADD, Key::KP_ADD }, // (157) Numeric keypad '+' key (for addition).
+ { AKEYCODE_NUMPAD_DOT, Key::KP_PERIOD }, // (158) Numeric keypad '.' key (for decimals or digit grouping).
+ { AKEYCODE_NUMPAD_ENTER, Key::KP_ENTER }, // (160) Numeric keypad Enter key.
+ { AKEYCODE_VOLUME_MUTE, Key::VOLUMEMUTE }, // (164) Volume Mute key.
+ { AKEYCODE_YEN, Key::YEN }, // (216) Japanese Yen key.
+ { AKEYCODE_HELP, Key::HELP }, // (259) Help key.
+ { AKEYCODE_REFRESH, Key::REFRESH }, // (285) Refresh key.
+ { AKEYCODE_MAX, Key::UNKNOWN }
};
-/*
-TODO: map these android key:
- AKEYCODE_SOFT_LEFT = 1,
- AKEYCODE_SOFT_RIGHT = 2,
- AKEYCODE_CALL = 5,
- AKEYCODE_ENDCALL = 6,
- AKEYCODE_STAR = 17,
- AKEYCODE_POUND = 18,
- AKEYCODE_POWER = 26,
- AKEYCODE_CAMERA = 27,
- AKEYCODE_CLEAR = 28,
- AKEYCODE_SYM = 63,
- AKEYCODE_ENVELOPE = 65,
- AKEYCODE_GRAVE = 68,
- AKEYCODE_SEMICOLON = 74,
- AKEYCODE_APOSTROPHE = 75,
- AKEYCODE_AT = 77,
- AKEYCODE_NUM = 78,
- AKEYCODE_HEADSETHOOK = 79,
- AKEYCODE_FOCUS = 80, // *Camera* focus
- AKEYCODE_NOTIFICATION = 83,
- AKEYCODE_SEARCH = 84,
- AKEYCODE_PICTSYMBOLS = 94,
- AKEYCODE_SWITCH_CHARSET = 95,
-*/
-Key android_get_keysym(unsigned int p_code);
+Key godot_code_from_android_code(unsigned int p_code);
+Key godot_code_from_unicode(unsigned int p_code);
#endif // ANDROID_KEYS_UTILS_H
diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h
index 8a0e021157..690fddae21 100644
--- a/platform/android/api/jni_singleton.h
+++ b/platform/android/api/jni_singleton.h
@@ -73,7 +73,7 @@ public:
return Object::callp(p_method, p_args, p_argcount, r_error);
}
- ERR_FAIL_COND_V(!instance, Variant());
+ ERR_FAIL_NULL_V(instance, Variant());
r_error.error = Callable::CallError::CALL_OK;
diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp
index dcaa586e3b..6b22a0ffa1 100644
--- a/platform/android/audio_driver_opensl.cpp
+++ b/platform/android/audio_driver_opensl.cpp
@@ -80,8 +80,6 @@ void AudioDriverOpenSL::_buffer_callbacks(
ad->_buffer_callback(queueItf);
}
-AudioDriverOpenSL *AudioDriverOpenSL::s_ad = nullptr;
-
const char *AudioDriverOpenSL::get_name() const {
return "Android";
}
@@ -133,8 +131,6 @@ void AudioDriverOpenSL::start() {
ERR_FAIL_COND(res != SL_RESULT_SUCCESS);
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, BUFFER_COUNT };
- //bufferQueue.locatorType = SL_DATALOCATOR_BUFFERQUEUE;
- //bufferQueue.numBuffers = BUFFER_COUNT; /* Four buffers in our buffer queue */
/* Setup the format of the content in the buffer queue */
pcm.formatType = SL_DATAFORMAT_PCM;
pcm.numChannels = 2;
@@ -155,13 +151,8 @@ void AudioDriverOpenSL::start() {
locator_outputmix.outputMix = OutputMix;
audioSink.pLocator = (void *)&locator_outputmix;
audioSink.pFormat = nullptr;
- /* Initialize the context for Buffer queue callbacks */
- //cntxt.pDataBase = (void*)&pcmData;
- //cntxt.pData = cntxt.pDataBase;
- //cntxt.size = sizeof(pcmData);
/* Create the music player */
-
{
const SLInterfaceID ids[2] = { SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND };
const SLboolean req[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
@@ -341,5 +332,4 @@ void AudioDriverOpenSL::set_pause(bool p_pause) {
}
AudioDriverOpenSL::AudioDriverOpenSL() {
- s_ad = this;
}
diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h
index eea1fc227f..7b09438858 100644
--- a/platform/android/audio_driver_opensl.h
+++ b/platform/android/audio_driver_opensl.h
@@ -105,4 +105,4 @@ public:
AudioDriverOpenSL();
};
-#endif // AUDIO_DRIVER_ANDROID_H
+#endif // AUDIO_DRIVER_OPENSL_H
diff --git a/platform/android/detect.py b/platform/android/detect.py
index 0099ac7e0d..2ff9cd3dc5 100644
--- a/platform/android/detect.py
+++ b/platform/android/detect.py
@@ -1,7 +1,7 @@
import os
import sys
import platform
-from distutils.version import LooseVersion
+import subprocess
def is_active():
@@ -13,176 +13,106 @@ def get_name():
def can_build():
- return ("ANDROID_SDK_ROOT" in os.environ) or ("ANDROID_HOME" in os.environ)
-
-
-def get_platform(platform):
- return int(platform.split("-")[1])
+ return os.path.exists(get_env_android_sdk_root())
def get_opts():
from SCons.Variables import BoolVariable, EnumVariable
return [
- ("ANDROID_NDK_ROOT", "Path to the Android NDK", get_android_ndk_root()),
- ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_android_sdk_root()),
+ ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()),
("ndk_platform", 'Target platform (android-<api>, e.g. "android-24")', "android-24"),
- EnumVariable("android_arch", "Target architecture", "arm64v8", ("armv7", "arm64v8", "x86", "x86_64")),
]
# Return the ANDROID_SDK_ROOT environment variable.
-# While ANDROID_HOME has been deprecated, it's used as a fallback for backward
-# compatibility purposes.
-def get_android_sdk_root():
- if "ANDROID_SDK_ROOT" in os.environ:
- return os.environ.get("ANDROID_SDK_ROOT", 0)
- else:
- return os.environ.get("ANDROID_HOME", 0)
+def get_env_android_sdk_root():
+ return os.environ.get("ANDROID_SDK_ROOT", -1)
-# Return the ANDROID_NDK_ROOT environment variable.
-# We generate one for this build using the ANDROID_SDK_ROOT env
-# variable and the project ndk version.
-# If the env variable is already defined, we override it with
-# our own to match what the project expects.
-def get_android_ndk_root():
- return get_android_sdk_root() + "/ndk/" + get_project_ndk_version()
+def get_min_sdk_version(platform):
+ return int(platform.split("-")[1])
+
+
+def get_android_ndk_root(env):
+ return env["ANDROID_SDK_ROOT"] + "/ndk/" + get_ndk_version()
+
+
+# This is kept in sync with the value in 'platform/android/java/app/config.gradle'.
+def get_ndk_version():
+ return "23.2.8568313"
def get_flags():
return [
+ ("arch", "arm64"), # Default for convenience.
("tools", False),
]
-def create(env):
- tools = env["TOOLS"]
- if "mingw" in tools:
- tools.remove("mingw")
- if "applelink" in tools:
- tools.remove("applelink")
- env.Tool("gcc")
- return env.Clone(tools=tools)
-
-
-# Check if ANDROID_NDK_ROOT is valid.
-# If not, install the ndk using ANDROID_SDK_ROOT and sdkmanager.
+# Check if Android NDK version is installed
+# If not, install it.
def install_ndk_if_needed(env):
print("Checking for Android NDK...")
- env_ndk_version = get_env_ndk_version(env["ANDROID_NDK_ROOT"])
- if env_ndk_version is None:
- # Reinstall the ndk and update ANDROID_NDK_ROOT.
- print("Installing Android NDK...")
- if env["ANDROID_SDK_ROOT"] is None:
- raise Exception("Invalid ANDROID_SDK_ROOT environment variable.")
-
- import subprocess
-
+ sdk_root = env["ANDROID_SDK_ROOT"]
+ if not os.path.exists(get_android_ndk_root(env)):
extension = ".bat" if os.name == "nt" else ""
- sdkmanager_path = env["ANDROID_SDK_ROOT"] + "/cmdline-tools/latest/bin/sdkmanager" + extension
- ndk_download_args = "ndk;" + get_project_ndk_version()
- subprocess.check_call([sdkmanager_path, ndk_download_args])
-
- env["ANDROID_NDK_ROOT"] = env["ANDROID_SDK_ROOT"] + "/ndk/" + get_project_ndk_version()
- print("ANDROID_NDK_ROOT: " + env["ANDROID_NDK_ROOT"])
-
-
-def configure(env):
- install_ndk_if_needed(env)
-
- # Workaround for MinGW. See:
- # https://www.scons.org/wiki/LongCmdLinesOnWin32
- if os.name == "nt":
-
- import subprocess
-
- def mySubProcess(cmdline, env):
- # print("SPAWNED : " + cmdline)
- startupinfo = subprocess.STARTUPINFO()
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- proc = subprocess.Popen(
- cmdline,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- startupinfo=startupinfo,
- shell=False,
- env=env,
+ sdkmanager = sdk_root + "/cmdline-tools/latest/bin/sdkmanager" + extension
+ if os.path.exists(sdkmanager):
+ # Install the Android NDK
+ print("Installing Android NDK...")
+ ndk_download_args = "ndk;" + get_ndk_version()
+ subprocess.check_call([sdkmanager, ndk_download_args])
+ else:
+ print("Cannot find " + sdkmanager)
+ print(
+ "Please ensure ANDROID_SDK_ROOT is correct and cmdline-tools are installed, or install NDK version "
+ + get_ndk_version()
+ + " manually."
)
- data, err = proc.communicate()
- rv = proc.wait()
- if rv:
- print("=====")
- print(err)
- print("=====")
- return rv
+ sys.exit()
+ env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env)
- def mySpawn(sh, escape, cmd, args, env):
- newargs = " ".join(args[1:])
- cmdline = cmd + " " + newargs
-
- rv = 0
- if len(cmdline) > 32000 and cmd.endswith("ar"):
- cmdline = cmd + " " + args[1] + " " + args[2] + " "
- for i in range(3, len(args)):
- rv = mySubProcess(cmdline + args[i], env)
- if rv:
- break
- else:
- rv = mySubProcess(cmdline, env)
-
- return rv
+def configure(env):
+ # Validate arch.
+ supported_arches = ["x86_32", "x86_64", "arm32", "arm64"]
+ if env["arch"] not in supported_arches:
+ print(
+ 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.'
+ % (env["arch"], ", ".join(supported_arches))
+ )
+ sys.exit()
- env["SPAWN"] = mySpawn
+ install_ndk_if_needed(env)
+ ndk_root = env["ANDROID_NDK_ROOT"]
# Architecture
- if env["android_arch"] not in ["armv7", "arm64v8", "x86", "x86_64"]:
- env["android_arch"] = "armv7"
-
- print("Building for Android, platform " + env["ndk_platform"] + " (" + env["android_arch"] + ")")
+ if get_min_sdk_version(env["ndk_platform"]) < 21 and env["arch"] in ["x86_64", "arm64"]:
+ print(
+ 'WARNING: arch="%s" is not supported with "ndk_platform" lower than "android-21". Forcing platform 21.'
+ % env["arch"]
+ )
+ env["ndk_platform"] = "android-21"
- can_vectorize = True
- if env["android_arch"] == "x86":
- env["ARCH"] = "arch-x86"
- env.extra_suffix = ".x86" + env.extra_suffix
- target_subpath = "x86-4.9"
- abi_subpath = "i686-linux-android"
- arch_subpath = "x86"
- env["x86_libtheora_opt_gcc"] = True
- elif env["android_arch"] == "x86_64":
- if get_platform(env["ndk_platform"]) < 21:
- print(
- "WARNING: android_arch=x86_64 is not supported by ndk_platform lower than android-21; setting"
- " ndk_platform=android-21"
- )
- env["ndk_platform"] = "android-21"
- env["ARCH"] = "arch-x86_64"
- env.extra_suffix = ".x86_64" + env.extra_suffix
- target_subpath = "x86_64-4.9"
- abi_subpath = "x86_64-linux-android"
- arch_subpath = "x86_64"
- env["x86_libtheora_opt_gcc"] = True
- elif env["android_arch"] == "armv7":
- env["ARCH"] = "arch-arm"
- target_subpath = "arm-linux-androideabi-4.9"
- abi_subpath = "arm-linux-androideabi"
- arch_subpath = "armeabi-v7a"
+ if env["arch"] == "arm32":
+ target_triple = "armv7a-linux-androideabi"
env.extra_suffix = ".armv7" + env.extra_suffix
- elif env["android_arch"] == "arm64v8":
- if get_platform(env["ndk_platform"]) < 21:
- print(
- "WARNING: android_arch=arm64v8 is not supported by ndk_platform lower than android-21; setting"
- " ndk_platform=android-21"
- )
- env["ndk_platform"] = "android-21"
- env["ARCH"] = "arch-arm64"
- target_subpath = "aarch64-linux-android-4.9"
- abi_subpath = "aarch64-linux-android"
- arch_subpath = "arm64-v8a"
+ elif env["arch"] == "arm64":
+ target_triple = "aarch64-linux-android"
env.extra_suffix = ".armv8" + env.extra_suffix
+ elif env["arch"] == "x86_32":
+ target_triple = "i686-linux-android"
+ env.extra_suffix = ".x86" + env.extra_suffix
+ elif env["arch"] == "x86_64":
+ target_triple = "x86_64-linux-android"
+ env.extra_suffix = ".x86_64" + env.extra_suffix
+
+ target_option = ["-target", target_triple + str(get_min_sdk_version(env["ndk_platform"]))]
+ env.Append(ASFLAGS=[target_option, "-c"])
+ env.Append(CCFLAGS=target_option)
+ env.Append(LINKFLAGS=target_option)
# Build type
@@ -191,27 +121,31 @@ def configure(env):
# `-O2` is more friendly to debuggers than `-O3`, leading to better crash backtraces
# when using `target=release_debug`.
opt = "-O3" if env["target"] == "release" else "-O2"
- env.Append(LINKFLAGS=[opt])
- env.Append(CCFLAGS=[opt, "-fomit-frame-pointer"])
+ env.Append(CCFLAGS=[opt])
elif env["optimize"] == "size": # optimize for size
- env.Append(CCFLAGS=["-Os"])
- env.Append(LINKFLAGS=["-Os"])
-
- env.Append(CPPDEFINES=["NDEBUG"])
- if can_vectorize:
- env.Append(CCFLAGS=["-ftree-vectorize"])
+ env.Append(CCFLAGS=["-Oz"])
elif env["target"] == "debug":
env.Append(LINKFLAGS=["-O0"])
- env.Append(CCFLAGS=["-O0", "-g", "-fno-limit-debug-info"])
- env.Append(CPPDEFINES=["_DEBUG"])
- env.Append(CPPFLAGS=["-UNDEBUG"])
+ env.Append(CCFLAGS=["-O0", "-g"])
+
+ # LTO
+
+ if env["lto"] == "auto": # LTO benefits for Android (size, performance) haven't been clearly established yet.
+ env["lto"] = "none"
+
+ if env["lto"] != "none":
+ if env["lto"] == "thin":
+ env.Append(CCFLAGS=["-flto=thin"])
+ env.Append(LINKFLAGS=["-flto=thin"])
+ else:
+ env.Append(CCFLAGS=["-flto"])
+ env.Append(LINKFLAGS=["-flto"])
# Compiler configuration
env["SHLIBSUFFIX"] = ".so"
if env["PLATFORM"] == "win32":
- env.Tool("gcc")
env.use_windows_spawn_fix()
if sys.platform.startswith("linux"):
@@ -224,32 +158,14 @@ def configure(env):
else:
host_subpath = "windows"
- compiler_path = env["ANDROID_NDK_ROOT"] + "/toolchains/llvm/prebuilt/" + host_subpath + "/bin"
- gcc_toolchain_path = env["ANDROID_NDK_ROOT"] + "/toolchains/" + target_subpath + "/prebuilt/" + host_subpath
- tools_path = gcc_toolchain_path + "/" + abi_subpath + "/bin"
+ toolchain_path = ndk_root + "/toolchains/llvm/prebuilt/" + host_subpath
+ compiler_path = toolchain_path + "/bin"
- # For Clang to find NDK tools in preference of those system-wide
- env.PrependENVPath("PATH", tools_path)
-
- ccache_path = os.environ.get("CCACHE")
- if ccache_path is None:
- env["CC"] = compiler_path + "/clang"
- env["CXX"] = compiler_path + "/clang++"
- else:
- # there aren't any ccache wrappers available for Android,
- # to enable caching we need to prepend the path to the ccache binary
- env["CC"] = ccache_path + " " + compiler_path + "/clang"
- env["CXX"] = ccache_path + " " + compiler_path + "/clang++"
- env["AR"] = tools_path + "/ar"
- env["RANLIB"] = tools_path + "/ranlib"
- env["AS"] = tools_path + "/as"
-
- common_opts = ["-gcc-toolchain", gcc_toolchain_path]
-
- # Compile flags
-
- env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/include"])
- env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++abi/include"])
+ env["CC"] = compiler_path + "/clang"
+ env["CXX"] = compiler_path + "/clang++"
+ env["AR"] = compiler_path + "/llvm-ar"
+ env["RANLIB"] = compiler_path + "/llvm-ranlib"
+ env["AS"] = compiler_path + "/clang"
# Disable exceptions and rtti on non-tools (template) builds
if env["tools"]:
@@ -261,100 +177,31 @@ def configure(env):
# Don't use dynamic_cast, necessary with no-rtti.
env.Append(CPPDEFINES=["NO_SAFE_CAST"])
- lib_sysroot = env["ANDROID_NDK_ROOT"] + "/platforms/" + env["ndk_platform"] + "/" + env["ARCH"]
-
- # Using NDK unified headers (NDK r15+)
- sysroot = env["ANDROID_NDK_ROOT"] + "/sysroot"
- env.Append(CPPFLAGS=["--sysroot=" + sysroot])
- env.Append(CPPFLAGS=["-isystem", sysroot + "/usr/include/" + abi_subpath])
- env.Append(CPPFLAGS=["-isystem", env["ANDROID_NDK_ROOT"] + "/sources/android/support/include"])
- # For unified headers this define has to be set manually
- env.Append(CPPDEFINES=[("__ANDROID_API__", str(get_platform(env["ndk_platform"])))])
-
env.Append(
CCFLAGS=(
- "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden"
- " -fno-strict-aliasing".split()
+ "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split()
)
)
env.Append(CPPDEFINES=["NO_STATVFS", "GLES_ENABLED"])
- if get_platform(env["ndk_platform"]) >= 24:
+ if get_min_sdk_version(env["ndk_platform"]) >= 24:
env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)])
- if env["android_arch"] == "x86":
- target_opts = ["-target", "i686-none-linux-android"]
- # The NDK adds this if targeting API < 21, so we can drop it when Godot targets it at least
+ if env["arch"] == "x86_32":
+ # The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least
env.Append(CCFLAGS=["-mstackrealign"])
-
- elif env["android_arch"] == "x86_64":
- target_opts = ["-target", "x86_64-none-linux-android"]
-
- elif env["android_arch"] == "armv7":
- target_opts = ["-target", "armv7-none-linux-androideabi"]
+ elif env["arch"] == "arm32":
env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split())
env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"])
- # Enable ARM NEON instructions to compile more optimized code.
- env.Append(CCFLAGS=["-mfpu=neon"])
env.Append(CPPDEFINES=["__ARM_NEON__"])
-
- elif env["android_arch"] == "arm64v8":
- target_opts = ["-target", "aarch64-none-linux-android"]
+ elif env["arch"] == "arm64":
env.Append(CCFLAGS=["-mfix-cortex-a53-835769"])
env.Append(CPPDEFINES=["__ARM_ARCH_8A__"])
- env.Append(CCFLAGS=target_opts)
- env.Append(CCFLAGS=common_opts)
-
# Link flags
- ndk_version = get_env_ndk_version(env["ANDROID_NDK_ROOT"])
- if ndk_version != None and LooseVersion(ndk_version) >= LooseVersion("17.1.4828580"):
- env.Append(LINKFLAGS=["-Wl,--exclude-libs,libgcc.a", "-Wl,--exclude-libs,libatomic.a", "-nostdlib++"])
- else:
- env.Append(
- LINKFLAGS=[
- env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/libandroid_support.a"
- ]
- )
- env.Append(LINKFLAGS=["-shared", "--sysroot=" + lib_sysroot, "-Wl,--warn-shared-textrel"])
- env.Append(LIBPATH=[env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/"])
- env.Append(
- LINKFLAGS=[env["ANDROID_NDK_ROOT"] + "/sources/cxx-stl/llvm-libc++/libs/" + arch_subpath + "/libc++_shared.so"]
- )
-
- if env["android_arch"] == "armv7":
- env.Append(LINKFLAGS="-Wl,--fix-cortex-a8".split())
- env.Append(LINKFLAGS="-Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now".split())
- env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so -Wl,--gc-sections".split())
-
- env.Append(LINKFLAGS=target_opts)
- env.Append(LINKFLAGS=common_opts)
-
- env.Append(
- LIBPATH=[
- env["ANDROID_NDK_ROOT"]
- + "/toolchains/"
- + target_subpath
- + "/prebuilt/"
- + host_subpath
- + "/lib/gcc/"
- + abi_subpath
- + "/4.9.x"
- ]
- )
- env.Append(
- LIBPATH=[
- env["ANDROID_NDK_ROOT"]
- + "/toolchains/"
- + target_subpath
- + "/prebuilt/"
- + host_subpath
- + "/"
- + abi_subpath
- + "/lib"
- ]
- )
+ env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split())
+ env.Append(LINKFLAGS="-Wl,-soname,libgodot_android.so")
env.Prepend(CPPPATH=["#platform/android"])
env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED", "NO_FCNTL"])
@@ -364,25 +211,3 @@ def configure(env):
env.Append(CPPDEFINES=["VULKAN_ENABLED"])
if not env["use_volk"]:
env.Append(LIBS=["vulkan"])
-
-
-# Return the project NDK version.
-# This is kept in sync with the value in 'platform/android/java/app/config.gradle'.
-def get_project_ndk_version():
- return "21.4.7075529"
-
-
-# Return NDK version string in source.properties (adapted from the Chromium project).
-def get_env_ndk_version(path):
- if path is None:
- return None
- prop_file_path = os.path.join(path, "source.properties")
- try:
- with open(prop_file_path) as prop_file:
- for line in prop_file:
- key_value = list(map(lambda x: x.strip(), line.split("=")))
- if key_value[0] == "Pkg.Revision":
- return key_value[1]
- except Exception:
- print("Could not read source prop file '%s'" % prop_file_path)
- return None
diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp
index e2b1c757d6..4f1ac16975 100644
--- a/platform/android/dir_access_jandroid.cpp
+++ b/platform/android/dir_access_jandroid.cpp
@@ -31,27 +31,28 @@
#include "dir_access_jandroid.h"
#include "core/string/print_string.h"
-#include "file_access_android.h"
#include "string_android.h"
#include "thread_jandroid.h"
-jobject DirAccessJAndroid::io = nullptr;
+jobject DirAccessJAndroid::dir_access_handler = nullptr;
jclass DirAccessJAndroid::cls = nullptr;
jmethodID DirAccessJAndroid::_dir_open = nullptr;
jmethodID DirAccessJAndroid::_dir_next = nullptr;
jmethodID DirAccessJAndroid::_dir_close = nullptr;
jmethodID DirAccessJAndroid::_dir_is_dir = nullptr;
-
-Ref<DirAccess> DirAccessJAndroid::create_fs() {
- return memnew(DirAccessJAndroid);
-}
+jmethodID DirAccessJAndroid::_dir_exists = nullptr;
+jmethodID DirAccessJAndroid::_file_exists = nullptr;
+jmethodID DirAccessJAndroid::_get_drive_count = nullptr;
+jmethodID DirAccessJAndroid::_get_drive = nullptr;
+jmethodID DirAccessJAndroid::_make_dir = nullptr;
+jmethodID DirAccessJAndroid::_get_space_left = nullptr;
+jmethodID DirAccessJAndroid::_rename = nullptr;
+jmethodID DirAccessJAndroid::_remove = nullptr;
+jmethodID DirAccessJAndroid::_current_is_hidden = nullptr;
Error DirAccessJAndroid::list_dir_begin() {
list_dir_end();
- JNIEnv *env = get_jni_env();
-
- jstring js = env->NewStringUTF(current_dir.utf8().get_data());
- int res = env->CallIntMethod(io, _dir_open, js);
+ int res = dir_open(current_dir);
if (res <= 0) {
return ERR_CANT_OPEN;
}
@@ -63,26 +64,39 @@ Error DirAccessJAndroid::list_dir_begin() {
String DirAccessJAndroid::get_next() {
ERR_FAIL_COND_V(id == 0, "");
+ if (_dir_next) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, "");
+ jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
+ if (!str) {
+ return "";
+ }
- JNIEnv *env = get_jni_env();
- jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id);
- if (!str) {
+ String ret = jstring_to_string((jstring)str, env);
+ env->DeleteLocalRef((jobject)str);
+ return ret;
+ } else {
return "";
}
-
- String ret = jstring_to_string((jstring)str, env);
- env->DeleteLocalRef((jobject)str);
- return ret;
}
bool DirAccessJAndroid::current_is_dir() const {
- JNIEnv *env = get_jni_env();
-
- return env->CallBooleanMethod(io, _dir_is_dir, id);
+ if (_dir_is_dir) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+ return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
+ } else {
+ return false;
+ }
}
bool DirAccessJAndroid::current_is_hidden() const {
- return current != "." && current != ".." && current.begins_with(".");
+ if (_current_is_hidden) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+ return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
+ }
+ return false;
}
void DirAccessJAndroid::list_dir_end() {
@@ -90,152 +104,248 @@ void DirAccessJAndroid::list_dir_end() {
return;
}
- JNIEnv *env = get_jni_env();
- env->CallVoidMethod(io, _dir_close, id);
+ dir_close(id);
id = 0;
}
int DirAccessJAndroid::get_drive_count() {
- return 0;
+ if (_get_drive_count) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+ return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type());
+ } else {
+ return 0;
+ }
}
String DirAccessJAndroid::get_drive(int p_drive) {
- return "";
-}
-
-Error DirAccessJAndroid::change_dir(String p_dir) {
- JNIEnv *env = get_jni_env();
+ if (_get_drive) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, "");
+ jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive);
+ if (!j_drive) {
+ return "";
+ }
- if (p_dir.is_empty() || p_dir == "." || (p_dir == ".." && current_dir.is_empty())) {
- return OK;
+ String drive = jstring_to_string(j_drive, env);
+ env->DeleteLocalRef(j_drive);
+ return drive;
+ } else {
+ return "";
}
+}
- String new_dir;
+String DirAccessJAndroid::_get_root_string() const {
+ if (get_access_type() == ACCESS_FILESYSTEM) {
+ return "/";
+ }
+ return DirAccessUnix::_get_root_string();
+}
- if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) {
- p_dir = p_dir.substr(0, p_dir.length() - 1);
+String DirAccessJAndroid::get_current_dir(bool p_include_drive) const {
+ String base = _get_root_path();
+ String bd = current_dir;
+ if (!base.is_empty()) {
+ bd = current_dir.replace_first(base, "");
}
- if (p_dir.begins_with("/")) {
- new_dir = p_dir.substr(1, p_dir.length());
- } else if (p_dir.begins_with("res://")) {
- new_dir = p_dir.substr(6, p_dir.length());
- } else if (current_dir.is_empty()) {
- new_dir = p_dir;
+ String root_string = _get_root_string();
+ if (bd.begins_with(root_string)) {
+ return bd;
+ } else if (bd.begins_with("/")) {
+ return root_string + bd.substr(1, bd.length());
} else {
- new_dir = current_dir.plus_file(p_dir);
+ return root_string + bd;
}
+}
- //test if newdir exists
- new_dir = new_dir.simplify_path();
+Error DirAccessJAndroid::change_dir(String p_dir) {
+ String new_dir = get_absolute_path(p_dir);
+ if (new_dir == current_dir) {
+ return OK;
+ }
- jstring js = env->NewStringUTF(new_dir.utf8().get_data());
- int res = env->CallIntMethod(io, _dir_open, js);
- env->DeleteLocalRef(js);
- if (res <= 0) {
+ if (!dir_exists(new_dir)) {
return ERR_INVALID_PARAMETER;
}
- env->CallVoidMethod(io, _dir_close, res);
-
current_dir = new_dir;
-
return OK;
}
-String DirAccessJAndroid::get_current_dir(bool p_include_drive) const {
- return "res://" + current_dir;
+String DirAccessJAndroid::get_absolute_path(String p_path) {
+ if (current_dir != "" && p_path == current_dir) {
+ return current_dir;
+ }
+
+ if (p_path.is_relative_path()) {
+ p_path = get_current_dir().path_join(p_path);
+ }
+
+ p_path = fix_path(p_path);
+ p_path = p_path.simplify_path();
+ return p_path;
}
bool DirAccessJAndroid::file_exists(String p_file) {
- String sd;
- if (current_dir.is_empty()) {
- sd = p_file;
+ if (_file_exists) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+
+ String path = get_absolute_path(p_file);
+ jstring j_path = env->NewStringUTF(path.utf8().get_data());
+ bool result = env->CallBooleanMethod(dir_access_handler, _file_exists, get_access_type(), j_path);
+ env->DeleteLocalRef(j_path);
+ return result;
} else {
- sd = current_dir.plus_file(p_file);
+ return false;
}
-
- Ref<FileAccessAndroid> f;
- f.instantiate();
- bool exists = f->file_exists(sd);
-
- return exists;
}
bool DirAccessJAndroid::dir_exists(String p_dir) {
- JNIEnv *env = get_jni_env();
-
- String sd;
-
- if (current_dir.is_empty()) {
- sd = p_dir;
+ if (_dir_exists) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+
+ String path = get_absolute_path(p_dir);
+ jstring j_path = env->NewStringUTF(path.utf8().get_data());
+ bool result = env->CallBooleanMethod(dir_access_handler, _dir_exists, get_access_type(), j_path);
+ env->DeleteLocalRef(j_path);
+ return result;
} else {
- if (p_dir.is_relative_path()) {
- sd = current_dir.plus_file(p_dir);
- } else {
- sd = fix_path(p_dir);
- }
+ return false;
}
+}
- String path = sd.simplify_path();
-
- if (path.begins_with("/")) {
- path = path.substr(1, path.length());
- } else if (path.begins_with("res://")) {
- path = path.substr(6, path.length());
+Error DirAccessJAndroid::make_dir_recursive(String p_dir) {
+ // Check if the directory exists already
+ if (dir_exists(p_dir)) {
+ return ERR_ALREADY_EXISTS;
}
- jstring js = env->NewStringUTF(path.utf8().get_data());
- int res = env->CallIntMethod(io, _dir_open, js);
- env->DeleteLocalRef(js);
- if (res <= 0) {
- return false;
- }
-
- env->CallVoidMethod(io, _dir_close, res);
+ if (_make_dir) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
- return true;
+ String path = get_absolute_path(p_dir);
+ jstring j_dir = env->NewStringUTF(path.utf8().get_data());
+ bool result = env->CallBooleanMethod(dir_access_handler, _make_dir, get_access_type(), j_dir);
+ env->DeleteLocalRef(j_dir);
+ if (result) {
+ return OK;
+ } else {
+ return FAILED;
+ }
+ } else {
+ return ERR_UNCONFIGURED;
+ }
}
Error DirAccessJAndroid::make_dir(String p_dir) {
- ERR_FAIL_V(ERR_UNAVAILABLE);
+ return make_dir_recursive(p_dir);
}
Error DirAccessJAndroid::rename(String p_from, String p_to) {
- ERR_FAIL_V(ERR_UNAVAILABLE);
-}
+ if (_rename) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
-Error DirAccessJAndroid::remove(String p_name) {
- ERR_FAIL_V(ERR_UNAVAILABLE);
+ String from_path = get_absolute_path(p_from);
+ jstring j_from = env->NewStringUTF(from_path.utf8().get_data());
+
+ String to_path = get_absolute_path(p_to);
+ jstring j_to = env->NewStringUTF(to_path.utf8().get_data());
+
+ bool result = env->CallBooleanMethod(dir_access_handler, _rename, get_access_type(), j_from, j_to);
+ env->DeleteLocalRef(j_from);
+ env->DeleteLocalRef(j_to);
+ if (result) {
+ return OK;
+ } else {
+ return FAILED;
+ }
+ } else {
+ return ERR_UNCONFIGURED;
+ }
}
-String DirAccessJAndroid::get_filesystem_type() const {
- return "APK";
+Error DirAccessJAndroid::remove(String p_name) {
+ if (_remove) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
+
+ String path = get_absolute_path(p_name);
+ jstring j_name = env->NewStringUTF(path.utf8().get_data());
+ bool result = env->CallBooleanMethod(dir_access_handler, _remove, get_access_type(), j_name);
+ env->DeleteLocalRef(j_name);
+ if (result) {
+ return OK;
+ } else {
+ return FAILED;
+ }
+ } else {
+ return ERR_UNCONFIGURED;
+ }
}
uint64_t DirAccessJAndroid::get_space_left() {
- return 0;
+ if (_get_space_left) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+ return env->CallLongMethod(dir_access_handler, _get_space_left, get_access_type());
+ } else {
+ return 0;
+ }
}
-void DirAccessJAndroid::setup(jobject p_io) {
+void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
JNIEnv *env = get_jni_env();
- io = p_io;
+ dir_access_handler = env->NewGlobalRef(p_dir_access_handler);
- jclass c = env->GetObjectClass(io);
+ jclass c = env->GetObjectClass(dir_access_handler);
cls = (jclass)env->NewGlobalRef(c);
- _dir_open = env->GetMethodID(cls, "dir_open", "(Ljava/lang/String;)I");
- _dir_next = env->GetMethodID(cls, "dir_next", "(I)Ljava/lang/String;");
- _dir_close = env->GetMethodID(cls, "dir_close", "(I)V");
- _dir_is_dir = env->GetMethodID(cls, "dir_is_dir", "(I)Z");
-
- //(*env)->CallVoidMethod(env,obj,aMethodID, myvar);
+ _dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I");
+ _dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;");
+ _dir_close = env->GetMethodID(cls, "dirClose", "(II)V");
+ _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z");
+ _dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z");
+ _file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z");
+ _get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I");
+ _get_drive = env->GetMethodID(cls, "getDrive", "(II)Ljava/lang/String;");
+ _make_dir = env->GetMethodID(cls, "makeDir", "(ILjava/lang/String;)Z");
+ _get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J");
+ _rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z");
+ _remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z");
+ _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
}
DirAccessJAndroid::DirAccessJAndroid() {
- id = 0;
}
DirAccessJAndroid::~DirAccessJAndroid() {
list_dir_end();
}
+
+int DirAccessJAndroid::dir_open(String p_path) {
+ if (_dir_open) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+
+ String path = get_absolute_path(p_path);
+ jstring js = env->NewStringUTF(path.utf8().get_data());
+ int dirId = env->CallIntMethod(dir_access_handler, _dir_open, get_access_type(), js);
+ env->DeleteLocalRef(js);
+ return dirId;
+ } else {
+ return 0;
+ }
+}
+
+void DirAccessJAndroid::dir_close(int p_id) {
+ if (_dir_close) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
+ }
+}
diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h
index 3d3c9f9223..5c4f1852a9 100644
--- a/platform/android/dir_access_jandroid.h
+++ b/platform/android/dir_access_jandroid.h
@@ -32,60 +32,74 @@
#define DIR_ACCESS_JANDROID_H
#include "core/io/dir_access.h"
+#include "drivers/unix/dir_access_unix.h"
#include "java_godot_lib_jni.h"
#include <stdio.h>
-class DirAccessJAndroid : public DirAccess {
- //AAssetDir* aad;
-
- static jobject io;
+/// Android implementation of the DirAccess interface used to provide access to
+/// ACCESS_FILESYSTEM and ACCESS_RESOURCES directory resources.
+/// The implementation use jni in order to comply with Android filesystem
+/// access restriction.
+class DirAccessJAndroid : public DirAccessUnix {
+ static jobject dir_access_handler;
static jclass cls;
static jmethodID _dir_open;
static jmethodID _dir_next;
static jmethodID _dir_close;
static jmethodID _dir_is_dir;
-
- int id;
-
- String current_dir;
- String current;
-
- static Ref<DirAccess> create_fs();
+ static jmethodID _dir_exists;
+ static jmethodID _file_exists;
+ static jmethodID _get_drive_count;
+ static jmethodID _get_drive;
+ static jmethodID _make_dir;
+ static jmethodID _get_space_left;
+ static jmethodID _rename;
+ static jmethodID _remove;
+ static jmethodID _current_is_hidden;
public:
- virtual Error list_dir_begin(); ///< This starts dir listing
- virtual String get_next();
- virtual bool current_is_dir() const;
- virtual bool current_is_hidden() const;
- virtual void list_dir_end(); ///<
+ virtual Error list_dir_begin() override; ///< This starts dir listing
+ virtual String get_next() override;
+ virtual bool current_is_dir() const override;
+ virtual bool current_is_hidden() const override;
+ virtual void list_dir_end() override; ///<
- virtual int get_drive_count();
- virtual String get_drive(int p_drive);
+ virtual int get_drive_count() override;
+ virtual String get_drive(int p_drive) override;
+ virtual String get_current_dir(bool p_include_drive = true) const override; ///< return current dir location
- virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success
- virtual String get_current_dir(bool p_include_drive = true) const; ///< return current dir location
+ virtual Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success
- virtual bool file_exists(String p_file);
- virtual bool dir_exists(String p_dir);
+ virtual bool file_exists(String p_file) override;
+ virtual bool dir_exists(String p_dir) override;
- virtual Error make_dir(String p_dir);
+ virtual Error make_dir(String p_dir) override;
+ virtual Error make_dir_recursive(String p_dir) override;
- virtual Error rename(String p_from, String p_to);
- virtual Error remove(String p_name);
+ virtual Error rename(String p_from, String p_to) override;
+ virtual Error remove(String p_name) override;
- virtual bool is_link(String p_file) { return false; }
- virtual String read_link(String p_file) { return p_file; }
- virtual Error create_link(String p_source, String p_target) { return FAILED; }
+ virtual bool is_link(String p_file) override { return false; }
+ virtual String read_link(String p_file) override { return p_file; }
+ virtual Error create_link(String p_source, String p_target) override { return FAILED; }
- virtual String get_filesystem_type() const;
+ virtual uint64_t get_space_left() override;
- uint64_t get_space_left();
-
- static void setup(jobject p_io);
+ static void setup(jobject p_dir_access_handler);
DirAccessJAndroid();
~DirAccessJAndroid();
+
+protected:
+ String _get_root_string() const override;
+
+private:
+ int id = 0;
+
+ int dir_open(String p_path);
+ void dir_close(int p_id);
+ String get_absolute_path(String p_path);
};
#endif // DIR_ACCESS_JANDROID_H
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index d414ea5824..d3bce12de1 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -83,7 +83,7 @@ bool DisplayServerAndroid::tts_is_paused() const {
return TTS_Android::is_paused();
}
-Array DisplayServerAndroid::tts_get_voices() const {
+TypedArray<Dictionary> DisplayServerAndroid::tts_get_voices() const {
return TTS_Android::get_voices();
}
@@ -105,7 +105,7 @@ void DisplayServerAndroid::tts_stop() {
void DisplayServerAndroid::clipboard_set(const String &p_text) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
- ERR_FAIL_COND(!godot_java);
+ ERR_FAIL_NULL(godot_java);
if (godot_java->has_set_clipboard()) {
godot_java->set_clipboard(p_text);
@@ -116,7 +116,7 @@ void DisplayServerAndroid::clipboard_set(const String &p_text) {
String DisplayServerAndroid::clipboard_get() const {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
- ERR_FAIL_COND_V(!godot_java, String());
+ ERR_FAIL_NULL_V(godot_java, String());
if (godot_java->has_get_clipboard()) {
return godot_java->get_clipboard();
@@ -127,7 +127,7 @@ String DisplayServerAndroid::clipboard_get() const {
bool DisplayServerAndroid::clipboard_has() const {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
- ERR_FAIL_COND_V(!godot_java, false);
+ ERR_FAIL_NULL_V(godot_java, false);
if (godot_java->has_has_clipboard()) {
return godot_java->has_clipboard();
@@ -136,7 +136,7 @@ bool DisplayServerAndroid::clipboard_has() const {
}
}
-Array DisplayServerAndroid::get_display_cutouts() const {
+TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, Array());
return godot_io_java->get_display_cutouts();
@@ -150,7 +150,7 @@ Rect2i DisplayServerAndroid::get_display_safe_area() const {
void DisplayServerAndroid::screen_set_keep_on(bool p_enable) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
- ERR_FAIL_COND(!godot_java);
+ ERR_FAIL_NULL(godot_java);
godot_java->set_keep_screen_on(p_enable);
keep_screen_on = p_enable;
@@ -162,14 +162,14 @@ bool DisplayServerAndroid::screen_is_kept_on() const {
void DisplayServerAndroid::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND(!godot_io_java);
+ ERR_FAIL_NULL(godot_io_java);
godot_io_java->set_screen_orientation(p_orientation);
}
DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(int p_screen) const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND_V(!godot_io_java, SCREEN_LANDSCAPE);
+ ERR_FAIL_NULL_V(godot_io_java, SCREEN_LANDSCAPE);
const int orientation = godot_io_java->get_screen_orientation();
ERR_FAIL_INDEX_V_MSG(orientation, 7, SCREEN_LANDSCAPE, "Unrecognized screen orientation");
@@ -195,14 +195,14 @@ Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const {
int DisplayServerAndroid::screen_get_dpi(int p_screen) const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND_V(!godot_io_java, 0);
+ ERR_FAIL_NULL_V(godot_io_java, 0);
return godot_io_java->get_screen_dpi();
}
float DisplayServerAndroid::screen_get_scale(int p_screen) const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND_V(!godot_io_java, 1.0f);
+ ERR_FAIL_NULL_V(godot_io_java, 1.0f);
return godot_io_java->get_scaled_density();
}
@@ -221,12 +221,12 @@ bool DisplayServerAndroid::screen_is_touchscreen(int p_screen) const {
return true;
}
-void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) {
+void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, VirtualKeyboardType p_type, int p_max_length, int p_cursor_start, int p_cursor_end) {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND(!godot_io_java);
+ ERR_FAIL_NULL(godot_io_java);
if (godot_io_java->has_vk()) {
- godot_io_java->show_vk(p_existing_text, p_multiline, p_max_length, p_cursor_start, p_cursor_end);
+ godot_io_java->show_vk(p_existing_text, (int)p_type, p_max_length, p_cursor_start, p_cursor_end);
} else {
ERR_PRINT("Virtual keyboard not available");
}
@@ -234,7 +234,7 @@ void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text,
void DisplayServerAndroid::virtual_keyboard_hide() {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND(!godot_io_java);
+ ERR_FAIL_NULL(godot_io_java);
if (godot_io_java->has_vk()) {
godot_io_java->hide_vk();
@@ -245,7 +245,7 @@ void DisplayServerAndroid::virtual_keyboard_hide() {
int DisplayServerAndroid::virtual_keyboard_get_height() const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
- ERR_FAIL_COND_V(!godot_io_java, 0);
+ ERR_FAIL_NULL_V(godot_io_java, 0);
return godot_io_java->get_vk_height();
}
@@ -276,9 +276,9 @@ void DisplayServerAndroid::_window_callback(const Callable &p_callable, const Va
Variant ret;
Callable::CallError ce;
if (p_deferred) {
- p_callable.call((const Variant **)&argp, 1, ret, ce);
+ p_callable.callp((const Variant **)&argp, 1, ret, ce);
} else {
- p_callable.call_deferred((const Variant **)&argp, 1);
+ p_callable.call_deferredp((const Variant **)&argp, 1);
}
}
}
@@ -456,9 +456,9 @@ void DisplayServerAndroid::reset_window() {
#if defined(VULKAN_ENABLED)
if (rendering_driver == "vulkan") {
ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
- ERR_FAIL_COND(!native_window);
+ ERR_FAIL_NULL(native_window);
- ERR_FAIL_COND(!context_vulkan);
+ ERR_FAIL_NULL(context_vulkan);
VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID);
context_vulkan->window_destroy(MAIN_WINDOW_ID);
@@ -482,7 +482,7 @@ void DisplayServerAndroid::notify_surface_changed(int p_width, int p_height) {
Variant ret;
Callable::CallError ce;
- rect_changed_callback.call(reinterpret_cast<const Variant **>(&sizep), 1, ret, ce);
+ rect_changed_callback.callp(reinterpret_cast<const Variant **>(&sizep), 1, ret, ce);
}
DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
@@ -519,7 +519,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
if (rendering_driver == "vulkan") {
ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window();
- ERR_FAIL_COND(!native_window);
+ ERR_FAIL_NULL(native_window);
context_vulkan = memnew(VulkanContextAndroid);
if (context_vulkan->initialize() != OK) {
diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h
index 65bf2ec1a8..6e14ba3e23 100644
--- a/platform/android/display_server_android.h
+++ b/platform/android/display_server_android.h
@@ -93,7 +93,7 @@ public:
virtual bool tts_is_speaking() const override;
virtual bool tts_is_paused() const override;
- virtual Array tts_get_voices() const override;
+ virtual TypedArray<Dictionary> tts_get_voices() const override;
virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override;
virtual void tts_pause() override;
@@ -104,7 +104,7 @@ public:
virtual String clipboard_get() const override;
virtual bool clipboard_has() const override;
- virtual Array get_display_cutouts() const override;
+ virtual TypedArray<Rect2> get_display_cutouts() const override;
virtual Rect2i get_display_safe_area() const override;
virtual void screen_set_keep_on(bool p_enable) override;
@@ -122,7 +122,7 @@ public:
virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
+ virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
virtual void virtual_keyboard_hide() override;
virtual int virtual_keyboard_get_height() const override;
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index aa4b394965..f4c4e985fe 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -30,17 +30,13 @@
#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() {
- String exe_ext;
- if (OS::get_singleton()->get_name() == "Windows") {
- exe_ext = "*.exe";
- }
-
+#ifndef ANDROID_ENABLED
EDITOR_DEF("export/android/android_sdk_path", "");
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/debug_keystore", "");
@@ -52,6 +48,7 @@ void register_android_exporter() {
EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
EDITOR_DEF("export/android/one_click_deploy_clear_previous_install", false);
+#endif
Ref<EditorExportPlatformAndroid> exporter = Ref<EditorExportPlatformAndroid>(memnew(EditorExportPlatformAndroid));
EditorExport::get_singleton()->add_export_platform(exporter);
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index a893571e54..366bd1c48c 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -123,6 +123,7 @@ static const char *android_perms[] = {
"MANAGE_ACCOUNTS",
"MANAGE_APP_TOKENS",
"MANAGE_DOCUMENTS",
+ "MANAGE_EXTERNAL_STORAGE",
"MASTER_CLEAR",
"MEDIA_CONTENT_CONTROL",
"MODIFY_AUDIO_SETTINGS",
@@ -207,9 +208,9 @@ static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash
static const char *GDNATIVE_LIBS_PATH = "res://android/build/libs/gdnativelibs.json";
static const int icon_densities_count = 6;
-static const char *launcher_icon_option = "launcher_icons/main_192x192";
-static const char *launcher_adaptive_icon_foreground_option = "launcher_icons/adaptive_foreground_432x432";
-static const char *launcher_adaptive_icon_background_option = "launcher_icons/adaptive_background_432x432";
+static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192");
+static const char *launcher_adaptive_icon_foreground_option = PNAME("launcher_icons/adaptive_foreground_432x432");
+static const char *launcher_adaptive_icon_background_option = PNAME("launcher_icons/adaptive_background_432x432");
static const LauncherIcon launcher_icons[icon_densities_count] = {
{ "res/mipmap-xxxhdpi-v4/icon.png", 192 },
@@ -245,9 +246,9 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
-static const int DEFAULT_TARGET_SDK_VERSION = 30; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
-const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION);
+static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
+#ifndef ANDROID_ENABLED
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);
@@ -409,8 +410,9 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
List<String> args;
args.push_back("kill-server");
OS::get_singleton()->execute(adb, args);
- };
+ }
}
+#endif
String EditorExportPlatformAndroid::get_project_name(const String &p_name) const {
String aname;
@@ -522,8 +524,8 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package,
bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) {
/*
- * By not compressing files with little or not benefit in doing so,
- * a performance gain is expected attime. Moreover, if the APK is
+ * By not compressing files with little or no benefit in doing so,
+ * a performance gain is expected at runtime. Moreover, if the APK is
* zip-aligned, assets stored as they are can be efficiently read by
* Android by memory-mapping them.
*/
@@ -567,16 +569,15 @@ bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, c
}
zip_fileinfo EditorExportPlatformAndroid::get_zip_fileinfo() {
- OS::Time time = OS::get_singleton()->get_time();
- OS::Date date = OS::get_singleton()->get_date();
+ OS::DateTime dt = OS::get_singleton()->get_datetime();
zip_fileinfo zipfi;
- zipfi.tmz_date.tm_hour = time.hour;
- zipfi.tmz_date.tm_mday = date.day;
- zipfi.tmz_date.tm_min = time.minute;
- zipfi.tmz_date.tm_mon = date.month - 1; // tm_mon is zero indexed
- zipfi.tmz_date.tm_sec = time.second;
- zipfi.tmz_date.tm_year = date.year;
+ zipfi.tmz_date.tm_year = dt.year;
+ zipfi.tmz_date.tm_mon = dt.month - 1; // tm_mon is zero indexed
+ zipfi.tmz_date.tm_mday = dt.day;
+ zipfi.tmz_date.tm_hour = dt.hour;
+ zipfi.tmz_date.tm_min = dt.minute;
+ zipfi.tmz_date.tm_sec = dt.second;
zipfi.dosDate = 0;
zipfi.external_fa = 0;
zipfi.internal_fa = 0;
@@ -622,7 +623,7 @@ Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path
Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_plugins() {
Vector<PluginConfigAndroid> loaded_plugins;
- String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/plugins");
+ String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().path_join("android/plugins");
// Add the prebuilt plugins
loaded_plugins.append_array(PluginConfigAndroid::get_prebuilt_plugins(plugins_dir));
@@ -633,7 +634,7 @@ Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_plugins() {
if (!plugins_filenames.is_empty()) {
Ref<ConfigFile> config_file = memnew(ConfigFile);
for (int i = 0; i < plugins_filenames.size(); i++) {
- PluginConfigAndroid config = PluginConfigAndroid::load_plugin_config(config_file, plugins_dir.plus_file(plugins_filenames[i]));
+ PluginConfigAndroid config = PluginConfigAndroid::load_plugin_config(config_file, plugins_dir.path_join(plugins_filenames[i]));
if (config.valid_config) {
loaded_plugins.push_back(config);
} else {
@@ -694,7 +695,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj
if (abi_index != -1) {
exported = true;
String abi = abis[abi_index];
- String dst_path = String("lib").plus_file(abi).plus_file(p_so.path.get_file());
+ String dst_path = String("lib").path_join(abi).path_join(p_so.path.get_file());
Vector<uint8_t> array = FileAccess::get_file_as_array(p_so.path);
Error store_err = store_in_apk(ed, dst_path, array);
ERR_FAIL_COND_V_MSG(store_err, store_err, "Cannot store in apk file '" + dst_path + "'.");
@@ -735,7 +736,7 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
String type = export_data->debug ? "debug" : "release";
String abi = abis[abi_index];
String filename = p_so.path.get_file();
- String dst_path = base.plus_file(type).plus_file(abi).plus_file(filename);
+ String dst_path = base.path_join(type).path_join(abi).path_join(filename);
Vector<uint8_t> data = FileAccess::get_file_as_array(p_so.path);
print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);
Error err = store_file_at_path(dst_path, data);
@@ -748,10 +749,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
return OK;
}
-bool EditorExportPlatformAndroid::_has_storage_permission(const Vector<String> &p_permissions) {
+bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) {
return p_permissions.find("android.permission.READ_EXTERNAL_STORAGE") != -1 || p_permissions.find("android.permission.WRITE_EXTERNAL_STORAGE") != -1;
}
+bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) {
+ return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1;
+}
+
void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
const char **aperms = android_perms;
while (*aperms) {
@@ -799,7 +804,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
_get_permissions(p_preset, p_give_internet, perms);
for (int i = 0; i < perms.size(); i++) {
String permission = perms.get(i);
- if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || permission == "android.permission.READ_EXTERNAL_STORAGE") {
+ if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {
manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);
} else {
manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission);
@@ -807,8 +812,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
}
manifest_text += _get_xr_features_tag(p_preset);
- manifest_text += _get_instrumentation_tag(p_preset);
- manifest_text += _get_application_tag(p_preset, _has_storage_permission(perms));
+ manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms));
manifest_text += "</manifest>\n";
String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
@@ -834,11 +838,9 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
uint32_t ofs = 8;
uint32_t string_count = 0;
- //uint32_t styles_count = 0;
uint32_t string_flags = 0;
uint32_t string_data_offset = 0;
- //uint32_t styles_offset = 0;
uint32_t string_table_begins = 0;
uint32_t string_table_ends = 0;
Vector<uint8_t> stable_extra;
@@ -868,7 +870,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
Vector<String> perms;
// Write permissions into the perms variable.
_get_permissions(p_preset, p_give_internet, perms);
- bool has_storage_permission = _has_storage_permission(perms);
+ bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);
while (ofs < (uint32_t)p_manifest.size()) {
uint32_t chunk = decode_uint32(&p_manifest[ofs]);
@@ -879,10 +881,8 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
int iofs = ofs + 8;
string_count = decode_uint32(&p_manifest[iofs]);
- // iofs + 4 is `styles_count`.
string_flags = decode_uint32(&p_manifest[iofs + 8]);
string_data_offset = decode_uint32(&p_manifest[iofs + 12]);
- // iofs + 16 is `styles_offset`.
uint32_t st_offset = iofs + 20;
string_table.resize(string_count);
@@ -954,7 +954,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
}
if (tname == "application" && attrname == "requestLegacyExternalStorage") {
- encode_uint32(has_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
+ encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
}
if (tname == "application" && attrname == "allowBackup") {
@@ -969,10 +969,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]);
}
- if (tname == "instrumentation" && attrname == "targetPackage") {
- string_table.write[attr_value] = get_package_name(package_name);
- }
-
if (tname == "activity" && attrname == "screenOrientation") {
encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]);
}
@@ -1672,15 +1668,8 @@ Vector<String> EditorExportPlatformAndroid::get_enabled_abis(const Ref<EditorExp
return enabled_abis;
}
-void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
- String driver = ProjectSettings::get_singleton()->get("rendering/driver/driver_name");
- if (driver == "opengl3") {
- r_features->push_back("etc");
- }
- // FIXME: Review what texture formats are used for Vulkan.
- if (driver == "vulkan") {
- r_features->push_back("etc2");
- }
+void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
+ r_features->push_back("etc2");
Vector<String> abis = get_enabled_abis(p_preset);
for (int i = 0; i < abis.size(); ++i) {
@@ -1691,23 +1680,30 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres
void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) {
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false));
- r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_template/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK));
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_build/use_custom_build"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK));
+ // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).
+ // This implies doing validation that the string is a proper int.
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), ""));
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, "plugins/" + plugins_configs[i].name), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false));
}
plugins_changed.clear();
+ // Android supports multiple architectures in an app bundle, so
+ // we expose each option as a checkbox in the export dialog.
const Vector<String> abis = get_abis();
for (int i = 0; i < abis.size(); ++i) {
const String abi = abis[i];
// All Android devices supporting Vulkan run 64-bit Android,
// so there is usually no point in exporting for 32-bit Android.
const bool is_default = abi == "arm64-v8a";
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + abi), is_default));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default));
}
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), ""));
@@ -1719,8 +1715,6 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
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::INT, "version/min_sdk", PROPERTY_HINT_RANGE, SDK_VERSION_RANGE), DEFAULT_MIN_SDK_VERSION));
- r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/target_sdk", PROPERTY_HINT_RANGE, SDK_VERSION_RANGE), DEFAULT_TARGET_SDK_VERSION));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), ""));
@@ -1758,7 +1752,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
const char **perms = android_perms;
while (*perms) {
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "permissions/" + String(*perms).to_lower()), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false));
perms++;
}
}
@@ -1827,7 +1821,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset,
String can_export_error;
bool can_export_missing_templates;
if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {
- EditorNode::add_io_error(can_export_error);
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);
return ERR_UNCONFIGURED;
}
@@ -1849,13 +1843,14 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset,
p_debug_flags |= DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST;
}
- String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
+ String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
#define CLEANUP_AND_RETURN(m_err) \
{ \
DirAccess::remove_file_or_error(tmp_export_path); \
return m_err; \
- }
+ } \
+ ((void)0)
// Export to temporary APK before sending to device.
Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags);
@@ -1905,7 +1900,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset,
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
print_verbose(output);
if (err || rv != 0) {
- EditorNode::add_io_error(vformat(TTR("Could not install to device: %s"), output));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output));
CLEANUP_AND_RETURN(ERR_CANT_CREATE);
}
@@ -1983,7 +1978,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset,
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
print_verbose(output);
if (err || rv != 0) {
- EditorNode::add_io_error(TTR("Could not execute on device."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device."));
CLEANUP_AND_RETURN(ERR_CANT_CREATE);
}
@@ -2001,7 +1996,7 @@ String EditorExportPlatformAndroid::get_adb_path() {
exe_ext = ".exe";
}
String sdk_path = EditorSettings::get_singleton()->get("export/android/android_sdk_path");
- return sdk_path.plus_file("platform-tools/adb" + exe_ext);
+ return sdk_path.path_join("platform-tools/adb" + exe_ext);
}
String EditorExportPlatformAndroid::get_apksigner_path() {
@@ -2014,7 +2009,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() {
String apksigner_path = "";
Error errn;
- String build_tools_dir = sdk_path.plus_file("build-tools");
+ String build_tools_dir = sdk_path.path_join("build-tools");
Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn);
if (errn != OK) {
print_error("Unable to open Android 'build-tools' directory.");
@@ -2027,7 +2022,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() {
while (!sub_dir.is_empty()) {
if (!sub_dir.begins_with(".") && da->current_is_dir()) {
// Check if the tool is here.
- String tool_path = build_tools_dir.plus_file(sub_dir).plus_file(apksigner_command_name);
+ String tool_path = build_tools_dir.path_join(sub_dir).path_join(apksigner_command_name);
if (FileAccess::exists(tool_path)) {
apksigner_path = tool_path;
break;
@@ -2038,16 +2033,16 @@ String EditorExportPlatformAndroid::get_apksigner_path() {
da->list_dir_end();
if (apksigner_path.is_empty()) {
- EditorNode::get_singleton()->show_warning(TTR("Unable to find the 'apksigner' tool."));
+ print_error("Unable to find the 'apksigner' tool.");
}
return apksigner_path;
}
-bool EditorExportPlatformAndroid::can_export(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) const {
String err;
bool valid = false;
- const bool custom_build_enabled = p_preset->get("custom_template/use_custom_build");
+ const bool custom_build_enabled = p_preset->get("custom_build/use_custom_build");
// Look for export templates (first official, and if defined custom templates).
@@ -2092,7 +2087,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr
valid = installed_android_build_template && !r_missing_templates;
}
- // Validate the rest of the configuration.
+ // Validate the rest of the export configuration.
String dk = p_preset->get("keystore/debug");
String dk_user = p_preset->get("keystore/debug_user");
@@ -2132,7 +2127,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr
} else {
Error errn;
// Check for the platform-tools directory.
- Ref<DirAccess> da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn);
+ Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);
if (errn != OK) {
err += TTR("Invalid Android SDK path in Editor Settings.");
err += TTR("Missing 'platform-tools' directory!");
@@ -2150,7 +2145,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr
}
// Check for the build-tools directory.
- Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn);
+ Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);
if (errn != OK) {
err += TTR("Invalid Android SDK path in Editor Settings.");
err += TTR("Missing 'build-tools' directory!");
@@ -2168,6 +2163,19 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr
}
}
+ if (!err.is_empty()) {
+ r_error = err;
+ }
+
+ return valid;
+}
+
+bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
+ String err;
+ bool valid = true;
+ const bool custom_build_enabled = p_preset->get("custom_build/use_custom_build");
+
+ // Validate the project configuration.
bool apk_expansion = p_preset->get("apk_expansion/enable");
if (apk_expansion) {
@@ -2209,43 +2217,73 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr
if (xr_mode_index != XR_MODE_OPENXR) {
if (hand_tracking > XR_HAND_TRACKING_NONE) {
valid = false;
- err += TTR("\"Hand Tracking\" is only valid when \"Xr Mode\" is \"OpenXR\".");
+ err += TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\".");
err += "\n";
}
if (passthrough_mode > XR_PASSTHROUGH_NONE) {
valid = false;
- err += TTR("\"Passthrough\" is only valid when \"Xr Mode\" is \"OpenXR\".");
+ err += TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\".");
err += "\n";
}
}
- if (int(p_preset->get("custom_template/export_format")) == EXPORT_FORMAT_AAB &&
+ if (int(p_preset->get("custom_build/export_format")) == EXPORT_FORMAT_AAB &&
!custom_build_enabled) {
valid = false;
err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled.");
err += "\n";
}
- // Check the min sdk version
- int min_sdk_version = p_preset->get("version/min_sdk");
- if (min_sdk_version != DEFAULT_MIN_SDK_VERSION && !custom_build_enabled) {
- valid = false;
- err += TTR("Changing the \"Min Sdk\" is only valid when \"Use Custom Build\" is enabled.");
- err += "\n";
+ // Check the min sdk version.
+ String min_sdk_str = p_preset->get("custom_build/min_sdk");
+ int min_sdk_int = DEFAULT_MIN_SDK_VERSION;
+ if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
+ if (!custom_build_enabled) {
+ valid = false;
+ err += TTR("\"Min SDK\" can only be overridden when \"Use Custom Build\" is enabled.");
+ err += "\n";
+ }
+ if (!min_sdk_str.is_valid_int()) {
+ valid = false;
+ err += vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str);
+ err += "\n";
+ } else {
+ min_sdk_int = min_sdk_str.to_int();
+ if (min_sdk_int < DEFAULT_MIN_SDK_VERSION) {
+ valid = false;
+ err += vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), DEFAULT_MIN_SDK_VERSION);
+ err += "\n";
+ }
+ }
}
- // Check the target sdk version
- int target_sdk_version = p_preset->get("version/target_sdk");
- if (target_sdk_version != DEFAULT_TARGET_SDK_VERSION && !custom_build_enabled) {
- valid = false;
- err += TTR("Changing the \"Target Sdk\" is only valid when \"Use Custom Build\" is enabled.");
- err += "\n";
+ // Check the target sdk version.
+ String target_sdk_str = p_preset->get("custom_build/target_sdk");
+ int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
+ if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
+ if (!custom_build_enabled) {
+ valid = false;
+ err += TTR("\"Target SDK\" can only be overridden when \"Use Custom Build\" is enabled.");
+ err += "\n";
+ }
+ if (!target_sdk_str.is_valid_int()) {
+ valid = false;
+ err += vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str);
+ err += "\n";
+ } else {
+ target_sdk_int = target_sdk_str.to_int();
+ if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) {
+ // Warning only, so don't override `valid`.
+ err += vformat(TTR("\"Target SDK\" %d is higher than the default version %d. This may work, but wasn't tested and may be unstable."), target_sdk_int, DEFAULT_TARGET_SDK_VERSION);
+ err += "\n";
+ }
+ }
}
- if (target_sdk_version < min_sdk_version) {
+ if (target_sdk_int < min_sdk_int) {
valid = false;
- err += TTR("\"Target Sdk\" version must be greater or equal to \"Min Sdk\" version.");
+ err += TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version.");
err += "\n";
}
@@ -2264,7 +2302,7 @@ String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorE
int version_code = p_preset->get("version/code");
String package_name = p_preset->get("package/unique_name");
String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb";
- String fullpath = p_path.get_base_dir().plus_file(apk_file_name);
+ String fullpath = p_path.get_base_dir().path_join(apk_file_name);
return fullpath;
}
@@ -2334,7 +2372,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
}
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
- int export_format = int(p_preset->get("custom_template/export_format"));
+ int export_format = int(p_preset->get("custom_build/export_format"));
String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
String release_keystore = p_preset->get("keystore/release");
String release_username = p_preset->get("keystore/release_user");
@@ -2343,7 +2381,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
String apksigner = get_apksigner_path();
print_verbose("Starting signing of the " + export_label + " binary using " + apksigner);
if (!FileAccess::exists(apksigner)) {
- EditorNode::add_io_error(vformat(TTR("'apksigner' could not be found.\nPlease check the command is available in the Android SDK build-tools directory.\nThe resulting %s is unsigned."), export_label));
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label));
return OK;
}
@@ -2376,7 +2414,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
}
if (!FileAccess::exists(keystore)) {
- EditorNode::add_io_error(TTR("Could not find keystore, unable to export."));
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
return ERR_FILE_CANT_OPEN;
}
@@ -2397,10 +2435,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
}
int retval;
output.clear();
- OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+ Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
+ return err;
+ }
print_verbose(output);
if (retval) {
- EditorNode::add_io_error(vformat(TTR("'apksigner' returned with error #%d"), retval));
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval));
return ERR_CANT_CREATE;
}
@@ -2417,10 +2459,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
}
output.clear();
- OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+ err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
+ return err;
+ }
print_verbose(output);
if (retval) {
- EditorNode::add_io_error(vformat(TTR("'apksigner' verification of %s failed."), export_label));
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label));
return ERR_CANT_CREATE;
}
@@ -2482,7 +2528,7 @@ String EditorExportPlatformAndroid::join_list(List<String> parts, const String &
}
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("custom_template/export_format"));
+ int export_format = int(p_preset->get("custom_build/export_format"));
bool should_sign = p_preset->get("package/signed");
return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);
}
@@ -2495,7 +2541,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
- bool use_custom_build = bool(p_preset->get("custom_template/use_custom_build"));
+ bool use_custom_build = bool(p_preset->get("custom_build/use_custom_build"));
bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG);
bool apk_expansion = p_preset->get("apk_expansion/enable");
Vector<String> enabled_abis = get_enabled_abis(p_preset);
@@ -2528,22 +2574,21 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (export_format == EXPORT_FORMAT_AAB) {
if (!p_path.ends_with(".aab")) {
- EditorNode::get_singleton()->show_warning(TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
return ERR_UNCONFIGURED;
}
if (apk_expansion) {
- EditorNode::get_singleton()->show_warning(TTR("APK Expansion not compatible with Android App Bundle."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));
return ERR_UNCONFIGURED;
}
}
if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {
- EditorNode::get_singleton()->show_warning(
- TTR("Invalid filename! Android APK requires the *.apk extension."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));
return ERR_UNCONFIGURED;
}
if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) {
- EditorNode::add_io_error(TTR("Unsupported export format!\n"));
- return ERR_UNCONFIGURED; //TODO: is this the right error?
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));
+ return ERR_UNCONFIGURED;
}
if (use_custom_build) {
@@ -2553,13 +2598,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Checking build version..");
Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ);
if (f.is_null()) {
- EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu."));
return ERR_UNCONFIGURED;
}
String version = f->get_line().strip_edges();
print_verbose("- build version: " + version);
if (version != VERSION_FULL_CONFIG) {
- EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG));
return ERR_UNCONFIGURED;
}
}
@@ -2572,7 +2617,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String project_name = get_project_name(p_preset->get("package/name"));
err = _create_project_name_strings_files(p_preset, project_name); //project name localization.
if (err != OK) {
- EditorNode::add_io_error(TTR("Unable to overwrite res://android/build/res/*.xml files with project name"));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name."));
}
// Copies the project icon files into the appropriate Gradle project directory.
_copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background);
@@ -2589,7 +2634,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
user_data.debug = p_debug;
err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so);
if (err != OK) {
- EditorNode::add_io_error(TTR("Could not export project files to gradle project\n"));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
return err;
}
if (user_data.libs.size() > 0) {
@@ -2601,7 +2646,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Saving apk expansion file..");
err = save_apk_expansion_file(p_preset, p_debug, p_path);
if (err != OK) {
- EditorNode::add_io_error(TTR("Could not write expansion package file!"));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
return err;
}
}
@@ -2618,14 +2663,20 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
build_command = "gradlew";
#endif
- String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build");
- build_command = build_path.plus_file(build_command);
+ String build_path = ProjectSettings::get_singleton()->get_resource_path().path_join("android/build");
+ build_command = build_path.path_join(build_command);
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 min_sdk_version = itos(p_preset->get("version/min_sdk"));
- String target_sdk_version = itos(p_preset->get("version/target_sdk"));
+ String min_sdk_version = p_preset->get("custom_build/min_sdk");
+ if (!min_sdk_version.is_valid_int()) {
+ min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION);
+ }
+ String target_sdk_version = p_preset->get("custom_build/target_sdk");
+ if (!target_sdk_version.is_valid_int()) {
+ target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
+ }
String enabled_abi_string = String("|").join(enabled_abis);
String sign_flag = should_sign ? "true" : "false";
String zipalign_flag = "true";
@@ -2683,10 +2734,10 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
debug_user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user");
}
if (debug_keystore.is_relative_path()) {
- debug_keystore = OS::get_singleton()->get_resource_dir().plus_file(debug_keystore).simplify_path();
+ debug_keystore = OS::get_singleton()->get_resource_dir().path_join(debug_keystore).simplify_path();
}
if (!FileAccess::exists(debug_keystore)) {
- EditorNode::add_io_error(TTR("Could not find keystore, unable to export."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
return ERR_FILE_CANT_OPEN;
}
@@ -2699,10 +2750,10 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String release_username = p_preset->get("keystore/release_user");
String release_password = p_preset->get("keystore/release_password");
if (release_keystore.is_relative_path()) {
- release_keystore = OS::get_singleton()->get_resource_dir().plus_file(release_keystore).simplify_path();
+ release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path();
}
if (!FileAccess::exists(release_keystore)) {
- EditorNode::add_io_error(TTR("Could not find keystore, unable to export."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
return ERR_FILE_CANT_OPEN;
}
@@ -2714,7 +2765,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline);
if (result != 0) {
- EditorNode::get_singleton()->show_warning(TTR("Building of Android project failed, check output for the error.\nAlternatively visit docs.godotengine.org for Android build documentation."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation."));
return ERR_CANT_CREATE;
}
@@ -2734,7 +2785,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String export_filename = p_path.get_file();
String export_path = p_path.get_base_dir();
if (export_path.is_relative_path()) {
- export_path = OS::get_singleton()->get_resource_dir().plus_file(export_path);
+ export_path = OS::get_singleton()->get_resource_dir().path_join(export_path);
}
export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path();
@@ -2744,7 +2795,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args);
if (copy_result != 0) {
- EditorNode::get_singleton()->show_warning(TTR("Unable to copy and rename export file, check gradle project directory for outputs."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs."));
return ERR_CANT_CREATE;
}
@@ -2766,7 +2817,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
src_apk = find_export_template("android_release.apk");
}
if (src_apk.is_empty()) {
- EditorNode::add_io_error(vformat(TTR("Package not found: %s"), src_apk));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk));
return ERR_FILE_NOT_FOUND;
}
}
@@ -2784,7 +2835,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io);
if (!pkg) {
- EditorNode::add_io_error(vformat(TTR("Could not find template APK to export:\n%s"), src_apk));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk));
return ERR_FILE_NOT_FOUND;
}
@@ -2793,13 +2844,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
Ref<FileAccess> io2_fa;
zlib_filefunc_def io2 = zipio_create_io(&io2_fa);
- String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
+ String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
#define CLEANUP_AND_RETURN(m_err) \
{ \
DirAccess::remove_file_or_error(tmp_unaligned_path); \
return m_err; \
- }
+ } \
+ ((void)0)
zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
@@ -2914,7 +2966,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (!invalid_abis.is_empty()) {
String unsupported_arch = String(", ").join(invalid_abis);
- EditorNode::add_io_error(vformat(TTR("Missing libraries in the export template for the selected architectures: %s.\nPlease build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Missing libraries in the export template for the selected architectures: %s. Please build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch));
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
}
@@ -2932,7 +2984,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (apk_expansion) {
err = save_apk_expansion_file(p_preset, p_debug, p_path);
if (err != OK) {
- EditorNode::add_io_error(TTR("Could not write expansion package file!"));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
return err;
}
} else {
@@ -2945,7 +2997,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
if (err != OK) {
unzClose(pkg);
- EditorNode::add_io_error(TTR("Could not export project files"));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files.")));
CLEANUP_AND_RETURN(ERR_SKIP);
}
@@ -2981,7 +3033,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io);
if (!tmp_unaligned) {
- EditorNode::add_io_error(TTR("Could not unzip temporary unaligned APK."));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK.")));
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
}
@@ -3062,29 +3114,28 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
CLEANUP_AND_RETURN(OK);
}
-void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) {
+void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) const {
r_features->push_back("mobile");
r_features->push_back("android");
}
-void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, RBSet<String> &p_features) {
+void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {
}
EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
- Ref<Image> img = memnew(Image(_android_logo));
- logo.instantiate();
- logo->create_from_image(img);
-
- img = Ref<Image>(memnew(Image(_android_run_icon)));
- run_icon.instantiate();
- run_icon->create_from_image(img);
+ logo = ImageTexture::create_from_image(memnew(Image(_android_logo)));
+ run_icon = ImageTexture::create_from_image(memnew(Image(_android_run_icon)));
devices_changed.set();
plugins_changed.set();
+#ifndef ANDROID_ENABLED
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
+#endif
}
EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
+#ifndef ANDROID_ENABLED
quit_request.set();
check_for_changes_thread.wait_to_finish();
+#endif
}
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index c857850007..46012bd46c 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -28,11 +28,14 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
+#ifndef ANDROID_EXPORT_PLUGIN_H
+#define ANDROID_EXPORT_PLUGIN_H
+
#include "godot_plugin_config.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
-#include "editor/editor_export.h"
+#include "editor/export/editor_export_platform.h"
const String SPLASH_CONFIG_XML_CONTENT = R"SPLASH(<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
@@ -77,10 +80,12 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
Vector<Device> devices;
SafeFlag devices_changed;
Mutex device_lock;
+#ifndef ANDROID_ENABLED
Thread check_for_changes_thread;
SafeFlag quit_request;
static void _check_for_changes_poll_thread(void *ud);
+#endif
String get_project_name(const String &p_name) const;
@@ -113,7 +118,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so);
- bool _has_storage_permission(const Vector<String> &p_permissions);
+ bool _has_read_write_storage_permission(const Vector<String> &p_permissions);
+
+ bool _has_manage_external_storage_permission(const Vector<String> &p_permissions);
void _get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions);
@@ -151,7 +158,7 @@ public:
typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key);
public:
- virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override;
+ virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
virtual void get_export_options(List<ExportOption> *r_options) override;
@@ -181,7 +188,8 @@ public:
static String get_apksigner_path();
- virtual bool can_export(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) 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;
@@ -226,11 +234,13 @@ public:
Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags);
- virtual void get_platform_features(List<String> *r_features) override;
+ virtual void get_platform_features(List<String> *r_features) const override;
- virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, RBSet<String> &p_features) override;
+ virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override;
EditorExportPlatformAndroid();
~EditorExportPlatformAndroid();
};
+
+#endif // ANDROID_EXPORT_PLUGIN_H
diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp
index 1210c09666..21580ae907 100644
--- a/platform/android/export/godot_plugin_config.cpp
+++ b/platform/android/export/godot_plugin_config.cpp
@@ -50,7 +50,7 @@ String PluginConfigAndroid::resolve_local_dependency_path(String plugin_config_d
if (dependency_path.is_absolute_path()) {
absolute_path = ProjectSettings::get_singleton()->globalize_path(dependency_path);
} else {
- absolute_path = plugin_config_dir.plus_file(dependency_path);
+ absolute_path = plugin_config_dir.path_join(dependency_path);
}
}
@@ -71,7 +71,6 @@ PluginConfigAndroid PluginConfigAndroid::resolve_prebuilt_plugin(PluginConfigAnd
Vector<PluginConfigAndroid> PluginConfigAndroid::get_prebuilt_plugins(String plugins_base_dir) {
Vector<PluginConfigAndroid> prebuilt_plugins;
- // prebuilt_plugins.push_back(resolve_prebuilt_plugin(MY_PREBUILT_PLUGIN, plugins_base_dir));
return prebuilt_plugins;
}
diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h
index 51cb4dea47..5188f615d4 100644
--- a/platform/android/export/godot_plugin_config.h
+++ b/platform/android/export/godot_plugin_config.h
@@ -103,4 +103,4 @@ struct PluginConfigAndroid {
static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs);
};
-#endif // GODOT_PLUGIN_CONFIG_H
+#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 d9574a1a52..2f53942f76 100644
--- a/platform/android/export/gradle_export_util.cpp
+++ b/platform/android/export/gradle_export_util.cpp
@@ -189,9 +189,7 @@ String bool_to_string(bool v) {
}
String _get_gles_tag() {
- bool min_gles3 = ProjectSettings::get_singleton()->get("rendering/driver/driver_name") == "GLES3" &&
- !ProjectSettings::get_singleton()->get("rendering/driver/fallback_to_gles2");
- return min_gles3 ? " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n" : "";
+ return " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n";
}
String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) {
@@ -232,19 +230,6 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) {
return manifest_xr_features;
}
-String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset) {
- String package_name = p_preset->get("package/unique_name");
- String manifest_instrumentation_text = vformat(
- " <instrumentation\n"
- " tools:node=\"replace\"\n"
- " android:name=\".GodotInstrumentation\"\n"
- " android:icon=\"@mipmap/icon\"\n"
- " android:label=\"@string/godot_project_name_string\"\n"
- " android:targetPackage=\"%s\" />\n",
- package_name);
- return manifest_instrumentation_text;
-}
-
String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) {
int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode"));
bool uses_xr = xr_mode_index == XR_MODE_OPENXR;
@@ -267,7 +252,7 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) {
return manifest_activity_text;
}
-String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission) {
+String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission) {
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(
@@ -284,7 +269,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_
bool_to_string(p_preset->get("user_data_backup/allow")),
bool_to_string(p_preset->get("package/classify_as_game")),
bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
- bool_to_string(p_has_storage_permission));
+ 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;
diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h
index 30a7f04729..232b4458c6 100644
--- a/platform/android/export/gradle_export_util.h
+++ b/platform/android/export/gradle_export_util.h
@@ -28,14 +28,14 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef GODOT_GRADLE_EXPORT_UTIL_H
-#define GODOT_GRADLE_EXPORT_UTIL_H
+#ifndef ANDROID_GRADLE_EXPORT_UTIL_H
+#define ANDROID_GRADLE_EXPORT_UTIL_H
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/io/zip_io.h"
#include "core/os/os.h"
-#include "editor/editor_export.h"
+#include "editor/export/editor_export.h"
const String godot_project_name_xml_string = R"(<?xml version="1.0" encoding="utf-8"?>
<!--WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
@@ -102,10 +102,8 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset);
String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset);
-String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset);
-
String _get_activity_tag(const Ref<EditorExportPreset> &p_preset);
-String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission);
+String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission);
-#endif //GODOT_GRADLE_EXPORT_UTIL_H
+#endif // ANDROID_GRADLE_EXPORT_UTIL_H
diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp
index d7ba31e3c9..d6cd62e9f5 100644
--- a/platform/android/file_access_android.cpp
+++ b/platform/android/file_access_android.cpp
@@ -34,14 +34,20 @@
AAssetManager *FileAccessAndroid::asset_manager = nullptr;
-Ref<FileAccess> FileAccessAndroid::create_android() {
- return memnew(FileAccessAndroid);
+String FileAccessAndroid::get_path() const {
+ return path_src;
}
-Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) {
+String FileAccessAndroid::get_path_absolute() const {
+ return absolute_path;
+}
+
+Error FileAccessAndroid::open_internal(const String &p_path, int p_mode_flags) {
_close();
+ path_src = p_path;
String path = fix_path(p_path).simplify_path();
+ absolute_path = path;
if (path.begins_with("/")) {
path = path.substr(1, path.length());
} else if (path.begins_with("res://")) {
@@ -49,11 +55,11 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) {
}
ERR_FAIL_COND_V(p_mode_flags & FileAccess::WRITE, ERR_UNAVAILABLE); //can't write on android..
- a = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING);
- if (!a) {
+ asset = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING);
+ if (!asset) {
return ERR_CANT_OPEN;
}
- len = AAsset_getLength(a);
+ len = AAsset_getLength(asset);
pos = 0;
eof = false;
@@ -61,21 +67,21 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) {
}
void FileAccessAndroid::_close() {
- if (!a) {
+ if (!asset) {
return;
}
- AAsset_close(a);
- a = nullptr;
+ AAsset_close(asset);
+ asset = nullptr;
}
bool FileAccessAndroid::is_open() const {
- return a != nullptr;
+ return asset != nullptr;
}
void FileAccessAndroid::seek(uint64_t p_position) {
- ERR_FAIL_COND(!a);
+ ERR_FAIL_NULL(asset);
- AAsset_seek(a, p_position, SEEK_SET);
+ AAsset_seek(asset, p_position, SEEK_SET);
pos = p_position;
if (pos > len) {
pos = len;
@@ -86,8 +92,8 @@ void FileAccessAndroid::seek(uint64_t p_position) {
}
void FileAccessAndroid::seek_end(int64_t p_position) {
- ERR_FAIL_COND(!a);
- AAsset_seek(a, p_position, SEEK_END);
+ ERR_FAIL_NULL(asset);
+ AAsset_seek(asset, p_position, SEEK_END);
pos = len + p_position;
}
@@ -110,7 +116,7 @@ uint8_t FileAccessAndroid::get_8() const {
}
uint8_t byte;
- AAsset_read(a, &byte, 1);
+ AAsset_read(asset, &byte, 1);
pos++;
return byte;
}
@@ -118,7 +124,7 @@ uint8_t FileAccessAndroid::get_8() const {
uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
- int r = AAsset_read(a, p_dst, p_length);
+ int r = AAsset_read(asset, p_dst, p_length);
if (pos + p_length > len) {
eof = true;
@@ -134,7 +140,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const
}
Error FileAccessAndroid::get_error() const {
- return eof ? ERR_FILE_EOF : OK; //not sure what else it may happen
+ return eof ? ERR_FILE_EOF : OK; // not sure what else it may happen
}
void FileAccessAndroid::flush() {
diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h
index 33b692da20..55f8fbe0f4 100644
--- a/platform/android/file_access_android.h
+++ b/platform/android/file_access_android.h
@@ -35,45 +35,48 @@
#include <android/asset_manager.h>
#include <android/log.h>
#include <stdio.h>
-//#include <android_native_app_glue.h>
class FileAccessAndroid : public FileAccess {
- static Ref<FileAccess> create_android();
- mutable AAsset *a = nullptr;
+ mutable AAsset *asset = nullptr;
mutable uint64_t len = 0;
mutable uint64_t pos = 0;
mutable bool eof = false;
+ String absolute_path;
+ String path_src;
void _close();
public:
static AAssetManager *asset_manager;
- virtual Error _open(const String &p_path, int p_mode_flags); ///< open a file
- virtual bool is_open() const; ///< true when file is open
+ virtual Error open_internal(const String &p_path, int p_mode_flags) override; // open a file
+ virtual bool is_open() const override; // true when file is open
- virtual void seek(uint64_t p_position); ///< seek to a given position
- virtual void seek_end(int64_t p_position = 0); ///< seek from the end of file
- virtual uint64_t get_position() const; ///< get position in the file
- virtual uint64_t get_length() const; ///< get size of the file
+ /// returns the path for the current open file
+ virtual String get_path() const override;
+ /// returns the absolute path for the current open file
+ virtual String get_path_absolute() const override;
- virtual bool eof_reached() const; ///< reading passed EOF
+ virtual void seek(uint64_t p_position) override; // seek to a given position
+ virtual void seek_end(int64_t p_position = 0) override; // seek from the end of file
+ virtual uint64_t get_position() const override; // get position in the file
+ virtual uint64_t get_length() const override; // get size of the file
- virtual uint8_t get_8() const; ///< get a byte
- virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const;
+ virtual bool eof_reached() const override; // reading passed EOF
- virtual Error get_error() const; ///< get last error
+ virtual uint8_t get_8() const override; // get a byte
+ virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
- virtual void flush();
- virtual void store_8(uint8_t p_dest); ///< store a byte
+ virtual Error get_error() const override; // get last error
- virtual bool file_exists(const String &p_path); ///< return true if a file exists
+ virtual void flush() override;
+ virtual void store_8(uint8_t p_dest) override; // store a byte
- virtual uint64_t _get_modified_time(const String &p_file) { return 0; }
- virtual uint32_t _get_unix_permissions(const String &p_file) { return 0; }
- virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) { return FAILED; }
+ virtual bool file_exists(const String &p_path) override; // return true if a file exists
- //static void make_default();
+ 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; }
~FileAccessAndroid();
};
diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp
new file mode 100644
index 0000000000..c2ee3389ae
--- /dev/null
+++ b/platform/android/file_access_filesystem_jandroid.cpp
@@ -0,0 +1,344 @@
+/*************************************************************************/
+/* file_access_filesystem_jandroid.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#include "file_access_filesystem_jandroid.h"
+
+#include "core/os/os.h"
+#include "core/templates/local_vector.h"
+#include "thread_jandroid.h"
+
+#include <unistd.h>
+
+jobject FileAccessFilesystemJAndroid::file_access_handler = nullptr;
+jclass FileAccessFilesystemJAndroid::cls;
+
+jmethodID FileAccessFilesystemJAndroid::_file_open = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_get_size = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_seek = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_seek_end = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_read = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_tell = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_eof = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_set_eof = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_close = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr;
+
+String FileAccessFilesystemJAndroid::get_path() const {
+ return path_src;
+}
+
+String FileAccessFilesystemJAndroid::get_path_absolute() const {
+ return absolute_path;
+}
+
+Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mode_flags) {
+ if (is_open()) {
+ _close();
+ }
+
+ if (_file_open) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
+
+ String path = fix_path(p_path).simplify_path();
+ jstring js = env->NewStringUTF(path.utf8().get_data());
+ int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags);
+ env->DeleteLocalRef(js);
+
+ if (res <= 0) {
+ switch (res) {
+ case 0:
+ default:
+ return ERR_FILE_CANT_OPEN;
+
+ case -1:
+ return ERR_FILE_NOT_FOUND;
+ }
+ }
+
+ id = res;
+ path_src = p_path;
+ absolute_path = path;
+ return OK;
+ } else {
+ return ERR_UNCONFIGURED;
+ }
+}
+
+void FileAccessFilesystemJAndroid::_close() {
+ if (!is_open()) {
+ return;
+ }
+
+ if (_file_close) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(file_access_handler, _file_close, id);
+ }
+ id = 0;
+}
+
+bool FileAccessFilesystemJAndroid::is_open() const {
+ return id != 0;
+}
+
+void FileAccessFilesystemJAndroid::seek(uint64_t p_position) {
+ if (_file_seek) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+ env->CallVoidMethod(file_access_handler, _file_seek, id, p_position);
+ }
+}
+
+void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) {
+ if (_file_seek_end) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+ env->CallVoidMethod(file_access_handler, _file_seek_end, id, p_position);
+ }
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_position() const {
+ if (_file_tell) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ return env->CallLongMethod(file_access_handler, _file_tell, id);
+ } else {
+ return 0;
+ }
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_length() const {
+ if (_file_get_size) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ return env->CallLongMethod(file_access_handler, _file_get_size, id);
+ } else {
+ return 0;
+ }
+}
+
+bool FileAccessFilesystemJAndroid::eof_reached() const {
+ if (_file_eof) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+ ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use.");
+ return env->CallBooleanMethod(file_access_handler, _file_eof, id);
+ } else {
+ return false;
+ }
+}
+
+void FileAccessFilesystemJAndroid::_set_eof(bool eof) {
+ if (_file_set_eof) {
+ ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ env->CallVoidMethod(file_access_handler, _file_set_eof, id, eof);
+ }
+}
+
+uint8_t FileAccessFilesystemJAndroid::get_8() const {
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ uint8_t byte;
+ get_buffer(&byte, 1);
+ return byte;
+}
+
+String FileAccessFilesystemJAndroid::get_line() const {
+ ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use.");
+
+ const size_t buffer_size_limit = 2048;
+ const uint64_t file_size = get_length();
+ const uint64_t start_position = get_position();
+
+ String result;
+ LocalVector<uint8_t> line_buffer;
+ size_t current_buffer_size = 0;
+ uint64_t line_buffer_position = 0;
+
+ while (true) {
+ size_t line_buffer_size = MIN(buffer_size_limit, file_size - get_position());
+ if (line_buffer_size <= 0) {
+ const_cast<FileAccessFilesystemJAndroid *>(this)->_set_eof(true);
+ break;
+ }
+
+ current_buffer_size += line_buffer_size;
+ line_buffer.resize(current_buffer_size);
+
+ uint64_t bytes_read = get_buffer(&line_buffer[line_buffer_position], current_buffer_size - line_buffer_position);
+ if (bytes_read <= 0) {
+ break;
+ }
+
+ for (; bytes_read > 0; line_buffer_position++, bytes_read--) {
+ uint8_t elem = line_buffer[line_buffer_position];
+ if (elem == '\n' || elem == '\0') {
+ // Found the end of the line
+ const_cast<FileAccessFilesystemJAndroid *>(this)->seek(start_position + line_buffer_position + 1);
+ if (result.parse_utf8((const char *)line_buffer.ptr(), line_buffer_position, true)) {
+ return String();
+ }
+ return result;
+ }
+ }
+ }
+
+ if (result.parse_utf8((const char *)line_buffer.ptr(), line_buffer_position, true)) {
+ return String();
+ }
+ return result;
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
+ if (_file_read) {
+ ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+ if (p_length == 0) {
+ return 0;
+ }
+
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, 0);
+
+ jobject j_buffer = env->NewDirectByteBuffer(p_dst, p_length);
+ int length = env->CallIntMethod(file_access_handler, _file_read, id, j_buffer);
+ env->DeleteLocalRef(j_buffer);
+ return length;
+ } else {
+ return 0;
+ }
+}
+
+void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) {
+ store_buffer(&p_dest, 1);
+}
+
+void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
+ if (_file_write) {
+ ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+ if (p_length == 0) {
+ return;
+ }
+
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+
+ jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length);
+ env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer);
+ env->DeleteLocalRef(j_buffer);
+ }
+}
+
+Error FileAccessFilesystemJAndroid::get_error() const {
+ if (eof_reached()) {
+ return ERR_FILE_EOF;
+ }
+ return OK;
+}
+
+void FileAccessFilesystemJAndroid::flush() {
+ if (_file_flush) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+ env->CallVoidMethod(file_access_handler, _file_flush, id);
+ }
+}
+
+bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) {
+ if (_file_exists) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+
+ String path = fix_path(p_path).simplify_path();
+ jstring js = env->NewStringUTF(path.utf8().get_data());
+ bool result = env->CallBooleanMethod(file_access_handler, _file_exists, js);
+ env->DeleteLocalRef(js);
+ return result;
+ } else {
+ return false;
+ }
+}
+
+uint64_t FileAccessFilesystemJAndroid::_get_modified_time(const String &p_file) {
+ if (_file_last_modified) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_COND_V(env == nullptr, false);
+
+ String path = fix_path(p_file).simplify_path();
+ jstring js = env->NewStringUTF(path.utf8().get_data());
+ uint64_t result = env->CallLongMethod(file_access_handler, _file_last_modified, js);
+ env->DeleteLocalRef(js);
+ return result;
+ } else {
+ return 0;
+ }
+}
+
+void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) {
+ JNIEnv *env = get_jni_env();
+ file_access_handler = env->NewGlobalRef(p_file_access_handler);
+
+ jclass c = env->GetObjectClass(file_access_handler);
+ cls = (jclass)env->NewGlobalRef(c);
+
+ _file_open = env->GetMethodID(cls, "fileOpen", "(Ljava/lang/String;I)I");
+ _file_get_size = env->GetMethodID(cls, "fileGetSize", "(I)J");
+ _file_tell = env->GetMethodID(cls, "fileGetPosition", "(I)J");
+ _file_eof = env->GetMethodID(cls, "isFileEof", "(I)Z");
+ _file_set_eof = env->GetMethodID(cls, "setFileEof", "(IZ)V");
+ _file_seek = env->GetMethodID(cls, "fileSeek", "(IJ)V");
+ _file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V");
+ _file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I");
+ _file_close = env->GetMethodID(cls, "fileClose", "(I)V");
+ _file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)V");
+ _file_flush = env->GetMethodID(cls, "fileFlush", "(I)V");
+ _file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z");
+ _file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J");
+}
+
+FileAccessFilesystemJAndroid::FileAccessFilesystemJAndroid() {
+ id = 0;
+}
+
+FileAccessFilesystemJAndroid::~FileAccessFilesystemJAndroid() {
+ if (is_open()) {
+ _close();
+ }
+}
diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h
new file mode 100644
index 0000000000..815ab36516
--- /dev/null
+++ b/platform/android/file_access_filesystem_jandroid.h
@@ -0,0 +1,100 @@
+/*************************************************************************/
+/* file_access_filesystem_jandroid.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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. */
+/*************************************************************************/
+
+#ifndef FILE_ACCESS_FILESYSTEM_JANDROID_H
+#define FILE_ACCESS_FILESYSTEM_JANDROID_H
+
+#include "core/io/file_access.h"
+#include "java_godot_lib_jni.h"
+
+class FileAccessFilesystemJAndroid : public FileAccess {
+ static jobject file_access_handler;
+ static jclass cls;
+
+ static jmethodID _file_open;
+ static jmethodID _file_get_size;
+ static jmethodID _file_seek;
+ static jmethodID _file_seek_end;
+ static jmethodID _file_tell;
+ static jmethodID _file_eof;
+ static jmethodID _file_set_eof;
+ static jmethodID _file_read;
+ static jmethodID _file_write;
+ static jmethodID _file_flush;
+ static jmethodID _file_close;
+ static jmethodID _file_exists;
+ static jmethodID _file_last_modified;
+
+ int id;
+ String absolute_path;
+ String path_src;
+
+ void _close(); ///< close a file
+ void _set_eof(bool eof);
+
+public:
+ virtual Error open_internal(const String &p_path, int p_mode_flags) override; ///< open a file
+ virtual bool is_open() const override; ///< true when file is open
+
+ /// returns the path for the current open file
+ virtual String get_path() const override;
+ /// returns the absolute path for the current open file
+ virtual String get_path_absolute() const override;
+
+ virtual void seek(uint64_t p_position) override; ///< seek to a given position
+ virtual void seek_end(int64_t p_position = 0) override; ///< seek from the end of file
+ virtual uint64_t get_position() const override; ///< get position in the file
+ virtual uint64_t get_length() const override; ///< get size of the file
+
+ virtual bool eof_reached() const override; ///< reading passed EOF
+
+ virtual uint8_t get_8() const override; ///< get a byte
+ virtual String get_line() const override; ///< get a line
+ virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
+
+ virtual Error get_error() const override; ///< get last error
+
+ virtual void flush() override;
+ virtual void store_8(uint8_t p_dest) override; ///< store a byte
+ virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override;
+
+ virtual bool file_exists(const String &p_path) override; ///< return true if a file exists
+
+ static void setup(jobject p_file_access_handler);
+
+ 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; }
+
+ FileAccessFilesystemJAndroid();
+ ~FileAccessFilesystemJAndroid();
+};
+
+#endif // FILE_ACCESS_FILESYSTEM_JANDROID_H
diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml
index c98e8f1d55..2d4c4763a2 100644
--- a/platform/android/java/app/AndroidManifest.xml
+++ b/platform/android/java/app/AndroidManifest.xml
@@ -59,6 +59,7 @@
android:theme="@style/GodotAppSplashTheme"
android:launchMode="singleTask"
android:excludeFromRecents="false"
+ android:exported="true"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:resizeableActivity="false"
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index b6303d1bc9..63b10e62b1 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -1,6 +1,4 @@
// Gradle build config for Godot Engine's Android port.
-apply from: 'config.gradle'
-
buildscript {
apply from: 'config.gradle'
@@ -14,7 +12,12 @@ buildscript {
}
}
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+apply from: 'config.gradle'
allprojects {
repositories {
@@ -79,6 +82,10 @@ android {
targetCompatibility versions.javaVersion
}
+ kotlinOptions {
+ jvmTarget = versions.javaVersion
+ }
+
assetPacks = [":assetPacks:installTime"]
defaultConfig {
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 1b2976e715..0346625e4b 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -1,20 +1,21 @@
ext.versions = [
androidGradlePlugin: '7.0.3',
- compileSdk : 30,
+ compileSdk : 32,
minSdk : 19, // Also update 'platform/android/java/lib/AndroidManifest.xml#minSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
- targetSdk : 30, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
- buildTools : '30.0.3',
- kotlinVersion : '1.6.10',
+ targetSdk : 32, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
+ buildTools : '32.0.0',
+ kotlinVersion : '1.6.21',
fragmentVersion : '1.3.6',
+ nexusPublishVersion: '1.1.0',
javaVersion : 11,
- ndkVersion : '21.4.7075529' // Also update 'platform/android/detect.py#get_project_ndk_version()' when this is updated.
+ ndkVersion : '23.2.8568313' // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
]
ext.libraries = [
androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin",
kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion",
- kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion",
+ kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion",
androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion",
]
@@ -126,16 +127,36 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
if (requiredKeys.empty) {
libraryVersionName = map.values().join(".")
try {
+ if (map.containsKey("status")) {
+ int statusCode = 0
+ String statusValue = map["status"]
+ if (statusValue == null) {
+ statusCode = 0
+ } else if (statusValue.startsWith("alpha")) {
+ statusCode = 1
+ } else if (statusValue.startsWith("beta")) {
+ statusCode = 2
+ } else if (statusValue.startsWith("rc")) {
+ statusCode = 3
+ } else if (statusValue.startsWith("stable")) {
+ statusCode = 4
+ } else {
+ statusCode = 0
+ }
+
+ libraryVersionCode = statusCode
+ }
+
if (map.containsKey("patch")) {
- libraryVersionCode = Integer.parseInt(map["patch"])
+ libraryVersionCode += Integer.parseInt(map["patch"]) * 10
}
if (map.containsKey("minor")) {
- libraryVersionCode += (Integer.parseInt(map["minor"]) * 100)
+ libraryVersionCode += (Integer.parseInt(map["minor"]) * 1000)
}
if (map.containsKey("major")) {
- libraryVersionCode += (Integer.parseInt(map["major"]) * 10000)
+ libraryVersionCode += (Integer.parseInt(map["major"]) * 100000)
}
} catch (NumberFormatException ignore) {
libraryVersionCode = 1
diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml
index 99f723f5ba..d64b50ca45 100644
--- a/platform/android/java/app/res/values/themes.xml
+++ b/platform/android/java/app/res/values/themes.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/>
+ <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/>
- <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme">
+ <style name="GodotAppSplashTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen">
<item name="android:windowBackground">@drawable/splash_drawable</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle
index e38d7b2ba6..ba53aefe7f 100644
--- a/platform/android/java/app/settings.gradle
+++ b/platform/android/java/app/settings.gradle
@@ -1,2 +1,15 @@
// This is the root directory of the Godot custom build.
+pluginManagement {
+ apply from: 'config.gradle'
+
+ plugins {
+ id 'com.android.application' version versions.androidGradlePlugin
+ id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
+ }
+ repositories {
+ gradlePluginPortal()
+ google()
+ }
+}
+
include ':assetPacks:installTime'
diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle
index e16ca65df5..81c7130c03 100644
--- a/platform/android/java/build.gradle
+++ b/platform/android/java/build.gradle
@@ -1,7 +1,3 @@
-apply plugin: 'io.github.gradle-nexus.publish-plugin'
-apply from: 'app/config.gradle'
-apply from: 'scripts/publish-root.gradle'
-
buildscript {
apply from: 'app/config.gradle'
@@ -17,6 +13,13 @@ buildscript {
}
}
+plugins {
+ id 'io.github.gradle-nexus.publish-plugin'
+}
+
+apply from: 'app/config.gradle'
+apply from: 'scripts/publish-root.gradle'
+
allprojects {
repositories {
google()
@@ -25,7 +28,7 @@ allprojects {
}
ext {
- supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"]
+ supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"]
supportedFlavors = ["editor", "template"]
@@ -34,7 +37,7 @@ ext {
// If building manually on the command line, it's recommended to use the
// `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
// The {selectedAbis} values must be from the {supportedAbis} values.
- selectedAbis = ["arm64v8"]
+ selectedAbis = ["arm64"]
}
def rootDir = "../../.."
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 3312f61ad3..9152492e9d 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -1,10 +1,34 @@
// Gradle build config for Godot Engine's Android port.
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
dependencies {
implementation libraries.kotlinStdLib
implementation libraries.androidxFragment
implementation project(":lib")
+
+ implementation "androidx.window:window:1.0.0"
+}
+
+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
+ // Value by which the Godot version code should be offset by to make room for the build number
+ editorBuildNumberOffset = 100
+}
+
+def generateVersionCode() {
+ int libraryVersionCode = getGodotLibraryVersionCode()
+ return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber
+}
+
+def generateVersionName() {
+ String libraryVersionName = getGodotLibraryVersionName()
+ return libraryVersionName + ".$editorBuildNumber"
}
android {
@@ -15,11 +39,10 @@ android {
defaultConfig {
// The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device
applicationId "org.godotengine.editor.v4"
- versionCode getGodotLibraryVersionCode()
- versionName getGodotLibraryVersionName()
+ versionCode generateVersionCode()
+ versionName generateVersionName()
minSdkVersion versions.minSdk
- //noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted
- targetSdkVersion 29 // versions.targetSdk
+ targetSdkVersion versions.targetSdk
missingDimensionStrategy 'products', 'editor'
}
@@ -29,6 +52,10 @@ android {
targetCompatibility versions.javaVersion
}
+ kotlinOptions {
+ jvmTarget = versions.javaVersion
+ }
+
buildTypes {
dev {
initWith debug
diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml
index bae075d929..6aa5f06f31 100644
--- a/platform/android/java/editor/src/main/AndroidManifest.xml
+++ b/platform/android/java/editor/src/main/AndroidManifest.xml
@@ -7,15 +7,19 @@
<supports-screens
android:largeScreens="true"
android:normalScreens="true"
- android:smallScreens="true"
+ android:smallScreens="false"
android:xlargeScreens="true" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ android:maxSdkVersion="29"/>
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
+ android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.INTERNET" />
<application
@@ -34,6 +38,9 @@
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
android:process=":GodotProjectManager">
+ <layout android:defaultHeight="@dimen/editor_default_window_height"
+ android:defaultWidth="@dimen/editor_default_window_width" />
+
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -46,7 +53,10 @@
android:process=":GodotEditor"
android:launchMode="singleTask"
android:screenOrientation="userLandscape"
+ android:exported="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <layout android:defaultHeight="@dimen/editor_default_window_height"
+ android:defaultWidth="@dimen/editor_default_window_width" />
</activity>
<activity
@@ -55,8 +65,11 @@
android:label="@string/godot_project_name_string"
android:process=":GodotGame"
android:launchMode="singleTask"
+ android:exported="false"
android:screenOrientation="userLandscape"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <layout android:defaultHeight="@dimen/editor_default_window_height"
+ android:defaultWidth="@dimen/editor_default_window_width" />
</activity>
</application>
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java
deleted file mode 100644
index 8a6bf88267..0000000000
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*************************************************************************/
-/* GodotEditor.java */
-/*************************************************************************/
-/* This file is part of: */
-/* GODOT ENGINE */
-/* https://godotengine.org */
-/*************************************************************************/
-/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
-/* */
-/* 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.editor;
-
-import org.godotengine.godot.FullScreenGodotApp;
-import org.godotengine.godot.utils.PermissionsUtil;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Debug;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Base class for the Godot Android Editor activities.
- *
- * This provides the basic templates for the activities making up this application.
- * Each derived activity runs in its own process, which enable up to have several instances of
- * the Godot engine up and running at the same time.
- *
- * It also plays the role of the primary editor window.
- */
-public class GodotEditor extends FullScreenGodotApp {
- private static final boolean WAIT_FOR_DEBUGGER = false;
- private static final String COMMAND_LINE_PARAMS = "command_line_params";
-
- private static final String EDITOR_ARG = "--editor";
- private static final String PROJECT_MANAGER_ARG = "--project-manager";
-
- private final List<String> commandLineParams = new ArrayList<>();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- PermissionsUtil.requestManifestPermissions(this);
-
- String[] params = getIntent().getStringArrayExtra(COMMAND_LINE_PARAMS);
- updateCommandLineParams(params);
-
- if (BuildConfig.BUILD_TYPE.equals("debug") && WAIT_FOR_DEBUGGER) {
- Debug.waitForDebugger();
- }
- super.onCreate(savedInstanceState);
- }
-
- private void updateCommandLineParams(@Nullable String[] args) {
- // Update the list of command line params with the new args
- commandLineParams.clear();
- if (args != null && args.length > 0) {
- commandLineParams.addAll(Arrays.asList(args));
- }
- }
-
- @Override
- public List<String> getCommandLine() {
- return commandLineParams;
- }
-
- @Override
- public void onNewGodotInstanceRequested(String[] args) {
- // Parse the arguments to figure out which activity to start.
- Class<?> targetClass = GodotGame.class;
- for (String arg : args) {
- if (EDITOR_ARG.equals(arg)) {
- targetClass = GodotEditor.class;
- break;
- }
-
- if (PROJECT_MANAGER_ARG.equals(arg)) {
- targetClass = GodotProjectManager.class;
- break;
- }
- }
-
- // Launch a new activity
- Intent newInstance = new Intent(this, targetClass).putExtra(COMMAND_LINE_PARAMS, args);
- startActivity(newInstance);
- }
-
- @Override
- public void setRequestedOrientation(int requestedOrientation) {
- if (!overrideOrientationRequest()) {
- super.setRequestedOrientation(requestedOrientation);
- }
- }
-
- /**
- * The Godot Android Editor sets its own orientation via its AndroidManifest
- */
- protected boolean overrideOrientationRequest() {
- return true;
- }
-}
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
new file mode 100644
index 0000000000..489a81fc1a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -0,0 +1,212 @@
+/*************************************************************************/
+/* GodotEditor.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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.editor
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Debug
+import android.os.Environment
+import android.widget.Toast
+import androidx.window.layout.WindowMetricsCalculator
+import org.godotengine.godot.FullScreenGodotApp
+import org.godotengine.godot.utils.PermissionsUtil
+import java.util.*
+import kotlin.math.min
+
+/**
+ * Base class for the Godot Android Editor activities.
+ *
+ * This provides the basic templates for the activities making up this application.
+ * Each derived activity runs in its own process, which enable up to have several instances of
+ * the Godot engine up and running at the same time.
+ *
+ * It also plays the role of the primary editor window.
+ */
+open class GodotEditor : FullScreenGodotApp() {
+
+ companion object {
+ private const val WAIT_FOR_DEBUGGER = false
+
+ private const val COMMAND_LINE_PARAMS = "command_line_params"
+
+ private const val EDITOR_ARG = "--editor"
+ private const val PROJECT_MANAGER_ARG = "--project-manager"
+ }
+
+ private val commandLineParams = ArrayList<String>()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ PermissionsUtil.requestManifestPermissions(this)
+
+ val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS)
+ updateCommandLineParams(params)
+
+ if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
+ Debug.waitForDebugger()
+ }
+
+ super.onCreate(savedInstanceState)
+
+ // Enable long press, panning and scaling gestures
+ godotFragment?.renderView?.inputHandler?.apply {
+ enableLongPress(enableLongPressGestures())
+ enablePanningAndScalingGestures(enablePanAndScaleGestures())
+ }
+ }
+
+ private fun updateCommandLineParams(args: Array<String>?) {
+ // Update the list of command line params with the new args
+ commandLineParams.clear()
+ if (args != null && args.isNotEmpty()) {
+ commandLineParams.addAll(listOf(*args))
+ }
+ }
+
+ override fun getCommandLine() = commandLineParams
+
+ override fun onNewGodotInstanceRequested(args: Array<String>) {
+ // Parse the arguments to figure out which activity to start.
+ var targetClass: Class<*> = GodotGame::class.java
+
+ // 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)
+
+ for (arg in args) {
+ if (EDITOR_ARG == arg) {
+ targetClass = GodotEditor::class.java
+ launchAdjacent = false
+ break
+ }
+
+ if (PROJECT_MANAGER_ARG == arg) {
+ targetClass = GodotProjectManager::class.java
+ launchAdjacent = false
+ break
+ }
+ }
+
+ // Launch a new activity
+ val newInstance = Intent(this, targetClass)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(COMMAND_LINE_PARAMS, args)
+ if (launchAdjacent) {
+ newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
+ }
+ startActivity(newInstance)
+ }
+
+ // Get the screen's density scale
+ protected val isLargeScreen: Boolean
+ // Get the minimum window size // Correspond to the EXPANDED window size class.
+ get() {
+ val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this)
+
+ // Get the screen's density scale
+ val scale = resources.displayMetrics.density
+
+ // Get the minimum window size
+ val minSize = min(metrics.bounds.width(), metrics.bounds.height()).toFloat()
+ val minSizeDp = minSize / scale
+ return minSizeDp >= 840f // Correspond to the EXPANDED window size class.
+ }
+
+ override fun setRequestedOrientation(requestedOrientation: Int) {
+ if (!overrideOrientationRequest()) {
+ super.setRequestedOrientation(requestedOrientation)
+ }
+ }
+
+ /**
+ * The Godot Android Editor sets its own orientation via its AndroidManifest
+ */
+ protected open fun overrideOrientationRequest() = true
+
+ /**
+ * Enable long press gestures for the Godot Android editor.
+ */
+ protected open fun enableLongPressGestures() = true
+
+ /**
+ * Enable pan and scale gestures for the Godot Android editor.
+ */
+ protected open fun enablePanAndScaleGestures() = true
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ // Check if we got the MANAGE_EXTERNAL_STORAGE permission
+ if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (!Environment.isExternalStorageManager()) {
+ Toast.makeText(
+ this,
+ R.string.denied_storage_permission_error_msg,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array<String?>,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ // Check if we got access to the necessary storage permissions
+ if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ var hasReadAccess = false
+ var hasWriteAccess = false
+ for (i in permissions.indices) {
+ if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ hasReadAccess = true
+ }
+ if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ hasWriteAccess = true
+ }
+ }
+ if (!hasReadAccess || !hasWriteAccess) {
+ Toast.makeText(
+ this,
+ R.string.denied_storage_permission_error_msg,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
index 12766775a8..b9536a7066 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
@@ -1,5 +1,5 @@
/*************************************************************************/
-/* GodotGame.java */
+/* GodotGame.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,13 +28,15 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-package org.godotengine.editor;
+package org.godotengine.editor
/**
* Drives the 'run project' window of the Godot Editor.
*/
-public class GodotGame extends GodotEditor {
- protected boolean overrideOrientationRequest() {
- return false;
- }
+class GodotGame : GodotEditor() {
+ override fun overrideOrientationRequest() = false
+
+ override fun enableLongPressGestures() = false
+
+ override fun enablePanAndScaleGestures() = false
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt
index d30f66bb8c..bcf4659603 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt
@@ -1,5 +1,5 @@
/*************************************************************************/
-/* GodotProjectManager.java */
+/* GodotProjectManager.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,14 +28,13 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-package org.godotengine.editor;
+package org.godotengine.editor
/**
* Launcher activity for the Godot Android Editor.
*
* It presents the user with the project manager interface.
* Upon selection of a project, this activity (via its parent logic) starts the
- * {@link GodotEditor} activity.
+ * [GodotEditor] activity.
*/
-public class GodotProjectManager extends GodotEditor {
-}
+class GodotProjectManager : GodotEditor()
diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..03fb6184d2
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="editor_default_window_height">600dp</dimen>
+ <dimen name="editor_default_window_width">800dp</dimen>
+</resources>
diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml
index e8ce34f34d..837a5d62e1 100644
--- a/platform/android/java/editor/src/main/res/values/strings.xml
+++ b/platform/android/java/editor/src/main/res/values/strings.xml
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="godot_editor_name_string">Godot Editor 4.x</string>
+
+ <string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
</resources>
diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml
index 2de62271c4..79b5aadf2a 100644
--- a/platform/android/java/lib/AndroidManifest.xml
+++ b/platform/android/java/lib/AndroidManifest.xml
@@ -5,7 +5,7 @@
android:versionName="1.0">
<!-- Should match the mindSdk and targetSdk values in platform/android/java/app/config.gradle -->
- <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="30" />
+ <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="32" />
<application>
@@ -16,12 +16,13 @@
<service android:name=".GodotDownloaderService" />
- </application>
+ <activity
+ android:name=".utils.ProcessPhoenix"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar"
+ android:process=":phoenix"
+ android:exported="false"
+ />
- <instrumentation
- android:icon="@mipmap/icon"
- android:label="@string/godot_project_name_string"
- android:name="org.godotengine.godot.GodotInstrumentation"
- android:targetPackage="org.godotengine.godot" />
+ </application>
</manifest>
diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle
index c806de1ded..318ae1143f 100644
--- a/platform/android/java/lib/build.gradle
+++ b/platform/android/java/lib/build.gradle
@@ -1,5 +1,7 @@
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
ext {
PUBLISH_VERSION = getGodotPublishVersion()
@@ -34,6 +36,10 @@ android {
targetCompatibility versions.javaVersion
}
+ kotlinOptions {
+ jvmTarget = versions.javaVersion
+ }
+
buildTypes {
dev {
initWith debug
@@ -153,7 +159,7 @@ android {
def taskName = getSconsTaskName(flavorName, buildType, selectedAbi)
tasks.create(name: taskName, type: Exec) {
executable sconsExecutableFile.absolutePath
- args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors()
+ args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "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/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml
index 010006b81e..f5a4ab1071 100644
--- a/platform/android/java/lib/res/values/strings.xml
+++ b/platform/android/java/lib/res/values/strings.xml
@@ -12,6 +12,8 @@
<string name="text_button_resume">Resume Download</string>
<string name="text_button_cancel">Cancel</string>
<string name="text_button_cancel_verify">Cancel Verification</string>
+ <string name="text_error_title">Error!</string>
+ <string name="error_engine_setup_message">Unable to setup the Godot Engine! Aborting…</string>
<!-- APK Expansion Strings -->
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 fb1604f6af..f21f88db0a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java
@@ -30,7 +30,8 @@
package org.godotengine.godot;
-import android.content.ComponentName;
+import org.godotengine.godot.utils.ProcessPhoenix;
+
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -71,6 +72,7 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
@Override
public void onDestroy() {
+ Log.v(TAG, "Destroying Godot app...");
super.onDestroy();
onGodotForceQuit(godotFragment);
}
@@ -78,27 +80,21 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God
@Override
public final void onGodotForceQuit(Godot instance) {
if (instance == godotFragment) {
- System.exit(0);
+ Log.v(TAG, "Force quitting Godot instance");
+ ProcessPhoenix.forceQuit(this);
}
}
@Override
public final void onGodotRestartRequested(Godot instance) {
if (instance == godotFragment) {
- // HACK:
- //
- // Currently it's very hard to properly deinitialize Godot on Android to restart the game
+ // 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 statics).
- //
- // Using instrumentation is a way of making the whole app process restart, because Android
- // will kill any process of the same package which was already running.
- //
- Bundle args = new Bundle();
- args.putParcelable("intent", getIntent());
- startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args);
+ Log.v(TAG, "Restarting Godot instance...");
+ ProcessPhoenix.triggerRebirth(this);
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
index 377881be85..92e5e59496 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java
@@ -34,6 +34,8 @@ 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;
@@ -55,6 +57,7 @@ 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;
@@ -67,6 +70,7 @@ 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;
@@ -83,6 +87,8 @@ import android.widget.TextView;
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;
@@ -103,6 +109,8 @@ 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;
@@ -164,9 +172,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private Sensor mMagnetometer;
private Sensor mGyroscope;
- public static GodotIO io;
- public static GodotNetUtils netUtils;
- public static GodotTTS tts;
+ public GodotIO io;
+ public GodotNetUtils netUtils;
+ public GodotTTS tts;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@@ -248,7 +256,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
* Used by the native code (java_godot_lib_jni.cpp) to complete initialization of the GLSurfaceView view and renderer.
*/
@Keep
- private void onVideoInit() {
+ private boolean onVideoInit() {
final Activity activity = getActivity();
containerLayout = new FrameLayout(activity);
containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
@@ -260,13 +268,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
// ...add to FrameLayout
containerLayout.addView(editText);
- GodotLib.setup(command_line);
+ 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;
+ }
- final String videoDriver = GodotLib.getGlobal("rendering/driver/driver_name");
- if (videoDriver.equals("vulkan")) {
- mRenderView = new GodotVulkanRenderView(activity, this);
- } else {
+ final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method");
+ if (renderer.equals("gl_compatibility")) {
mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl);
+ } else {
+ mRenderView = new GodotVulkanRenderView(activity, this);
}
View view = mRenderView.getView();
@@ -301,6 +313,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
}
}
+ return true;
}
public void setKeepScreenOn(final boolean p_enabled) {
@@ -342,13 +355,27 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
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) -> dialog.cancel());
+ (dialog, id) -> {
+ if (okCallback != null) {
+ okCallback.run();
+ }
+ dialog.cancel();
+ });
AlertDialog dialog = builder.create();
dialog.show();
});
@@ -458,24 +485,28 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
final Activity activity = getActivity();
io = new GodotIO(activity);
- GodotLib.io = io;
netUtils = new GodotNetUtils(activity);
tts = new GodotTTS(activity);
+ Context context = getContext();
+ DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
+ FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
- mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
- mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
- mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME);
- GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion);
+ godot_initialized = GodotLib.initialize(activity,
+ this,
+ activity.getAssets(),
+ io,
+ netUtils,
+ directoryAccessHandler,
+ fileAccessHandler,
+ use_apk_expansion,
+ tts);
result_callback = null;
-
- godot_initialized = true;
}
@Override
@@ -1015,7 +1046,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
}
@Keep
- private GodotRenderView getRenderView() { // used by native side to get renderView
+ public GodotRenderView getRenderView() { // used by native side to get renderView
return mRenderView;
}
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 08da1b1832..513021f1d1 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
@@ -31,7 +31,6 @@
package org.godotengine.godot;
import org.godotengine.godot.gl.GLSurfaceView;
import org.godotengine.godot.gl.GodotRenderer;
-import org.godotengine.godot.input.GodotGestureHandler;
import org.godotengine.godot.input.GodotInputHandler;
import org.godotengine.godot.utils.GLUtils;
import org.godotengine.godot.xr.XRMode;
@@ -46,7 +45,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
-import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
@@ -75,7 +73,6 @@ import androidx.annotation.Keep;
public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
private final Godot godot;
private final GodotInputHandler inputHandler;
- private final GestureDetector detector;
private final GodotRenderer godotRenderer;
private PointerIcon pointerIcon;
@@ -85,7 +82,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
this.godot = godot;
this.inputHandler = new GodotInputHandler(this);
- this.detector = new GestureDetector(context, new GodotGestureHandler(this));
this.godotRenderer = new GodotRenderer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT);
@@ -132,7 +128,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
- this.detector.onTouchEvent(event);
return inputHandler.onTouchEvent(event);
}
@@ -156,6 +151,24 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
return inputHandler.onGenericMotionEvent(event);
}
+ @Override
+ public void onPointerCaptureChange(boolean hasCapture) {
+ super.onPointerCaptureChange(hasCapture);
+ inputHandler.onPointerCaptureChange(hasCapture);
+ }
+
+ @Override
+ public void requestPointerCapture() {
+ super.requestPointerCapture();
+ inputHandler.onPointerCaptureChange(true);
+ }
+
+ @Override
+ public void releasePointerCapture() {
+ super.releasePointerCapture();
+ inputHandler.onPointerCaptureChange(false);
+ }
+
/**
* called from JNI to change pointer icon
*/
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
index b69d25dd8b..d283de8ce8 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
@@ -36,7 +36,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.res.AssetManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
@@ -46,12 +45,10 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.WindowInsets;
-import java.io.IOException;
import java.util.List;
import java.util.Locale;
@@ -60,7 +57,6 @@ import java.util.Locale;
public class GodotIO {
private static final String TAG = GodotIO.class.getSimpleName();
- private final AssetManager am;
private final Activity activity;
private final String uniqueId;
GodotEditText edit;
@@ -73,100 +69,8 @@ public class GodotIO {
final int SCREEN_SENSOR_PORTRAIT = 5;
final int SCREEN_SENSOR = 6;
- /////////////////////////
- /// DIRECTORIES
- /////////////////////////
-
- static class AssetDir {
- public String[] files;
- public int current;
- public String path;
- }
-
- private int last_dir_id = 1;
-
- private final SparseArray<AssetDir> dirs;
-
- public int dir_open(String path) {
- AssetDir ad = new AssetDir();
- ad.current = 0;
- ad.path = path;
-
- try {
- ad.files = am.list(path);
- // no way to find path is directory or file exactly.
- // but if ad.files.length==0, then it's an empty directory or file.
- if (ad.files.length == 0) {
- return -1;
- }
- } catch (IOException e) {
- System.out.printf("Exception on dir_open: %s\n", e);
- return -1;
- }
-
- ++last_dir_id;
- dirs.put(last_dir_id, ad);
-
- return last_dir_id;
- }
-
- public boolean dir_is_dir(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_next: invalid dir id: %d\n", id);
- return false;
- }
- AssetDir ad = dirs.get(id);
- //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
- int idx = ad.current;
- if (idx > 0)
- idx--;
-
- if (idx >= ad.files.length)
- return false;
- String fname = ad.files[idx];
-
- try {
- if (ad.path.equals(""))
- am.open(fname);
- else
- am.open(ad.path + "/" + fname);
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- public String dir_next(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_next: invalid dir id: %d\n", id);
- return "";
- }
-
- AssetDir ad = dirs.get(id);
- //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
-
- if (ad.current >= ad.files.length) {
- ad.current++;
- return "";
- }
- String r = ad.files[ad.current];
- ad.current++;
- return r;
- }
-
- public void dir_close(int id) {
- if (dirs.get(id) == null) {
- System.out.printf("dir_close: invalid dir id: %d\n", id);
- return;
- }
-
- dirs.remove(id);
- }
-
GodotIO(Activity p_activity) {
- am = p_activity.getAssets();
activity = p_activity;
- dirs = new SparseArray<>();
String androidId = Settings.Secure.getString(activity.getContentResolver(),
Settings.Secure.ANDROID_ID);
if (androidId == null) {
@@ -224,12 +128,30 @@ public class GodotIO {
}
public int getScreenDPI() {
- DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
- return (int)(metrics.density * 160f);
+ return activity.getResources().getDisplayMetrics().densityDpi;
}
+ /**
+ * Returns bucketized density values.
+ */
public float getScaledDensity() {
- return activity.getResources().getDisplayMetrics().scaledDensity;
+ int densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
+ float selectedScaledDensity;
+ if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) {
+ selectedScaledDensity = 4.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
+ selectedScaledDensity = 3.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
+ selectedScaledDensity = 2.0f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
+ selectedScaledDensity = 1.5f;
+ } else if (densityDpi >= DisplayMetrics.DENSITY_MEDIUM) {
+ selectedScaledDensity = 1.0f;
+ } else {
+ selectedScaledDensity = 0.75f;
+ }
+ Log.d(TAG, "Selected scaled density: " + selectedScaledDensity);
+ return selectedScaledDensity;
}
public double getScreenRefreshRate(double fallback) {
@@ -281,9 +203,10 @@ public class GodotIO {
return result;
}
- public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
- if (edit != null)
- edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end);
+ public void showKeyboard(String p_existing_text, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
+ if (edit != null) {
+ edit.showKeyboard(p_existing_text, GodotEditText.VirtualKeyboardType.values()[p_type], p_max_input_length, p_cursor_start, p_cursor_end);
+ }
//InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
//inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
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 1f8f8c82a6..26aad867b1 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -31,8 +31,13 @@
package org.godotengine.godot;
import org.godotengine.godot.gl.GodotRenderer;
+import org.godotengine.godot.io.directory.DirectoryAccessHandler;
+import org.godotengine.godot.io.file.FileAccessHandler;
+import org.godotengine.godot.tts.GodotTTS;
+import org.godotengine.godot.utils.GodotNetUtils;
import android.app.Activity;
+import android.content.res.AssetManager;
import android.hardware.SensorEvent;
import android.view.Surface;
@@ -42,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10;
* Wrapper for native library
*/
public class GodotLib {
- public static GodotIO io;
-
static {
System.loadLibrary("godot_android");
}
@@ -51,7 +54,15 @@ public class GodotLib {
/**
* Invoked on the main thread to initialize Godot native layer.
*/
- public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
+ public static native boolean initialize(Activity activity,
+ Godot p_instance,
+ AssetManager p_asset_manager,
+ GodotIO godotIO,
+ GodotNetUtils netUtils,
+ DirectoryAccessHandler directoryAccessHandler,
+ FileAccessHandler fileAccessHandler,
+ boolean use_apk_expansion,
+ GodotTTS tts);
/**
* Invoked on the main thread to clean up Godot native layer.
@@ -63,7 +74,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 void setup(String[] p_cmdline);
+ public static native boolean setup(String[] p_cmdline);
/**
* Invoked on the GL thread when the underlying Android surface has changed size.
@@ -81,7 +92,7 @@ public class GodotLib {
public static native void newcontext(Surface p_surface);
/**
- * Forward {@link Activity#onBackPressed()} event from the main thread to the GL thread.
+ * Forward {@link Activity#onBackPressed()} event.
*/
public static native void back();
@@ -97,68 +108,60 @@ public class GodotLib {
public static native void ttsCallback(int event, int id, int pos);
/**
- * Forward touch events from the main thread to the GL thread.
+ * Forward touch events.
*/
- public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions);
- public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask);
- public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions, int buttonsMask, float verticalFactor, float horizontalFactor);
+ public static native void dispatchTouchEvent(int event, int pointer, int pointerCount, float[] positions);
/**
- * Forward hover events from the main thread to the GL thread.
+ * Dispatch mouse events
*/
- public static native void hover(int type, float x, float y);
+ public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative);
- /**
- * Forward double_tap events from the main thread to the GL thread.
- */
- public static native void doubleTap(int buttonMask, int x, int y);
+ public static native void magnify(float x, float y, float factor);
- /**
- * Forward scroll events from the main thread to the GL thread.
- */
- public static native void scroll(int x, int y);
+ public static native void pan(float x, float y, float deltaX, float deltaY);
/**
- * Forward accelerometer sensor events from the main thread to the GL thread.
+ * Forward accelerometer sensor events.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void accelerometer(float x, float y, float z);
/**
- * Forward gravity sensor events from the main thread to the GL thread.
+ * Forward gravity sensor events.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void gravity(float x, float y, float z);
/**
- * Forward magnetometer sensor events from the main thread to the GL thread.
+ * Forward magnetometer sensor events.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void magnetometer(float x, float y, float z);
/**
- * Forward gyroscope sensor events from the main thread to the GL thread.
+ * Forward gyroscope sensor events.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void gyroscope(float x, float y, float z);
/**
- * Forward regular key events from the main thread to the GL thread.
+ * Forward regular key events.
*/
- public static native void key(int p_keycode, int p_scancode, int p_unicode_char, boolean p_pressed);
+ public static native void key(int p_keycode, int p_physical_keycode, int p_unicode, boolean p_pressed);
/**
- * Forward game device's key events from the main thread to the GL thread.
+ * Forward game device's key events.
*/
public static native void joybutton(int p_device, int p_but, boolean p_pressed);
/**
- * Forward joystick devices axis motion events from the main thread to the GL thread.
+ * Forward joystick devices axis motion events.
*/
public static native void joyaxis(int p_device, int p_axis, float p_value);
/**
- * Forward joystick devices hat motion events from the main thread to the GL thread.
+ * Forward joystick devices hat motion events.
*/
public static native void joyhat(int p_device, int p_hat_x, int p_hat_y);
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 c386a2d2eb..fa6c3280b9 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
@@ -30,7 +30,6 @@
package org.godotengine.godot;
-import org.godotengine.godot.input.GodotGestureHandler;
import org.godotengine.godot.input.GodotInputHandler;
import org.godotengine.godot.vulkan.VkRenderer;
import org.godotengine.godot.vulkan.VkSurfaceView;
@@ -38,7 +37,6 @@ import org.godotengine.godot.vulkan.VkSurfaceView;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
-import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
@@ -49,7 +47,6 @@ import androidx.annotation.Keep;
public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
private final Godot godot;
private final GodotInputHandler mInputHandler;
- private final GestureDetector mGestureDetector;
private final VkRenderer mRenderer;
private PointerIcon pointerIcon;
@@ -58,7 +55,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
this.godot = godot;
mInputHandler = new GodotInputHandler(this);
- mGestureDetector = new GestureDetector(context, new GodotGestureHandler(this));
mRenderer = new VkRenderer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT);
@@ -106,7 +102,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
- mGestureDetector.onTouchEvent(event);
return mInputHandler.onTouchEvent(event);
}
@@ -130,6 +125,24 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
return mInputHandler.onGenericMotionEvent(event);
}
+ @Override
+ public void requestPointerCapture() {
+ super.requestPointerCapture();
+ mInputHandler.onPointerCaptureChange(true);
+ }
+
+ @Override
+ public void releasePointerCapture() {
+ super.releasePointerCapture();
+ mInputHandler.onPointerCaptureChange(false);
+ }
+
+ @Override
+ public void onPointerCaptureChange(boolean hasCapture) {
+ super.onPointerCaptureChange(hasCapture);
+ mInputHandler.onPointerCaptureChange(hasCapture);
+ }
+
/**
* called from JNI to change pointer icon
*/
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
index ecb2af0a7b..7925b54fc4 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java
@@ -52,6 +52,18 @@ public class GodotEditText extends EditText {
private final static int HANDLER_OPEN_IME_KEYBOARD = 2;
private final static int HANDLER_CLOSE_IME_KEYBOARD = 3;
+ // Enum must be kept up-to-date with DisplayServer::VirtualKeyboardType
+ public enum VirtualKeyboardType {
+ KEYBOARD_TYPE_DEFAULT,
+ KEYBOARD_TYPE_MULTILINE,
+ KEYBOARD_TYPE_NUMBER,
+ KEYBOARD_TYPE_NUMBER_DECIMAL,
+ KEYBOARD_TYPE_PHONE,
+ KEYBOARD_TYPE_EMAIL_ADDRESS,
+ KEYBOARD_TYPE_PASSWORD,
+ KEYBOARD_TYPE_URL
+ }
+
// ===========================================================
// Fields
// ===========================================================
@@ -60,7 +72,7 @@ public class GodotEditText extends EditText {
private EditHandler sHandler = new EditHandler(this);
private String mOriginText;
private int mMaxInputLength = Integer.MAX_VALUE;
- private boolean mMultiline = false;
+ private VirtualKeyboardType mKeyboardType = VirtualKeyboardType.KEYBOARD_TYPE_DEFAULT;
private static class EditHandler extends Handler {
private final WeakReference<GodotEditText> mEdit;
@@ -100,8 +112,8 @@ public class GodotEditText extends EditText {
setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE);
}
- public boolean isMultiline() {
- return mMultiline;
+ public VirtualKeyboardType getKeyboardType() {
+ return mKeyboardType;
}
private void handleMessage(final Message msg) {
@@ -122,8 +134,31 @@ public class GodotEditText extends EditText {
}
int inputType = InputType.TYPE_CLASS_TEXT;
- if (edit.isMultiline()) {
- inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ switch (edit.getKeyboardType()) {
+ case KEYBOARD_TYPE_DEFAULT:
+ inputType = InputType.TYPE_CLASS_TEXT;
+ break;
+ case KEYBOARD_TYPE_MULTILINE:
+ inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ break;
+ case KEYBOARD_TYPE_NUMBER:
+ inputType = InputType.TYPE_CLASS_NUMBER;
+ break;
+ case KEYBOARD_TYPE_NUMBER_DECIMAL:
+ inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED;
+ break;
+ case KEYBOARD_TYPE_PHONE:
+ inputType = InputType.TYPE_CLASS_PHONE;
+ break;
+ case KEYBOARD_TYPE_EMAIL_ADDRESS:
+ inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ break;
+ case KEYBOARD_TYPE_PASSWORD:
+ inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ break;
+ case KEYBOARD_TYPE_URL:
+ inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
+ break;
}
edit.setInputType(inputType);
@@ -201,7 +236,7 @@ public class GodotEditText extends EditText {
// ===========================================================
// Methods
// ===========================================================
- public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
+ public void showKeyboard(String p_existing_text, VirtualKeyboardType p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
int maxInputLength = (p_max_input_length <= 0) ? Integer.MAX_VALUE : p_max_input_length;
if (p_cursor_start == -1) { // cursor position not given
this.mOriginText = p_existing_text;
@@ -214,7 +249,7 @@ public class GodotEditText extends EditText {
this.mMaxInputLength = maxInputLength - (p_existing_text.length() - p_cursor_end);
}
- this.mMultiline = p_multiline;
+ this.mKeyboardType = p_type;
final Message msg = new Message();
msg.what = HANDLER_OPEN_IME_KEYBOARD;
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
new file mode 100644
index 0000000000..9715c31fc1
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
@@ -0,0 +1,289 @@
+/*************************************************************************/
+/* GodotGestureHandler.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* 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.input
+
+import android.os.Build
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.ScaleGestureDetector.OnScaleGestureListener
+import org.godotengine.godot.GodotLib
+
+/**
+ * Handles regular and scale gesture input related events for the [GodotView] view.
+ *
+ * @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener
+ * @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener
+ */
+internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener {
+
+ companion object {
+ private val TAG = GodotGestureHandler::class.java.simpleName
+ }
+
+ /**
+ * Enable pan and scale gestures
+ */
+ var panningAndScalingEnabled = false
+
+ private var doubleTapInProgress = false
+ private var dragInProgress = false
+ private var scaleInProgress = false
+ private var contextClickInProgress = false
+ private var pointerCaptureInProgress = false
+
+ override fun onDown(event: MotionEvent): Boolean {
+ // Don't send / register a down event while we're in the middle of a double-tap
+ if (!doubleTapInProgress) {
+ // Send the down event
+ GodotInputHandler.handleMotionEvent(event)
+ }
+ return true
+ }
+
+ override fun onSingleTapUp(event: MotionEvent): Boolean {
+ GodotInputHandler.handleMotionEvent(event)
+ return true
+ }
+
+ override fun onLongPress(event: MotionEvent) {
+ contextClickRouter(event)
+ }
+
+ private fun contextClickRouter(event: MotionEvent) {
+ if (scaleInProgress) {
+ return
+ }
+
+ // Cancel the previous down event
+ GodotInputHandler.handleMotionEvent(
+ event.source,
+ MotionEvent.ACTION_CANCEL,
+ event.buttonState,
+ event.x,
+ event.y
+ )
+
+ // Turn a context click into a single tap right mouse button click.
+ GodotInputHandler.handleMouseEvent(
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.BUTTON_SECONDARY,
+ event.x,
+ event.y
+ )
+ contextClickInProgress = true
+ }
+
+ fun onPointerCaptureChange(hasCapture: Boolean) {
+ if (pointerCaptureInProgress == hasCapture) {
+ return
+ }
+
+ if (!hasCapture) {
+ // Dispatch a mouse relative ACTION_UP event to signal the end of the capture
+ GodotInputHandler.handleMouseEvent(
+ MotionEvent.ACTION_UP,
+ 0,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ false,
+ true
+ )
+ }
+ pointerCaptureInProgress = hasCapture
+ }
+
+ fun onMotionEvent(event: MotionEvent): Boolean {
+ return when (event.actionMasked) {
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
+ onActionUp(event)
+ }
+ MotionEvent.ACTION_MOVE -> {
+ onActionMove(event)
+ }
+ else -> false
+ }
+ }
+
+ private fun onActionUp(event: MotionEvent): Boolean {
+ val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)
+ } else {
+ false
+ }
+ when {
+ pointerCaptureInProgress -> {
+ return if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
+ // Don't dispatch the ACTION_CANCEL while a capture is in progress
+ true
+ } else {
+ GodotInputHandler.handleMouseEvent(
+ MotionEvent.ACTION_UP,
+ event.buttonState,
+ event.x,
+ event.y,
+ 0f,
+ 0f,
+ false,
+ sourceMouseRelative
+ )
+ pointerCaptureInProgress = false
+ true
+ }
+ }
+ dragInProgress -> {
+ GodotInputHandler.handleMotionEvent(event)
+ dragInProgress = false
+ return true
+ }
+ contextClickInProgress -> {
+ GodotInputHandler.handleMouseEvent(
+ event.actionMasked,
+ 0,
+ event.x,
+ event.y,
+ 0f,
+ 0f,
+ false,
+ sourceMouseRelative
+ )
+ contextClickInProgress = false
+ return true
+ }
+ else -> return false
+ }
+ }
+
+ private fun onActionMove(event: MotionEvent): Boolean {
+ if (contextClickInProgress) {
+ val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)
+ } else {
+ false
+ }
+ GodotInputHandler.handleMouseEvent(
+ event.actionMasked,
+ MotionEvent.BUTTON_SECONDARY,
+ event.x,
+ event.y,
+ 0f,
+ 0f,
+ false,
+ sourceMouseRelative
+ )
+ return true
+ }
+ return false
+ }
+
+ override fun onDoubleTapEvent(event: MotionEvent): Boolean {
+ if (event.actionMasked == MotionEvent.ACTION_UP) {
+ doubleTapInProgress = false
+ }
+ return true
+ }
+
+ override fun onDoubleTap(event: MotionEvent): Boolean {
+ doubleTapInProgress = true
+ val x = event.x
+ val y = event.y
+ val buttonMask =
+ if (GodotInputHandler.isMouseEvent(event)) {
+ event.buttonState
+ } else {
+ MotionEvent.BUTTON_PRIMARY
+ }
+ GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_DOWN, buttonMask, x, y, true)
+ GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_UP, 0, x, y, false)
+
+ return true
+ }
+
+ override fun onScroll(
+ originEvent: MotionEvent,
+ terminusEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ if (scaleInProgress) {
+ if (dragInProgress) {
+ // Cancel the drag
+ GodotInputHandler.handleMotionEvent(
+ originEvent.source,
+ MotionEvent.ACTION_CANCEL,
+ originEvent.buttonState,
+ originEvent.x,
+ originEvent.y
+ )
+ dragInProgress = false
+ }
+ return true
+ }
+
+ dragInProgress = true
+
+ val x = terminusEvent.x
+ val y = terminusEvent.y
+ if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled) {
+ GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f)
+ } else {
+ GodotInputHandler.handleMotionEvent(terminusEvent)
+ }
+ return true
+ }
+
+ override fun onScale(detector: ScaleGestureDetector?): Boolean {
+ if (detector == null || !panningAndScalingEnabled) {
+ return false
+ }
+ GodotLib.magnify(
+ detector.focusX,
+ detector.focusY,
+ detector.scaleFactor
+ )
+ return true
+ }
+
+ override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
+ if (detector == null || !panningAndScalingEnabled) {
+ return false
+ }
+ scaleInProgress = true
+ return true
+ }
+
+ override fun onScaleEnd(detector: ScaleGestureDetector?) {
+ scaleInProgress = false
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
index 8694bb91e1..03cb8034fa 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
@@ -41,13 +41,13 @@ import android.os.Build;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.view.GestureDetector;
import android.view.InputDevice;
-import android.view.InputDevice.MotionRange;
import android.view.KeyEvent;
import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
@@ -55,21 +55,49 @@ import java.util.Set;
* Handles input related events for the {@link GodotRenderView} view.
*/
public class GodotInputHandler implements InputManager.InputDeviceListener {
- private final GodotRenderView mRenderView;
- private final InputManager mInputManager;
-
- private final String tag = this.getClass().getSimpleName();
+ private static final String TAG = GodotInputHandler.class.getSimpleName();
private final SparseIntArray mJoystickIds = new SparseIntArray(4);
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
+ private final GodotRenderView mRenderView;
+ private final InputManager mInputManager;
+ private final GestureDetector gestureDetector;
+ private final ScaleGestureDetector scaleGestureDetector;
+ private final GodotGestureHandler godotGestureHandler;
+
public GodotInputHandler(GodotRenderView godotView) {
+ final Context context = godotView.getView().getContext();
mRenderView = godotView;
- mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE);
+ mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
mInputManager.registerInputDeviceListener(this, null);
+
+ this.godotGestureHandler = new GodotGestureHandler();
+ this.gestureDetector = new GestureDetector(context, godotGestureHandler);
+ this.gestureDetector.setIsLongpressEnabled(false);
+ this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ this.scaleGestureDetector.setStylusScaleEnabled(true);
+ }
}
- private boolean isKeyEvent_GameDevice(int source) {
+ /**
+ * Enable long press events. This is false by default.
+ */
+ public void enableLongPress(boolean enable) {
+ this.gestureDetector.setIsLongpressEnabled(enable);
+ }
+
+ /**
+ * Enable multi-fingers pan & scale gestures. This is false by default.
+ *
+ * Note: This may interfere with multi-touch handling / support.
+ */
+ public void enablePanningAndScalingGestures(boolean enable) {
+ this.godotGestureHandler.setPanningAndScalingEnabled(enable);
+ }
+
+ private boolean isKeyEventGameDevice(int source) {
// Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD)
if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD))
return false;
@@ -77,6 +105,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 void onPointerCaptureChange(boolean hasCapture) {
+ godotGestureHandler.onPointerCaptureChange(hasCapture);
+ }
+
public boolean onKeyUp(final int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return true;
@@ -87,7 +119,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
int source = event.getSource();
- if (isKeyEvent_GameDevice(source)) {
+ if (isKeyEventGameDevice(source)) {
// Check if the device exists
final int deviceId = event.getDeviceId();
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
@@ -96,10 +128,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
GodotLib.joybutton(godotJoyId, button, false);
}
} else {
- final int scanCode = event.getScanCode();
- final int chr = event.getUnicodeChar(0);
- GodotLib.key(keyCode, scanCode, chr, false);
- }
+ // getKeyCode(): The physical key that was pressed.
+ // Godot's keycodes match the ASCII codes, so for single byte unicode characters,
+ // we can use the unmodified unicode character to determine Godot's keycode.
+ final int keycode = event.getUnicodeChar(0);
+ final int physical_keycode = event.getKeyCode();
+ final int unicode = event.getUnicodeChar();
+ GodotLib.key(keycode, physical_keycode, unicode, false);
+ };
return true;
}
@@ -117,11 +153,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
int source = event.getSource();
- //Log.e(TAG, String.format("Key down! source %d, device %d, joystick %d, %d, %d", event.getDeviceId(), source, (source & InputDevice.SOURCE_JOYSTICK), (source & InputDevice.SOURCE_DPAD), (source & InputDevice.SOURCE_GAMEPAD)));
final int deviceId = event.getDeviceId();
// Check if source is a game device and that the device is a registered gamepad
- if (isKeyEvent_GameDevice(source)) {
+ if (isKeyEventGameDevice(source)) {
if (event.getRepeatCount() > 0) // ignore key echo
return true;
@@ -131,74 +166,71 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
GodotLib.joybutton(godotJoyId, button, true);
}
} else {
- final int scanCode = event.getScanCode();
- final int chr = event.getUnicodeChar(0);
- GodotLib.key(keyCode, scanCode, chr, true);
+ final int keycode = event.getUnicodeChar(0);
+ final int physical_keycode = event.getKeyCode();
+ final int unicode = event.getUnicodeChar();
+ GodotLib.key(keycode, physical_keycode, unicode, true);
}
return true;
}
public boolean onTouchEvent(final MotionEvent event) {
- // Mouse drag (mouse pressed and move) doesn't fire onGenericMotionEvent so this is needed
- if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
- if (event.getAction() != MotionEvent.ACTION_MOVE) {
- // we return true because every time a mouse event is fired, the event is already handled
- // in onGenericMotionEvent, so by touch event we can say that the event is also handled
- return true;
- }
- return handleMouseEvent(event);
+ this.scaleGestureDetector.onTouchEvent(event);
+ if (this.gestureDetector.onTouchEvent(event)) {
+ // The gesture detector has handled the event.
+ return true;
}
- final int evcount = event.getPointerCount();
- if (evcount == 0)
+ if (godotGestureHandler.onMotionEvent(event)) {
+ // The gesture handler has handled the event.
return true;
+ }
- if (mRenderView != null) {
- final float[] arr = new float[event.getPointerCount() * 3]; // pointerId1, x1, y1, pointerId2, etc...
+ // Drag events are handled by the [GodotGestureHandler]
+ if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
+ return true;
+ }
- for (int i = 0; i < event.getPointerCount(); i++) {
- arr[i * 3 + 0] = event.getPointerId(i);
- arr[i * 3 + 1] = event.getX(i);
- arr[i * 3 + 2] = event.getY(i);
- }
- final int action = event.getActionMasked();
- final int pointer_idx = event.getPointerId(event.getActionIndex());
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_MOVE:
- case MotionEvent.ACTION_POINTER_UP:
- case MotionEvent.ACTION_POINTER_DOWN: {
- GodotLib.touch(event.getSource(), action, pointer_idx, evcount, arr);
- } break;
- }
+ if (isMouseEvent(event)) {
+ return handleMouseEvent(event);
}
- return true;
+
+ return handleTouchEvent(event);
}
public boolean onGenericMotionEvent(MotionEvent event) {
- if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getAction() == MotionEvent.ACTION_MOVE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) {
+ // The gesture detector has handled the event.
+ return true;
+ }
+
+ if (godotGestureHandler.onMotionEvent(event)) {
+ // The gesture handler has handled the event.
+ return true;
+ }
+
+ if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getActionMasked() == MotionEvent.ACTION_MOVE) {
// Check if the device exists
final int deviceId = event.getDeviceId();
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int godotJoyId = mJoystickIds.get(deviceId);
Joystick joystick = mJoysticksDevices.get(deviceId);
+ if (joystick == null) {
+ return true;
+ }
for (int i = 0; i < joystick.axes.size(); i++) {
final int axis = joystick.axes.get(i);
final float value = event.getAxisValue(axis);
- /**
- * As all axes are polled for each event, only fire an axis event if the value has actually changed.
- * Prevents flooding Godot with repeated events.
+ /*
+ As all axes are polled for each event, only fire an axis event if the value has actually changed.
+ Prevents flooding Godot with repeated events.
*/
if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) {
// save value to prevent repeats
joystick.axesValues.put(axis, value);
- final int godotAxisIdx = i;
- GodotLib.joyaxis(godotJoyId, godotAxisIdx, value);
+ GodotLib.joyaxis(godotJoyId, i, value);
}
}
@@ -213,17 +245,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
return true;
}
- } else if (event.isFromSource(InputDevice.SOURCE_STYLUS)) {
- final float x = event.getX();
- final float y = event.getY();
- final int type = event.getAction();
- GodotLib.hover(type, x, y);
- return true;
-
- } else if (event.isFromSource(InputDevice.SOURCE_MOUSE) || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return handleMouseEvent(event);
- }
+ } else if (isMouseEvent(event)) {
+ return handleMouseEvent(event);
}
return false;
@@ -235,7 +258,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
for (int deviceId : deviceIds) {
InputDevice device = mInputManager.getInputDevice(deviceId);
if (DEBUG) {
- Log.v("GodotInputHandler", String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
+ Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
}
onInputDeviceAdded(deviceId);
}
@@ -280,13 +303,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
joystick.name = device.getName();
//Helps with creating new joypad mappings.
- Log.i(tag, "=== New Input Device: " + joystick.name);
+ Log.i(TAG, "=== New Input Device: " + joystick.name);
Set<Integer> already = new HashSet<>();
for (InputDevice.MotionRange range : device.getMotionRanges()) {
boolean isJoystick = range.isFromSource(InputDevice.SOURCE_JOYSTICK);
boolean isGamepad = range.isFromSource(InputDevice.SOURCE_GAMEPAD);
- //Log.i(tag, "axis: "+range.getAxis()+ ", isJoystick: "+isJoystick+", isGamepad: "+isGamepad);
if (!isJoystick && !isGamepad) {
continue;
}
@@ -298,14 +320,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
already.add(axis);
joystick.axes.add(axis);
} else {
- Log.w(tag, " - DUPLICATE AXIS VALUE IN LIST: " + axis);
+ Log.w(TAG, " - DUPLICATE AXIS VALUE IN LIST: " + axis);
}
}
}
Collections.sort(joystick.axes);
for (int idx = 0; idx < joystick.axes.size(); idx++) {
//Helps with creating new joypad mappings.
- Log.i(tag, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx);
+ Log.i(TAG, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx);
}
mJoysticksDevices.put(deviceId, joystick);
@@ -330,13 +352,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
onInputDeviceAdded(deviceId);
}
- private static class RangeComparator implements Comparator<MotionRange> {
- @Override
- public int compare(MotionRange arg0, MotionRange arg1) {
- return arg0.getAxis() - arg1.getAxis();
- }
- }
-
public static int getGodotButton(int keyCode) {
int button;
switch (keyCode) {
@@ -402,39 +417,113 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
return button;
}
- private boolean handleMouseEvent(final MotionEvent event) {
- switch (event.getActionMasked()) {
+ static boolean isMouseEvent(MotionEvent event) {
+ return isMouseEvent(event.getSource());
+ }
+
+ private static boolean isMouseEvent(int eventSource) {
+ boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mouseSource = mouseSource || ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE);
+ }
+ return mouseSource;
+ }
+
+ static boolean handleMotionEvent(final MotionEvent event) {
+ if (isMouseEvent(event)) {
+ return handleMouseEvent(event);
+ }
+
+ return handleTouchEvent(event);
+ }
+
+ static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y) {
+ return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, 0, 0);
+ }
+
+ static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY) {
+ if (isMouseEvent(eventSource)) {
+ return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, false, false);
+ }
+
+ return handleTouchEvent(eventAction, x, y);
+ }
+
+ static boolean handleMouseEvent(final MotionEvent event) {
+ final int eventAction = event.getActionMasked();
+ final float x = event.getX();
+ final float y = event.getY();
+ final int buttonsMask = event.getButtonState();
+
+ final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ boolean sourceMouseRelative = false;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE);
+ }
+ return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative);
+ }
+
+ static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y) {
+ return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, false, false);
+ }
+
+ static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, boolean doubleClick) {
+ return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, doubleClick, false);
+ }
+
+ static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) {
+ 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:
case MotionEvent.ACTION_HOVER_MOVE:
- case MotionEvent.ACTION_HOVER_EXIT: {
- final float x = event.getX();
- final float y = event.getY();
- final int type = event.getAction();
- GodotLib.hover(type, x, y);
- return true;
- }
- case MotionEvent.ACTION_BUTTON_PRESS:
- case MotionEvent.ACTION_BUTTON_RELEASE:
- case MotionEvent.ACTION_MOVE: {
- final float x = event.getX();
- final float y = event.getY();
- final int buttonsMask = event.getButtonState();
- final int action = event.getAction();
- GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask);
- return true;
- }
+ case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_SCROLL: {
- final float x = event.getX();
- final float y = event.getY();
- final int buttonsMask = event.getButtonState();
- final int action = event.getAction();
- final float verticalFactor = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
- final float horizontalFactor = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
- GodotLib.touch(event.getSource(), action, 0, 1, new float[] { 0, x, y }, buttonsMask, verticalFactor, horizontalFactor);
+ GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative);
+ return true;
}
+ }
+ return false;
+ }
+
+ static boolean handleTouchEvent(final MotionEvent event) {
+ final int pointerCount = event.getPointerCount();
+ if (pointerCount == 0) {
+ return true;
+ }
+
+ final float[] positions = new float[pointerCount * 3]; // pointerId1, x1, y1, pointerId2, etc...
+
+ for (int i = 0; i < pointerCount; i++) {
+ positions[i * 3 + 0] = event.getPointerId(i);
+ positions[i * 3 + 1] = event.getX(i);
+ positions[i * 3 + 2] = event.getY(i);
+ }
+ final int action = event.getActionMasked();
+ final int actionPointerId = event.getPointerId(event.getActionIndex());
+
+ return handleTouchEvent(action, actionPointerId, pointerCount, positions);
+ }
+
+ static boolean handleTouchEvent(int eventAction, float x, float y) {
+ return handleTouchEvent(eventAction, 0, 1, new float[] { 0, x, y });
+ }
+
+ static boolean handleTouchEvent(int eventAction, int actionPointerId, int pointerCount, float[] positions) {
+ switch (eventAction) {
case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_UP: {
- // we can safely ignore these cases because they are always come beside ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_MOVE:
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ GodotLib.dispatchTouchEvent(eventAction, actionPointerId, pointerCount, positions);
return true;
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java
index e940aafa9e..c959b5f28c 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
@@ -92,11 +92,9 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
@Override
public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) {
- //Log.d(TAG, "beforeTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",after: " + after);
-
for (int i = 0; i < count; ++i) {
- GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, true);
- GodotLib.key(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL, 0, false);
+ GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, true);
+ GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, false);
if (mHasSelection) {
mHasSelection = false;
@@ -107,20 +105,18 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
@Override
public void onTextChanged(final CharSequence pCharSequence, final int start, final int before, final int count) {
- //Log.d(TAG, "onTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",before: " + before);
-
final int[] newChars = new int[count];
for (int i = start; i < start + count; ++i) {
newChars[i - start] = pCharSequence.charAt(i);
}
for (int i = 0; i < count; ++i) {
int key = newChars[i];
- if ((key == '\n') && !mEdit.isMultiline()) {
+ if ((key == '\n') && !(mEdit.getKeyboardType() == GodotEditText.VirtualKeyboardType.KEYBOARD_TYPE_MULTILINE)) {
// Return keys are handled through action events
continue;
}
- GodotLib.key(0, 0, key, true);
- GodotLib.key(0, 0, key, false);
+ GodotLib.key(key, 0, key, true);
+ GodotLib.key(key, 0, key, false);
}
}
@@ -131,16 +127,16 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
for (int i = 0; i < characters.length(); i++) {
final int ch = characters.codePointAt(i);
- GodotLib.key(0, 0, ch, true);
- GodotLib.key(0, 0, ch, false);
+ GodotLib.key(ch, 0, ch, true);
+ GodotLib.key(ch, 0, ch, false);
}
}
if (pActionID == EditorInfo.IME_ACTION_DONE) {
// Enter key has been pressed
mRenderView.queueOnRenderThread(() -> {
- GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, true);
- GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, false);
+ GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, true);
+ GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, false);
});
mRenderView.getView().requestFocus();
return true;
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
new file mode 100644
index 0000000000..c9282dd247
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
@@ -0,0 +1,113 @@
+/*************************************************************************/
+/* StorageScope.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io
+
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import java.io.File
+
+/**
+ * Represents the different storage scopes.
+ */
+internal enum class StorageScope {
+ /**
+ * Covers internal and external directories accessible to the app without restrictions.
+ */
+ APP,
+
+ /**
+ * Covers shared directories (from Android 10 and higher).
+ */
+ SHARED,
+
+ /**
+ * Everything else..
+ */
+ UNKNOWN;
+
+ class Identifier(context: Context) {
+
+ private val internalAppDir: String? = context.filesDir.canonicalPath
+ private val internalCacheDir: String? = context.cacheDir.canonicalPath
+ private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath
+ private val sharedDir : String? = Environment.getExternalStorageDirectory().canonicalPath
+ private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
+ private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
+
+ /**
+ * Determines which [StorageScope] the given path falls under.
+ */
+ fun identifyStorageScope(path: String?): StorageScope {
+ if (path == null) {
+ return UNKNOWN
+ }
+
+ val pathFile = File(path)
+ if (!pathFile.isAbsolute) {
+ return UNKNOWN
+ }
+
+ val canonicalPathFile = pathFile.canonicalPath
+
+ if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) {
+ return APP
+ }
+
+ if (internalCacheDir != null && canonicalPathFile.startsWith(internalCacheDir)) {
+ return APP
+ }
+
+ if (externalAppDir != null && canonicalPathFile.startsWith(externalAppDir)) {
+ return APP
+ }
+
+ if (sharedDir != null && canonicalPathFile.startsWith(sharedDir)) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // Before R, apps had access to shared storage so long as they have the right
+ // permissions (and flag on Q).
+ return APP
+ }
+
+ // Post R, access is limited based on the target destination
+ // 'Downloads' and 'Documents' are still accessible
+ if ((downloadsSharedDir != null && canonicalPathFile.startsWith(downloadsSharedDir))
+ || (documentsSharedDir != null && canonicalPathFile.startsWith(documentsSharedDir))) {
+ return APP
+ }
+
+ return SHARED
+ }
+
+ return UNKNOWN
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
new file mode 100644
index 0000000000..098b10ae36
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
@@ -0,0 +1,177 @@
+/*************************************************************************/
+/* AssetsDirectoryAccess.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.directory
+
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
+import java.io.File
+import java.io.IOException
+
+/**
+ * Handles directories access within the Android assets directory.
+ */
+internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
+
+ companion object {
+ private val TAG = AssetsDirectoryAccess::class.java.simpleName
+ }
+
+ private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
+
+ private val assetManager = context.assets
+
+ private var lastDirId = STARTING_DIR_ID
+ private val dirs = SparseArray<AssetDir>()
+
+ private fun getAssetsPath(originalPath: String): String {
+ if (originalPath.startsWith(File.separatorChar)) {
+ return originalPath.substring(1)
+ }
+ return originalPath
+ }
+
+ override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
+
+ override fun dirOpen(path: String): Int {
+ val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID
+ try {
+ val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ if (files.isEmpty()) {
+ return INVALID_DIR_ID
+ }
+
+ val ad = AssetDir(assetsPath, files)
+
+ dirs.put(++lastDirId, ad)
+ return lastDirId
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on dirOpen", e)
+ return INVALID_DIR_ID
+ }
+ }
+
+ override fun dirExists(path: String): Boolean {
+ val assetsPath = getAssetsPath(path)
+ try {
+ val files = assetManager.list(assetsPath) ?: return false
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ return files.isNotEmpty()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on dirExists", e)
+ return false
+ }
+ }
+
+ override fun fileExists(path: String): Boolean {
+ val assetsPath = getAssetsPath(path) ?: return false
+ try {
+ val files = assetManager.list(assetsPath) ?: return false
+ // Empty directories don't get added to the 'assets' directory, so
+ // if ad.files.length > 0 ==> path is directory
+ // if ad.files.length == 0 ==> path is file
+ return files.isEmpty()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on fileExists", e)
+ return false
+ }
+ }
+
+ override fun dirIsDir(dirId: Int): Boolean {
+ val ad: AssetDir = dirs[dirId]
+
+ var idx = ad.current
+ if (idx > 0) {
+ idx--
+ }
+
+ if (idx >= ad.files.size) {
+ return false
+ }
+
+ val fileName = ad.files[idx]
+ // List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a
+ // directory
+ val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}"
+ val fileContents = assetManager.list(filePath)
+ return (fileContents?.size?: 0) > 0
+ }
+
+ override fun isCurrentHidden(dirId: Int): Boolean {
+ val ad = dirs[dirId]
+
+ var idx = ad.current
+ if (idx > 0) {
+ idx--
+ }
+
+ if (idx >= ad.files.size) {
+ return false
+ }
+
+ val fileName = ad.files[idx]
+ return fileName.startsWith('.')
+ }
+
+ override fun dirNext(dirId: Int): String {
+ val ad: AssetDir = dirs[dirId]
+
+ if (ad.current >= ad.files.size) {
+ ad.current++
+ return ""
+ }
+
+ return ad.files[ad.current++]
+ }
+
+ override fun dirClose(dirId: Int) {
+ dirs.remove(dirId)
+ }
+
+ override fun getDriveCount() = 0
+
+ override fun getDrive(drive: Int) = ""
+
+ override fun makeDir(dir: String) = false
+
+ override fun getSpaceLeft() = 0L
+
+ override fun rename(from: String, to: String) = false
+
+ override fun remove(filename: String) = false
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
new file mode 100644
index 0000000000..fedcf4843f
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
@@ -0,0 +1,224 @@
+/*************************************************************************/
+/* DirectoryAccessHandler.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.directory
+
+import android.content.Context
+import android.util.Log
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
+
+/**
+ * Handles files and directories access and manipulation for the Android platform
+ */
+class DirectoryAccessHandler(context: Context) {
+
+ companion object {
+ private val TAG = DirectoryAccessHandler::class.java.simpleName
+
+ internal const val INVALID_DIR_ID = -1
+ internal const val STARTING_DIR_ID = 1
+
+ private fun getAccessTypeFromNative(accessType: Int): AccessType? {
+ return when (accessType) {
+ ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
+ ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
+ else -> null
+ }
+ }
+ }
+
+ private enum class AccessType(val nativeValue: Int) {
+ ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
+ }
+
+ internal interface DirectoryAccess {
+ fun dirOpen(path: String): Int
+ fun dirNext(dirId: Int): String
+ fun dirClose(dirId: Int)
+ fun dirIsDir(dirId: Int): Boolean
+ fun dirExists(path: String): Boolean
+ fun fileExists(path: String): Boolean
+ fun hasDirId(dirId: Int): Boolean
+ fun isCurrentHidden(dirId: Int): Boolean
+ fun getDriveCount() : Int
+ fun getDrive(drive: Int): String
+ fun makeDir(dir: String): Boolean
+ fun getSpaceLeft(): Long
+ fun rename(from: String, to: String): Boolean
+ fun remove(filename: String): Boolean
+ }
+
+ private val assetsDirAccess = AssetsDirectoryAccess(context)
+ private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
+
+ private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
+ }
+ }
+
+ fun dirOpen(nativeAccessType: Int, path: String?): Int {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return INVALID_DIR_ID
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
+ }
+ }
+
+ fun dirNext(nativeAccessType: Int, dirId: Int): String {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirNext: Invalid dir id: $dirId")
+ return ""
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
+ }
+ }
+
+ fun dirClose(nativeAccessType: Int, dirId: Int) {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirClose: Invalid dir id: $dirId")
+ return
+ }
+
+ when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
+ }
+ }
+
+ fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
+ }
+ }
+
+ fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (accessType == null || !hasDirId(accessType, dirId)) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
+ }
+ }
+
+ fun dirExists(nativeAccessType: Int, path: String?): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
+ }
+ }
+
+ fun fileExists(nativeAccessType: Int, path: String?): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType)
+ if (path == null || accessType == null) {
+ return false
+ }
+
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
+ }
+ }
+
+ fun getDriveCount(nativeAccessType: Int): Int {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
+ return when(accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
+ }
+ }
+
+ fun getDrive(nativeAccessType: Int, drive: Int): String {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
+ }
+ }
+
+ fun makeDir(nativeAccessType: Int, dir: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
+ }
+ }
+
+ fun getSpaceLeft(nativeAccessType: Int): Long {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
+ }
+ }
+
+ fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
+ }
+ }
+
+ fun remove(nativeAccessType: Int, filename: String): Boolean {
+ val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ return when (accessType) {
+ ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
+ ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
+ }
+ }
+
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
new file mode 100644
index 0000000000..54fc56fa3e
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
@@ -0,0 +1,231 @@
+/*************************************************************************/
+/* FileSystemDirectoryAccess.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.directory
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.storage.StorageManager
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
+import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
+import org.godotengine.godot.io.file.FileAccessHandler
+import java.io.File
+
+/**
+ * Handles directories access with the internal and external filesystem.
+ */
+internal class FilesystemDirectoryAccess(private val context: Context):
+ DirectoryAccessHandler.DirectoryAccess {
+
+ companion object {
+ private val TAG = FilesystemDirectoryAccess::class.java.simpleName
+ }
+
+ private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
+
+ private val storageScopeIdentifier = StorageScope.Identifier(context)
+ private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
+ private var lastDirId = STARTING_DIR_ID
+ private val dirs = SparseArray<DirData>()
+
+ private fun inScope(path: String): Boolean {
+ // Directory access is available for shared storage on Android 11+
+ // On Android 10, access is also available as long as the `requestLegacyExternalStorage`
+ // tag is available.
+ return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN
+ }
+
+ override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
+
+ override fun dirOpen(path: String): Int {
+ if (!inScope(path)) {
+ Log.w(TAG, "Path $path is not accessible.")
+ return INVALID_DIR_ID
+ }
+
+ // Check this is a directory.
+ val dirFile = File(path)
+ if (!dirFile.isDirectory) {
+ return INVALID_DIR_ID
+ }
+
+ // Get the files in the directory
+ val files = dirFile.listFiles()?: return INVALID_DIR_ID
+
+ // Create the data representing this directory
+ val dirData = DirData(dirFile, files)
+
+ dirs.put(++lastDirId, dirData)
+ return lastDirId
+ }
+
+ override fun dirExists(path: String): Boolean {
+ if (!inScope(path)) {
+ Log.w(TAG, "Path $path is not accessible.")
+ return false
+ }
+
+ try {
+ return File(path).isDirectory
+ } catch (e: SecurityException) {
+ return false
+ }
+ }
+
+ override fun fileExists(path: String) = FileAccessHandler.fileExists(context, storageScopeIdentifier, path)
+
+ override fun dirNext(dirId: Int): String {
+ val dirData = dirs[dirId]
+ if (dirData.current >= dirData.files.size) {
+ dirData.current++
+ return ""
+ }
+
+ return dirData.files[dirData.current++].name
+ }
+
+ override fun dirClose(dirId: Int) {
+ dirs.remove(dirId)
+ }
+
+ override fun dirIsDir(dirId: Int): Boolean {
+ val dirData = dirs[dirId]
+
+ var index = dirData.current
+ if (index > 0) {
+ index--
+ }
+
+ if (index >= dirData.files.size) {
+ return false
+ }
+
+ return dirData.files[index].isDirectory
+ }
+
+ override fun isCurrentHidden(dirId: Int): Boolean {
+ val dirData = dirs[dirId]
+
+ var index = dirData.current
+ if (index > 0) {
+ index--
+ }
+
+ if (index >= dirData.files.size) {
+ return false
+ }
+
+ return dirData.files[index].isHidden
+ }
+
+ override fun getDriveCount(): Int {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ storageManager.storageVolumes.size
+ } else {
+ 0
+ }
+ }
+
+ override fun getDrive(drive: Int): String {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return ""
+ }
+
+ if (drive < 0 || drive >= storageManager.storageVolumes.size) {
+ return ""
+ }
+
+ val storageVolume = storageManager.storageVolumes[drive]
+ return storageVolume.getDescription(context)
+ }
+
+ override fun makeDir(dir: String): Boolean {
+ if (!inScope(dir)) {
+ Log.w(TAG, "Directory $dir is not accessible.")
+ return false
+ }
+
+ try {
+ val dirFile = File(dir)
+ return dirFile.isDirectory || dirFile.mkdirs()
+ } catch (e: SecurityException) {
+ return false
+ }
+ }
+
+ @SuppressLint("UsableSpace")
+ override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L
+
+ override fun rename(from: String, to: String): Boolean {
+ if (!inScope(from) || !inScope(to)) {
+ Log.w(TAG, "Argument filenames are not accessible:\n" +
+ "from: $from\n" +
+ "to: $to")
+ return false
+ }
+
+ return try {
+ val fromFile = File(from)
+ if (fromFile.isDirectory) {
+ fromFile.renameTo(File(to))
+ } else {
+ FileAccessHandler.renameFile(context, storageScopeIdentifier, from, to)
+ }
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ override fun remove(filename: String): Boolean {
+ if (!inScope(filename)) {
+ Log.w(TAG, "Filename $filename is not accessible.")
+ return false
+ }
+
+ return try {
+ val deleteFile = File(filename)
+ if (deleteFile.exists()) {
+ if (deleteFile.isDirectory) {
+ deleteFile.delete()
+ } else {
+ FileAccessHandler.removeFile(context, storageScopeIdentifier, filename)
+ }
+ } else {
+ true
+ }
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
new file mode 100644
index 0000000000..f23537a29e
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
@@ -0,0 +1,183 @@
+/*************************************************************************/
+/* DataAccess.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import org.godotengine.godot.io.StorageScope
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import kotlin.math.max
+
+/**
+ * Base class for file IO operations.
+ *
+ * Its derived instances provide concrete implementations to handle regular file access, as well
+ * as file access through the media store API on versions of Android were scoped storage is enabled.
+ */
+internal abstract class DataAccess(private val filePath: String) {
+
+ companion object {
+ private val TAG = DataAccess::class.java.simpleName
+
+ fun generateDataAccess(
+ storageScope: StorageScope,
+ context: Context,
+ filePath: String,
+ accessFlag: FileAccessFlags
+ ): DataAccess? {
+ return when (storageScope) {
+ StorageScope.APP -> FileData(filePath, accessFlag)
+
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData(context, filePath, accessFlag)
+ } else {
+ null
+ }
+
+ StorageScope.UNKNOWN -> null
+ }
+ }
+
+ fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.fileExists(path)
+ StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+
+ fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.fileLastModified(path)
+ StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
+ StorageScope.UNKNOWN -> 0L
+ }
+ }
+
+ fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.delete(path)
+ StorageScope.SHARED -> MediaStoreData.delete(context, path)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+
+ fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
+ return when(storageScope) {
+ StorageScope.APP -> FileData.rename(from, to)
+ StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
+ StorageScope.UNKNOWN -> false
+ }
+ }
+ }
+
+ protected abstract val fileChannel: FileChannel
+ internal var endOfFile = false
+
+ fun close() {
+ try {
+ fileChannel.close()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when closing file $filePath.", e)
+ }
+ }
+
+ fun flush() {
+ try {
+ fileChannel.force(false)
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when flushing file $filePath.", e)
+ }
+ }
+
+ fun seek(position: Long) {
+ try {
+ fileChannel.position(position)
+ endOfFile = position >= fileChannel.size()
+ } catch (e: Exception) {
+ Log.w(TAG, "Exception when seeking file $filePath.", e)
+ }
+ }
+
+ fun seekFromEnd(positionFromEnd: Long) {
+ val positionFromBeginning = max(0, size() - positionFromEnd)
+ seek(positionFromBeginning)
+ }
+
+ fun position(): Long {
+ return try {
+ fileChannel.position()
+ } catch (e: IOException) {
+ Log.w(
+ TAG,
+ "Exception when retrieving position for file $filePath.",
+ e
+ )
+ 0L
+ }
+ }
+
+ fun size() = try {
+ fileChannel.size()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
+ 0L
+ }
+
+ fun read(buffer: ByteBuffer): Int {
+ return try {
+ val readBytes = fileChannel.read(buffer)
+ endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
+ if (readBytes == -1) {
+ 0
+ } else {
+ readBytes
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while reading from file $filePath.", e)
+ 0
+ }
+ }
+
+ fun write(buffer: ByteBuffer) {
+ try {
+ val writtenBytes = fileChannel.write(buffer)
+ if (writtenBytes > 0) {
+ endOfFile = false
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while writing to file $filePath.", e)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
index ac13cad23e..c6b242a4b6 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
@@ -1,5 +1,5 @@
/*************************************************************************/
-/* GodotGestureHandler.java */
+/* FileAccessFlags.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,69 +28,60 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-package org.godotengine.godot.input;
-
-import org.godotengine.godot.GodotLib;
-import org.godotengine.godot.GodotRenderView;
-
-import android.view.GestureDetector;
-import android.view.MotionEvent;
+package org.godotengine.godot.io.file
/**
- * Handles gesture input related events for the {@link GodotRenderView} view.
- * https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener
+ * Android representation of Godot native access flags.
*/
-public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener {
- private final GodotRenderView mRenderView;
-
- public GodotGestureHandler(GodotRenderView godotView) {
- mRenderView = godotView;
- }
-
- private void queueEvent(Runnable task) {
- mRenderView.queueOnRenderThread(task);
- }
+internal enum class FileAccessFlags(val nativeValue: Int) {
+ /**
+ * Opens the file for read operations.
+ * The cursor is positioned at the beginning of the file.
+ */
+ READ(1),
- @Override
- public boolean onDown(MotionEvent event) {
- super.onDown(event);
- //Log.i("GodotGesture", "onDown");
- return true;
- }
+ /**
+ * Opens the file for write operations.
+ * The file is created if it does not exist, and truncated if it does.
+ */
+ WRITE(2),
- @Override
- public boolean onSingleTapConfirmed(MotionEvent event) {
- super.onSingleTapConfirmed(event);
- return true;
- }
+ /**
+ * Opens the file for read and write operations.
+ * Does not truncate the file. The cursor is positioned at the beginning of the file.
+ */
+ READ_WRITE(3),
- @Override
- public void onLongPress(MotionEvent event) {
- //Log.i("GodotGesture", "onLongPress");
- }
+ /**
+ * Opens the file for read and write operations.
+ * The file is created if it does not exist, and truncated if it does.
+ * The cursor is positioned at the beginning of the file.
+ */
+ WRITE_READ(7);
- @Override
- public boolean onDoubleTap(MotionEvent event) {
- //Log.i("GodotGesture", "onDoubleTap");
- final int x = Math.round(event.getX());
- final int y = Math.round(event.getY());
- final int buttonMask = event.getButtonState();
- GodotLib.doubleTap(buttonMask, x, y);
- return true;
- }
+ fun getMode(): String {
+ return when (this) {
+ READ -> "r"
+ WRITE -> "w"
+ READ_WRITE, WRITE_READ -> "rw"
+ }
+ }
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- //Log.i("GodotGesture", "onScroll");
- final int x = Math.round(distanceX);
- final int y = Math.round(distanceY);
- GodotLib.scroll(x, y);
- return true;
- }
+ fun shouldTruncate(): Boolean {
+ return when (this) {
+ READ, READ_WRITE -> false
+ WRITE, WRITE_READ -> true
+ }
+ }
- @Override
- public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
- //Log.i("GodotGesture", "onFling");
- return true;
- }
+ companion object {
+ fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
+ for (flag in values()) {
+ if (flag.nativeValue == modeFlag) {
+ return flag
+ }
+ }
+ return null
+ }
+ }
}
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
new file mode 100644
index 0000000000..83da3a24b3
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -0,0 +1,208 @@
+/*************************************************************************/
+/* FileAccessHandler.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
+import java.io.FileNotFoundException
+import java.nio.ByteBuffer
+
+/**
+ * Handles regular and media store file access and interactions.
+ */
+class FileAccessHandler(val context: Context) {
+
+ companion object {
+ private val TAG = FileAccessHandler::class.java.simpleName
+
+ private const val FILE_NOT_FOUND_ERROR_ID = -1
+ private const val INVALID_FILE_ID = 0
+ private const val STARTING_FILE_ID = 1
+
+ internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.fileExists(storageScope, context, path!!)
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ internal fun removeFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.removeFile(storageScope, context, path!!)
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(from)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return false
+ }
+
+ return try {
+ DataAccess.renameFile(storageScope, context, from!!, to!!)
+ } catch (e: Exception) {
+ false
+ }
+ }
+ }
+
+ private val storageScopeIdentifier = StorageScope.Identifier(context)
+ private val files = SparseArray<DataAccess>()
+ private var lastFileId = STARTING_FILE_ID
+
+ private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
+
+ fun fileOpen(path: String?, modeFlags: Int): 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)
+ return lastFileId
+ } catch (e: FileNotFoundException) {
+ return FILE_NOT_FOUND_ERROR_ID
+ } catch (e: Exception) {
+ Log.w(TAG, "Error while opening $path", e)
+ return INVALID_FILE_ID
+ }
+ }
+
+ fun fileGetSize(fileId: Int): Long {
+ if (!hasFileId(fileId)) {
+ return 0L
+ }
+
+ return files[fileId].size()
+ }
+
+ fun fileSeek(fileId: Int, position: Long) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].seek(position)
+ }
+
+ fun fileSeekFromEnd(fileId: Int, position: Long) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].seekFromEnd(position)
+ }
+
+ fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int {
+ if (!hasFileId(fileId) || byteBuffer == null) {
+ return 0
+ }
+
+ return files[fileId].read(byteBuffer)
+ }
+
+ fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
+ if (!hasFileId(fileId) || byteBuffer == null) {
+ return
+ }
+
+ files[fileId].write(byteBuffer)
+ }
+
+ fun fileFlush(fileId: Int) {
+ if (!hasFileId(fileId)) {
+ return
+ }
+
+ files[fileId].flush()
+ }
+
+ fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
+
+ fun fileLastModified(filepath: String?): Long {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(filepath)
+ if (storageScope == StorageScope.UNKNOWN) {
+ return 0L
+ }
+
+ return try {
+ DataAccess.fileLastModified(storageScope, context, filepath!!)
+ } catch (e: SecurityException) {
+ 0L
+ }
+ }
+
+ fun fileGetPosition(fileId: Int): Long {
+ if (!hasFileId(fileId)) {
+ return 0L
+ }
+
+ return files[fileId].position()
+ }
+
+ fun isFileEof(fileId: Int): Boolean {
+ if (!hasFileId(fileId)) {
+ return false
+ }
+
+ return files[fileId].endOfFile
+ }
+
+ fun setFileEof(fileId: Int, eof: Boolean) {
+ val file = files[fileId] ?: return
+ file.endOfFile = eof
+ }
+
+ fun fileClose(fileId: Int) {
+ if (hasFileId(fileId)) {
+ files[fileId].close()
+ files.remove(fileId)
+ }
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
index 44f0a3eb3e..5af694ad99 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
@@ -1,5 +1,5 @@
/*************************************************************************/
-/* GodotInstrumentation.java */
+/* FileData.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,23 +28,66 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-package org.godotengine.godot;
+package org.godotengine.godot.io.file
-import android.app.Instrumentation;
-import android.content.Intent;
-import android.os.Bundle;
+import java.io.File
+import java.io.FileOutputStream
+import java.io.RandomAccessFile
+import java.nio.channels.FileChannel
-public class GodotInstrumentation extends Instrumentation {
- private Intent intent;
+/**
+ * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
+ */
+internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
- @Override
- public void onCreate(Bundle arguments) {
- intent = arguments.getParcelable("intent");
- start();
+ companion object {
+ private val TAG = FileData::class.java.simpleName
+
+ fun fileExists(path: String): Boolean {
+ return try {
+ File(path).isFile
+ } catch (e: SecurityException) {
+ false
+ }
+ }
+
+ fun fileLastModified(filepath: String): Long {
+ return try {
+ File(filepath).lastModified()
+ } catch (e: SecurityException) {
+ 0L
+ }
+ }
+
+ fun delete(filepath: String): Boolean {
+ return try {
+ File(filepath).delete()
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ fun rename(from: String, to: String): Boolean {
+ return try {
+ val fromFile = File(from)
+ fromFile.renameTo(File(to))
+ } catch (e: Exception) {
+ false
+ }
+ }
}
- @Override
- public void onStart() {
- startActivitySync(intent);
+ override val fileChannel: FileChannel
+
+ init {
+ if (accessFlag == FileAccessFlags.WRITE) {
+ fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
+ } else {
+ fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
+ }
+
+ if (accessFlag.shouldTruncate()) {
+ fileChannel.truncate(0)
+ }
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
new file mode 100644
index 0000000000..81a7dd1705
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
@@ -0,0 +1,284 @@
+/*************************************************************************/
+/* MediaStoreData.kt */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.annotation.RequiresApi
+
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.nio.channels.FileChannel
+
+/**
+ * Implementation of [DataAccess] which handles access and interactions with file and data
+ * under scoped storage via the MediaStore API.
+ */
+@RequiresApi(Build.VERSION_CODES.Q)
+internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
+ DataAccess(filePath) {
+
+ private data class DataItem(
+ val id: Long,
+ val uri: Uri,
+ val displayName: String,
+ val relativePath: String,
+ val size: Int,
+ val dateModified: Int,
+ val mediaType: Int
+ )
+
+ companion object {
+ private val TAG = MediaStoreData::class.java.simpleName
+
+ private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+
+ private val PROJECTION = arrayOf(
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DISPLAY_NAME,
+ MediaStore.Files.FileColumns.RELATIVE_PATH,
+ MediaStore.Files.FileColumns.SIZE,
+ MediaStore.Files.FileColumns.DATE_MODIFIED,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ )
+
+ private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
+ " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
+
+ private fun getSelectionByPathArguments(path: String): Array<String> {
+ return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
+ }
+
+ private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? "
+
+ private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString())
+
+ private fun getMediaStoreDisplayName(path: String) = File(path).name
+
+ private fun getMediaStoreRelativePath(path: String): String {
+ val pathFile = File(path)
+ val environmentDir = Environment.getExternalStorageDirectory()
+ var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/')
+ if (relativePath.isNotBlank()) {
+ relativePath += "/"
+ }
+ return relativePath
+ }
+
+ private fun queryById(context: Context, id: Long): List<DataItem> {
+ val query = context.contentResolver.query(
+ COLLECTION,
+ PROJECTION,
+ SELECTION_BY_ID,
+ getSelectionByIdArgument(id),
+ null
+ )
+ return dataItemFromCursor(query)
+ }
+
+ private fun queryByPath(context: Context, path: String): List<DataItem> {
+ val query = context.contentResolver.query(
+ COLLECTION,
+ PROJECTION,
+ SELECTION_BY_PATH,
+ getSelectionByPathArguments(path),
+ null
+ )
+ return dataItemFromCursor(query)
+ }
+
+ private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
+ query?.use { cursor ->
+ cursor.count
+ if (cursor.count == 0) {
+ return emptyList()
+ }
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
+ val displayNameColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
+ val relativePathColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
+ val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
+ val dateModifiedColumn =
+ cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
+ val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
+
+ val result = ArrayList<DataItem>()
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(idColumn)
+ result.add(
+ DataItem(
+ id,
+ ContentUris.withAppendedId(COLLECTION, id),
+ cursor.getString(displayNameColumn),
+ cursor.getString(relativePathColumn),
+ cursor.getInt(sizeColumn),
+ cursor.getInt(dateModifiedColumn),
+ cursor.getInt(mediaTypeColumn)
+ )
+ )
+ }
+ return result
+ }
+ return emptyList()
+ }
+
+ private fun addFile(context: Context, path: String): DataItem? {
+ val fileDetails = ContentValues().apply {
+ put(MediaStore.Files.FileColumns._ID, 0)
+ put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path))
+ put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path))
+ }
+
+ context.contentResolver.insert(COLLECTION, fileDetails) ?: return null
+
+ // File was successfully added, let's retrieve its info
+ val infos = queryByPath(context, path)
+ if (infos.isEmpty()) {
+ return null
+ }
+
+ return infos[0]
+ }
+
+ fun delete(context: Context, path: String): Boolean {
+ val itemsToDelete = queryByPath(context, path)
+ if (itemsToDelete.isEmpty()) {
+ return false
+ }
+
+ val resolver = context.contentResolver
+ var itemsDeleted = 0
+ for (item in itemsToDelete) {
+ itemsDeleted += resolver.delete(item.uri, null, null)
+ }
+
+ return itemsDeleted > 0
+ }
+
+ fun fileExists(context: Context, path: String): Boolean {
+ return queryByPath(context, path).isNotEmpty()
+ }
+
+ fun fileLastModified(context: Context, path: String): Long {
+ val result = queryByPath(context, path)
+ if (result.isEmpty()) {
+ return 0L
+ }
+
+ val dataItem = result[0]
+ return dataItem.dateModified.toLong()
+ }
+
+ fun rename(context: Context, from: String, to: String): Boolean {
+ // Ensure the source exists.
+ val sources = queryByPath(context, from)
+ if (sources.isEmpty()) {
+ return false
+ }
+
+ // Take the first source
+ val source = sources[0]
+
+ // Set up the updated values
+ val updatedDetails = ContentValues().apply {
+ put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to))
+ put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to))
+ }
+
+ val updated = context.contentResolver.update(
+ source.uri,
+ updatedDetails,
+ SELECTION_BY_ID,
+ getSelectionByIdArgument(source.id)
+ )
+ return updated > 0
+ }
+ }
+
+ private val id: Long
+ private val uri: Uri
+ override val fileChannel: FileChannel
+
+ init {
+ val contentResolver = context.contentResolver
+ val dataItems = queryByPath(context, filePath)
+
+ val dataItem = when (accessFlag) {
+ FileAccessFlags.READ -> {
+ // The file should already exist
+ if (dataItems.isEmpty()) {
+ throw FileNotFoundException("Unable to access file $filePath")
+ }
+
+ val dataItem = dataItems[0]
+ dataItem
+ }
+
+ FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> {
+ // Create the file if it doesn't exist
+ val dataItem = if (dataItems.isEmpty()) {
+ addFile(context, filePath)
+ } else {
+ dataItems[0]
+ }
+
+ if (dataItem == null) {
+ throw FileNotFoundException("Unable to access file $filePath")
+ }
+ dataItem
+ }
+ }
+
+ id = dataItem.id
+ uri = dataItem.uri
+
+ val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode())
+ ?: throw IllegalStateException("Unable to access file descriptor")
+ fileChannel = if (accessFlag == FileAccessFlags.READ) {
+ FileInputStream(parcelFileDescriptor.fileDescriptor).channel
+ } else {
+ FileOutputStream(parcelFileDescriptor.fileDescriptor).channel
+ }
+
+ if (accessFlag.shouldTruncate()) {
+ fileChannel.truncate(0)
+ }
+ }
+}
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 e5b4f41153..57db0709f0 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,10 +32,14 @@ package org.godotengine.godot.utils;
import android.Manifest;
import android.app.Activity;
+import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
+import android.net.Uri;
import android.os.Build;
+import android.os.Environment;
+import android.provider.Settings;
import android.util.Log;
import androidx.core.content.ContextCompat;
@@ -53,7 +57,8 @@ public final class PermissionsUtil {
static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
static final int REQUEST_CAMERA_PERMISSION = 2;
static final int REQUEST_VIBRATE_PERMISSION = 3;
- static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
+ public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
+ public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
private PermissionsUtil() {
}
@@ -108,13 +113,26 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return true;
- List<String> dangerousPermissions = new ArrayList<>();
+ List<String> requestedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
- dangerousPermissions.add(manifestPermission);
+ if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ } catch (Exception ignored) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
+ }
+ }
+ } else {
+ PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
+ int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
+ if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
+ requestedPermissions.add(manifestPermission);
+ }
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -122,13 +140,12 @@ public final class PermissionsUtil {
}
}
- if (dangerousPermissions.isEmpty()) {
+ if (requestedPermissions.isEmpty()) {
// If list is empty, all of dangerous permissions were granted.
return true;
}
- String[] requestedPermissions = dangerousPermissions.toArray(new String[0]);
- activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE);
+ activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
return false;
}
@@ -148,13 +165,19 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return manifestPermissions;
- List<String> dangerousPermissions = new ArrayList<>();
+ List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
- PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
- int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
- if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
- dangerousPermissions.add(manifestPermission);
+ if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
+ grantedPermissions.add(manifestPermission);
+ }
+ } else {
+ PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
+ int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
+ if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
+ grantedPermissions.add(manifestPermission);
+ }
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -162,7 +185,7 @@ public final class PermissionsUtil {
}
}
- return dangerousPermissions.toArray(new String[0]);
+ return grantedPermissions.toArray(new String[0]);
}
/**
@@ -177,7 +200,7 @@ public final class PermissionsUtil {
if (permission.equals(p))
return true;
}
- } catch (PackageManager.NameNotFoundException e) {
+ } catch (PackageManager.NameNotFoundException ignored) {
}
return false;
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
new file mode 100644
index 0000000000..2cc37b627a
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
@@ -0,0 +1,141 @@
+// clang-format off
+
+/* Third-party library.
+ * Upstream: https://github.com/JakeWharton/ProcessPhoenix
+ * Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4
+ */
+
+/*
+ * Copyright (C) 2014 Jake Wharton
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.godotengine.godot.utils;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Process;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+/**
+ * Process Phoenix facilitates restarting your application process. This should only be used for
+ * things like fundamental state changes in your debug builds (e.g., changing from staging to
+ * production).
+ * <p>
+ * Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance.
+ */
+public final class ProcessPhoenix extends Activity {
+ private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
+ private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
+
+ /**
+ * Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default}
+ * activity as an intent.
+ * <p>
+ * Behavior of the current process after invoking this method is undefined.
+ */
+ public static void triggerRebirth(Context context) {
+ triggerRebirth(context, getRestartIntent(context));
+ }
+
+ /**
+ * Call to restart the application process using the specified intents.
+ * <p>
+ * Behavior of the current process after invoking this method is undefined.
+ */
+ public static void triggerRebirth(Context context, Intent... nextIntents) {
+ if (nextIntents.length < 1) {
+ throw new IllegalArgumentException("intents cannot be empty");
+ }
+ // create a new task for the first activity.
+ nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+
+ Intent intent = new Intent(context, ProcessPhoenix.class);
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
+ intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
+ intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
+ context.startActivity(intent);
+ }
+
+ // -- GODOT start --
+ /**
+ * Finish the activity and kill its process
+ */
+ public static void forceQuit(Activity activity) {
+ forceQuit(activity, Process.myPid());
+ }
+
+ /**
+ * Finish the activity and kill its process
+ * @param activity
+ * @param pid
+ */
+ public static void forceQuit(Activity activity, int pid) {
+ Process.killProcess(pid); // Kill original main process
+ activity.finish();
+ Runtime.getRuntime().exit(0); // Kill kill kill!
+ }
+
+ // -- GODOT end --
+
+ private static Intent getRestartIntent(Context context) {
+ String packageName = context.getPackageName();
+ Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ if (defaultIntent != null) {
+ return defaultIntent;
+ }
+
+ throw new IllegalStateException("Unable to determine default activity for "
+ + packageName
+ + ". Does an activity specify the DEFAULT category in its intent filter?");
+ }
+
+ @Override protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // -- GODOT start --
+ ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
+ startActivities(intents.toArray(new Intent[intents.size()]));
+ forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1));
+ // -- GODOT end --
+ }
+
+ /**
+ * Checks if the current process is a temporary Phoenix Process.
+ * This can be used to avoid initialisation of unused resources or to prevent running code that
+ * is not multi-process ready.
+ *
+ * @return true if the current process is a temporary Phoenix Process
+ */
+ public static boolean isPhoenixProcess(Context context) {
+ int currentPid = Process.myPid();
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses();
+ if (runningProcesses != null) {
+ for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
+ if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle
index 0cb769b539..5e810ae1ba 100644
--- a/platform/android/java/nativeSrcsConfigs/build.gradle
+++ b/platform/android/java/nativeSrcsConfigs/build.gradle
@@ -1,6 +1,7 @@
// Non functional android library used to provide Android Studio editor support to the project.
plugins {
id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
}
android {
@@ -18,6 +19,10 @@ android {
targetCompatibility versions.javaVersion
}
+ kotlinOptions {
+ jvmTarget = versions.javaVersion
+ }
+
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle
index 56e1b6fd3a..466ffebf22 100644
--- a/platform/android/java/settings.gradle
+++ b/platform/android/java/settings.gradle
@@ -1,4 +1,19 @@
// Configure the root project.
+pluginManagement {
+ apply from: 'app/config.gradle'
+
+ plugins {
+ id 'com.android.application' version versions.androidGradlePlugin
+ id 'com.android.library' version versions.androidGradlePlugin
+ id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
+ id 'io.github.gradle-nexus.publish-plugin' version versions.nexusPublishVersion
+ }
+ repositories {
+ gradlePluginPortal()
+ google()
+ }
+}
+
rootProject.name = "Godot"
include ':app'
diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp
index 349c3a3ab3..349ae704f9 100644
--- a/platform/android/java_class_wrapper.cpp
+++ b/platform/android/java_class_wrapper.cpp
@@ -40,7 +40,7 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
}
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, false);
+ ERR_FAIL_NULL_V(env, false);
MethodInfo *method = nullptr;
for (MethodInfo &E : M->value) {
@@ -971,14 +971,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
}
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, Ref<JavaClass>());
+ ERR_FAIL_NULL_V(env, Ref<JavaClass>());
jclass bclass = env->FindClass(p_class.utf8().get_data());
- ERR_FAIL_COND_V(!bclass, Ref<JavaClass>());
+ ERR_FAIL_NULL_V(bclass, Ref<JavaClass>());
jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, getDeclaredMethods);
- ERR_FAIL_COND_V(!methods, Ref<JavaClass>());
+ ERR_FAIL_NULL_V(methods, Ref<JavaClass>());
Ref<JavaClass> java_class = memnew(JavaClass);
@@ -1061,7 +1061,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
if (E->get().param_types.size() != mi.param_types.size()) {
continue;
}
- bool valid = true;
+ bool this_valid = true;
for (int j = 0; j < E->get().param_types.size(); j++) {
Variant::Type _new;
float new_l;
@@ -1070,14 +1070,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
JavaClass::_convert_to_variant_type(E->get().param_types[j], existing, existing_l);
JavaClass::_convert_to_variant_type(mi.param_types[j], _new, new_l);
if (_new != existing) {
- valid = false;
+ this_valid = false;
break;
}
new_likeliness += new_l;
existing_likeliness = existing_l;
}
- if (!valid) {
+ if (!this_valid) {
continue;
}
@@ -1155,10 +1155,10 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) {
singleton = this;
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
- jclass activityClass = env->FindClass("android/app/Activity");
- jmethodID getClassLoader = env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
+ jclass activity = env->FindClass("android/app/Activity");
+ jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;");
classLoader = env->CallObjectMethod(p_activity, getClassLoader);
classLoader = (jclass)env->NewGlobalRef(classLoader);
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
@@ -1168,18 +1168,18 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) {
getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;");
Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
- //
+
bclass = env->FindClass("java/lang/reflect/Method");
getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;");
getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;");
getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
- ///
+
bclass = env->FindClass("java/lang/reflect/Field");
Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;");
Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I");
Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
- // each
+
bclass = env->FindClass("java/lang/Boolean");
Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z");
diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp
index 7ae3a65105..cea64a7f22 100644
--- a/platform/android/java_godot_io_wrapper.cpp
+++ b/platform/android/java_godot_io_wrapper.cpp
@@ -61,7 +61,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc
_get_scaled_density = p_env->GetMethodID(cls, "getScaledDensity", "()F");
_get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D");
_get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;");
- _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V");
+ _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;IIII)V");
_hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V");
_set_screen_orientation = p_env->GetMethodID(cls, "setScreenOrientation", "(I)V");
_get_screen_orientation = p_env->GetMethodID(cls, "getScreenOrientation", "()I");
@@ -80,7 +80,7 @@ jobject GodotIOJavaWrapper::get_instance() {
Error GodotIOJavaWrapper::open_uri(const String &p_uri) {
if (_open_URI) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, ERR_UNAVAILABLE);
+ ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE);
jstring jStr = env->NewStringUTF(p_uri.utf8().get_data());
return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK;
} else {
@@ -91,7 +91,7 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) {
String GodotIOJavaWrapper::get_cache_dir() {
if (_get_cache_dir) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_cache_dir);
return jstring_to_string(s, env);
} else {
@@ -102,7 +102,7 @@ String GodotIOJavaWrapper::get_cache_dir() {
String GodotIOJavaWrapper::get_user_data_dir() {
if (_get_data_dir) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_data_dir);
return jstring_to_string(s, env);
} else {
@@ -113,7 +113,7 @@ String GodotIOJavaWrapper::get_user_data_dir() {
String GodotIOJavaWrapper::get_locale() {
if (_get_locale) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_locale);
return jstring_to_string(s, env);
} else {
@@ -124,7 +124,7 @@ String GodotIOJavaWrapper::get_locale() {
String GodotIOJavaWrapper::get_model() {
if (_get_model) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_model);
return jstring_to_string(s, env);
} else {
@@ -135,7 +135,7 @@ String GodotIOJavaWrapper::get_model() {
int GodotIOJavaWrapper::get_screen_dpi() {
if (_get_screen_DPI) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, 160);
+ ERR_FAIL_NULL_V(env, 160);
return env->CallIntMethod(godot_io_instance, _get_screen_DPI);
} else {
return 160;
@@ -145,7 +145,7 @@ int GodotIOJavaWrapper::get_screen_dpi() {
float GodotIOJavaWrapper::get_scaled_density() {
if (_get_scaled_density) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, 1.0f);
+ ERR_FAIL_NULL_V(env, 1.0f);
return env->CallFloatMethod(godot_io_instance, _get_scaled_density);
} else {
return 1.0f;
@@ -165,8 +165,8 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) {
return fallback;
}
-Array GodotIOJavaWrapper::get_display_cutouts() {
- Array result;
+TypedArray<Rect2> GodotIOJavaWrapper::get_display_cutouts() {
+ TypedArray<Rect2> result;
ERR_FAIL_NULL_V(_get_display_cutouts, result);
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, result);
@@ -202,7 +202,7 @@ Rect2i GodotIOJavaWrapper::get_display_safe_area() {
String GodotIOJavaWrapper::get_unique_id() {
if (_get_unique_id) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_unique_id);
return jstring_to_string(s, env);
} else {
@@ -211,22 +211,22 @@ String GodotIOJavaWrapper::get_unique_id() {
}
bool GodotIOJavaWrapper::has_vk() {
- return (_show_keyboard != 0) && (_hide_keyboard != 0);
+ return (_show_keyboard != nullptr) && (_hide_keyboard != nullptr);
}
-void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
+void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
if (_show_keyboard) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_existing.utf8().get_data());
- env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end);
+ env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_type, p_max_input_length, p_cursor_start, p_cursor_end);
}
}
void GodotIOJavaWrapper::hide_vk() {
if (_hide_keyboard) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_io_instance, _hide_keyboard);
}
}
@@ -234,7 +234,7 @@ void GodotIOJavaWrapper::hide_vk() {
void GodotIOJavaWrapper::set_screen_orientation(int p_orient) {
if (_set_screen_orientation) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_io_instance, _set_screen_orientation, p_orient);
}
}
@@ -242,7 +242,7 @@ void GodotIOJavaWrapper::set_screen_orientation(int p_orient) {
int GodotIOJavaWrapper::get_screen_orientation() {
if (_get_screen_orientation) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, 0);
+ ERR_FAIL_NULL_V(env, 0);
return env->CallIntMethod(godot_io_instance, _get_screen_orientation);
} else {
return 0;
@@ -252,7 +252,7 @@ int GodotIOJavaWrapper::get_screen_orientation() {
String GodotIOJavaWrapper::get_system_dir(int p_dir, bool p_shared_storage) {
if (_get_system_dir) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String("."));
+ ERR_FAIL_NULL_V(env, String("."));
jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_system_dir, p_dir, p_shared_storage);
return jstring_to_string(s, env);
} else {
diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h
index 02c57130ab..24995147d4 100644
--- a/platform/android/java_godot_io_wrapper.h
+++ b/platform/android/java_godot_io_wrapper.h
@@ -28,9 +28,6 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-// note, swapped java and godot around in the file name so all the java
-// wrappers are together
-
#ifndef JAVA_GODOT_IO_WRAPPER_H
#define JAVA_GODOT_IO_WRAPPER_H
@@ -38,7 +35,7 @@
#include <jni.h>
#include "core/math/rect2i.h"
-#include "core/variant/array.h"
+#include "core/variant/typed_array.h"
#include "string_android.h"
// Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++
@@ -78,11 +75,11 @@ public:
int get_screen_dpi();
float get_scaled_density();
float get_screen_refresh_rate(float fallback);
- Array get_display_cutouts();
+ TypedArray<Rect2> get_display_cutouts();
Rect2i get_display_safe_area();
String get_unique_id();
bool has_vk();
- void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end);
+ void show_vk(const String &p_existing, int p_type, int p_max_input_length, int p_cursor_start, int p_cursor_end);
void hide_vk();
int get_vk_height();
void set_vk_height(int p_height);
@@ -91,4 +88,4 @@ public:
String get_system_dir(int p_dir, bool p_shared_storage);
};
-#endif /* !JAVA_GODOT_IO_WRAPPER_H */
+#endif // JAVA_GODOT_IO_WRAPPER_H
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 8ad72b499e..04b69d5b86 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -43,6 +43,7 @@
#include "dir_access_jandroid.h"
#include "display_server_android.h"
#include "file_access_android.h"
+#include "file_access_filesystem_jandroid.h"
#include "jni_utils.h"
#include "main/main.h"
#include "net_socket_android.h"
@@ -62,7 +63,6 @@ static AndroidInputHandler *input_handler = nullptr;
static GodotJavaWrapper *godot_java = nullptr;
static GodotIOJavaWrapper *godot_io_java = nullptr;
-static bool initialized = false;
static SafeNumeric<int> step; // Shared between UI and render threads
static Size2 new_size;
@@ -79,15 +79,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei
}
}
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion) {
- initialized = true;
-
+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) {
JavaVM *jvm;
env->GetJavaVM(&jvm);
// create our wrapper classes
- godot_java = new GodotJavaWrapper(env, activity, godot_instance);
- godot_io_java = new GodotIOJavaWrapper(env, godot_java->get_member_object("io", "Lorg/godotengine/godot/GodotIO;", env));
+ godot_java = new GodotJavaWrapper(env, p_activity, p_godot_instance);
+ godot_io_java = new GodotIOJavaWrapper(env, p_godot_io);
init_thread_jandroid(jvm, env);
@@ -95,16 +93,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en
FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr);
- DirAccessJAndroid::setup(godot_io_java->get_instance());
- NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env));
- TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/godotengine/godot/tts/GodotTTS;", env));
+ 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);
- char wd[500];
- getcwd(wd, 500);
-
- godot_java->on_video_init(env);
+ return godot_java->on_video_init(env);
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) {
@@ -127,7 +123,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env
}
}
-JNIEXPORT void 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) {
setup_android_thread();
const char **cmdline = nullptr;
@@ -137,14 +133,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
cmdlen = env->GetArrayLength(p_cmdline);
if (cmdlen) {
cmdline = (const char **)memalloc((cmdlen + 1) * sizeof(const char *));
- ERR_FAIL_NULL_MSG(cmdline, "Out of memory.");
+ ERR_FAIL_NULL_V_MSG(cmdline, false, "Out of memory.");
cmdline[cmdlen] = nullptr;
j_cmdline = (jstring *)memalloc(cmdlen * sizeof(jstring));
- ERR_FAIL_NULL_MSG(j_cmdline, "Out of memory.");
+ ERR_FAIL_NULL_V_MSG(j_cmdline, false, "Out of memory.");
for (int i = 0; i < cmdlen; i++) {
jstring string = (jstring)env->GetObjectArrayElement(p_cmdline, i);
- const char *rawString = env->GetStringUTFChars(string, 0);
+ const char *rawString = env->GetStringUTFChars(string, nullptr);
cmdline[i] = rawString;
j_cmdline[i] = string;
@@ -163,12 +159,14 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
memfree(cmdline);
}
+ // Note: --help and --version return ERR_HELP, but this should be translated to 0 if exit codes are propagated.
if (err != OK) {
- return; // should exit instead and print the error
+ return false;
}
java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity()));
GDREGISTER_CLASS(JNISingleton);
+ return true;
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height) {
@@ -257,7 +255,17 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env,
return should_swap_buffers;
}
-void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) {
+// Called on the UI thread
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative) {
+ if (step.get() <= 0) {
+ return;
+ }
+
+ input_handler->process_mouse_event(p_event_type, p_button_mask, Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y), p_double_click, p_source_mouse_relative);
+}
+
+// Called on the UI thread
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray position) {
if (step.get() <= 0) {
return;
}
@@ -265,59 +273,30 @@ void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev,
Vector<AndroidInputHandler::TouchPos> points;
for (int i = 0; i < pointer_count; i++) {
jfloat p[3];
- env->GetFloatArrayRegion(positions, i * 3, 3, p);
+ env->GetFloatArrayRegion(position, i * 3, 3, p);
AndroidInputHandler::TouchPos tp;
tp.pos = Point2(p[1], p[2]);
tp.id = (int)p[0];
points.push_back(tp);
}
- if ((input_device & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE || (input_device & AINPUT_SOURCE_MOUSE_RELATIVE) == AINPUT_SOURCE_MOUSE_RELATIVE) {
- input_handler->process_mouse_event(input_device, ev, buttons_mask, points[0].pos, vertical_factor, horizontal_factor);
- } else {
- input_handler->process_touch(ev, pointer, points);
- }
-}
-
-// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position) {
- touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position);
-}
-
-// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FI(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position, jint buttons_mask) {
- touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position, buttons_mask);
-}
-
-// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FIFF(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray position, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) {
- touch_preprocessing(env, clazz, input_device, ev, pointer, pointer_count, position, buttons_mask, vertical_factor, horizontal_factor);
-}
-
-// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jfloat p_x, jfloat p_y) {
- if (step.get() <= 0) {
- return;
- }
- input_handler->process_hover(p_type, Point2(p_x, p_y));
+ input_handler->process_touch_event(ev, pointer, points);
}
// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env, jclass clazz, jint p_button_mask, jint p_x, jint p_y) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor) {
if (step.get() <= 0) {
return;
}
-
- input_handler->process_double_tap(p_button_mask, Point2(p_x, p_y));
+ input_handler->process_magnify(Point2(p_x, p_y), p_factor);
}
// Called on the UI thread
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y) {
+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) {
if (step.get() <= 0) {
return;
}
-
- input_handler->process_scroll(Point2(p_x, p_y));
+ input_handler->process_pan(Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y));
}
// Called on the UI thread
@@ -388,12 +367,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_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_physical_keycode, jint p_unicode, jboolean p_pressed) {
if (step.get() <= 0) {
return;
}
-
- input_handler->process_key_event(p_keycode, p_scancode, p_unicode_char, p_pressed);
+ input_handler->process_key_event(p_keycode, p_physical_keycode, p_unicode, p_pressed);
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) {
@@ -436,7 +414,7 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) {
Object *obj = ObjectDB::get_instance(ObjectID(ID));
- ERR_FAIL_COND(!obj);
+ ERR_FAIL_NULL(obj);
int res = env->PushLocalFrame(16);
ERR_FAIL_COND(res != 0);
@@ -447,27 +425,26 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en
Variant *vlist = (Variant *)alloca(sizeof(Variant) * count);
Variant **vptr = (Variant **)alloca(sizeof(Variant *) * count);
for (int i = 0; i < count; i++) {
- jobject obj = env->GetObjectArrayElement(params, i);
+ jobject jobj = env->GetObjectArrayElement(params, i);
Variant v;
- if (obj) {
- v = _jobject_to_variant(env, obj);
+ if (jobj) {
+ v = _jobject_to_variant(env, jobj);
}
memnew_placement(&vlist[i], Variant);
vlist[i] = v;
vptr[i] = &vlist[i];
- env->DeleteLocalRef(obj);
+ env->DeleteLocalRef(jobj);
}
Callable::CallError err;
obj->callp(str_method, (const Variant **)vptr, count, err);
- // something
env->PopLocalFrame(nullptr);
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) {
Object *obj = ObjectDB::get_instance(ObjectID(ID));
- ERR_FAIL_COND(!obj);
+ ERR_FAIL_NULL(obj);
int res = env->PushLocalFrame(16);
ERR_FAIL_COND(res != 0);
@@ -480,16 +457,16 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *
const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count);
for (int i = 0; i < count; i++) {
- jobject obj = env->GetObjectArrayElement(params, i);
- if (obj) {
- args[i] = _jobject_to_variant(env, obj);
+ jobject jobj = env->GetObjectArrayElement(params, i);
+ if (jobj) {
+ args[i] = _jobject_to_variant(env, jobj);
}
- env->DeleteLocalRef(obj);
+ env->DeleteLocalRef(jobj);
argptrs[i] = &args[i];
}
MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count);
- // something
+
env->PopLocalFrame(nullptr);
}
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index 4f2195942c..09fed15690 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -37,22 +37,19 @@
// 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 void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion);
+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 void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz);
-JNIEXPORT void 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);
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);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz);
-void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask = 0, jfloat vertical_factor = 0, jfloat horizontal_factor = 0);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FI(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3FIFF(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hover(JNIEnv *env, jclass clazz, jint p_type, jfloat p_x, jfloat p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_doubleTap(JNIEnv *env, jclass clazz, jint p_button_mask, jint p_x, jint p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_scroll(JNIEnv *env, jclass clazz, jint p_x, jint p_y);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_scancode, jint p_unicode_char, jboolean p_pressed);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions);
+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_keycode, jint p_physical_keycode, jint p_unicode, jboolean p_pressed);
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);
@@ -72,4 +69,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
}
-#endif /* !JAVA_GODOT_LIB_JNI_H */
+#endif // JAVA_GODOT_LIB_JNI_H
diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp
index 5605d834fa..0153ba96fc 100644
--- a/platform/android/java_godot_view_wrapper.cpp
+++ b/platform/android/java_godot_view_wrapper.cpp
@@ -34,7 +34,7 @@
GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
_godot_view = env->NewGlobalRef(godot_view);
@@ -48,27 +48,27 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) {
}
void GodotJavaViewWrapper::request_pointer_capture() {
- if (_request_pointer_capture != 0) {
+ if (_request_pointer_capture != nullptr) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(_godot_view, _request_pointer_capture);
}
}
void GodotJavaViewWrapper::release_pointer_capture() {
- if (_request_pointer_capture != 0) {
+ if (_request_pointer_capture != nullptr) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(_godot_view, _release_pointer_capture);
}
}
void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) {
- if (_set_pointer_icon != 0) {
+ if (_set_pointer_icon != nullptr) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(_godot_view, _set_pointer_icon, pointer_type);
}
@@ -76,7 +76,7 @@ void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) {
GodotJavaViewWrapper::~GodotJavaViewWrapper() {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
+ ERR_FAIL_NULL(env);
env->DeleteGlobalRef(_godot_view);
env->DeleteGlobalRef(_cls);
diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h
index 6e02c26ae2..c52f459d64 100644
--- a/platform/android/java_godot_view_wrapper.h
+++ b/platform/android/java_godot_view_wrapper.h
@@ -28,8 +28,8 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#ifndef GODOT_JAVA_GODOT_VIEW_WRAPPER_H
-#define GODOT_JAVA_GODOT_VIEW_WRAPPER_H
+#ifndef JAVA_GODOT_VIEW_WRAPPER_H
+#define JAVA_GODOT_VIEW_WRAPPER_H
#include <android/log.h>
#include <jni.h>
@@ -57,4 +57,4 @@ public:
~GodotJavaViewWrapper();
};
-#endif //GODOT_JAVA_GODOT_VIEW_WRAPPER_H
+#endif // JAVA_GODOT_VIEW_WRAPPER_H
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 2c8378e685..07b0d75921 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -58,7 +58,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
}
// get some Godot method pointers...
- _on_video_init = p_env->GetMethodID(godot_class, "onVideoInit", "()V");
+ _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", "()V");
_set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V");
@@ -96,9 +96,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl
if (p_env == nullptr) {
p_env = get_jni_env();
}
-
- ERR_FAIL_COND_V(p_env == nullptr, nullptr);
-
+ 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 {
@@ -109,8 +107,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl
jobject GodotJavaWrapper::get_class_loader() {
if (_get_class_loader) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, nullptr);
-
+ ERR_FAIL_NULL_V(env, nullptr);
return env->CallObjectMethod(activity, _get_class_loader);
} else {
return nullptr;
@@ -122,22 +119,21 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() {
return _godot_view;
}
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, nullptr);
-
+ ERR_FAIL_NULL_V(env, nullptr);
jmethodID godot_view_getter = env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
_godot_view = new GodotJavaViewWrapper(env->CallObjectMethod(godot_instance, godot_view_getter));
return _godot_view;
}
-void GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
+bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
if (_on_video_init) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
- ERR_FAIL_COND(p_env == nullptr);
-
- p_env->CallVoidMethod(godot_instance, _on_video_init);
+ 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) {
@@ -154,7 +150,7 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
- ERR_FAIL_COND(p_env == nullptr);
+ ERR_FAIL_NULL(p_env);
p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started);
}
}
@@ -164,8 +160,7 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
- ERR_FAIL_COND(p_env == nullptr);
-
+ ERR_FAIL_NULL(p_env);
p_env->CallVoidMethod(godot_instance, _restart);
}
}
@@ -175,8 +170,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
if (p_env == nullptr) {
p_env = get_jni_env();
}
- ERR_FAIL_COND(p_env == nullptr);
-
+ ERR_FAIL_NULL(p_env);
p_env->CallVoidMethod(godot_instance, _finish);
}
}
@@ -184,8 +178,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {
if (_set_keep_screen_on) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_instance, _set_keep_screen_on, p_enabled);
}
}
@@ -193,8 +186,7 @@ void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {
void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
if (_alert) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data());
jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data());
env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle);
@@ -203,24 +195,21 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) {
int GodotJavaWrapper::get_gles_version_code() {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, 0);
-
+ 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 != 0;
+ return _get_clipboard != nullptr;
}
String GodotJavaWrapper::get_clipboard() {
if (_get_clipboard) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
-
+ ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard);
return jstring_to_string(s, env);
} else {
@@ -231,8 +220,7 @@ String GodotJavaWrapper::get_clipboard() {
String GodotJavaWrapper::get_input_fallback_mapping() {
if (_get_input_fallback_mapping) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, String());
-
+ ERR_FAIL_NULL_V(env, String());
jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping);
return jstring_to_string(fallback_mapping, env);
} else {
@@ -241,28 +229,26 @@ String GodotJavaWrapper::get_input_fallback_mapping() {
}
bool GodotJavaWrapper::has_set_clipboard() {
- return _set_clipboard != 0;
+ return _set_clipboard != nullptr;
}
void GodotJavaWrapper::set_clipboard(const String &p_text) {
if (_set_clipboard) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
jstring jStr = env->NewStringUTF(p_text.utf8().get_data());
env->CallVoidMethod(godot_instance, _set_clipboard, jStr);
}
}
bool GodotJavaWrapper::has_has_clipboard() {
- return _has_clipboard != 0;
+ return _has_clipboard != nullptr;
}
bool GodotJavaWrapper::has_clipboard() {
if (_has_clipboard) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, false);
-
+ ERR_FAIL_NULL_V(env, false);
return env->CallBooleanMethod(godot_instance, _has_clipboard);
} else {
return false;
@@ -272,8 +258,7 @@ bool GodotJavaWrapper::has_clipboard() {
bool GodotJavaWrapper::request_permission(const String &p_name) {
if (_request_permission) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, false);
-
+ ERR_FAIL_NULL_V(env, false);
jstring jStrName = env->NewStringUTF(p_name.utf8().get_data());
return env->CallBooleanMethod(godot_instance, _request_permission, jStrName);
} else {
@@ -284,8 +269,7 @@ bool GodotJavaWrapper::request_permission(const String &p_name) {
bool GodotJavaWrapper::request_permissions() {
if (_request_permissions) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, false);
-
+ ERR_FAIL_NULL_V(env, false);
return env->CallBooleanMethod(godot_instance, _request_permissions);
} else {
return false;
@@ -296,14 +280,12 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const {
Vector<String> permissions_list;
if (_get_granted_permissions) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, permissions_list);
-
+ ERR_FAIL_NULL_V(env, permissions_list);
jobject permissions_object = env->CallObjectMethod(godot_instance, _get_granted_permissions);
jobjectArray *arr = reinterpret_cast<jobjectArray *>(&permissions_object);
- int i = 0;
jsize len = env->GetArrayLength(*arr);
- for (i = 0; i < len; i++) {
+ for (int i = 0; i < len; i++) {
jstring jstr = (jstring)env->GetObjectArrayElement(*arr, i);
String str = jstring_to_string(jstr, env);
permissions_list.push_back(str);
@@ -316,8 +298,7 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const {
void GodotJavaWrapper::init_input_devices() {
if (_init_input_devices) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_instance, _init_input_devices);
}
}
@@ -325,8 +306,7 @@ void GodotJavaWrapper::init_input_devices() {
jobject GodotJavaWrapper::get_surface() {
if (_get_surface) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, nullptr);
-
+ ERR_FAIL_NULL_V(env, nullptr);
return env->CallObjectMethod(godot_instance, _get_surface);
} else {
return nullptr;
@@ -336,8 +316,7 @@ jobject GodotJavaWrapper::get_surface() {
bool GodotJavaWrapper::is_activity_resumed() {
if (_is_activity_resumed) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND_V(env == nullptr, false);
-
+ ERR_FAIL_NULL_V(env, false);
return env->CallBooleanMethod(godot_instance, _is_activity_resumed);
} else {
return false;
@@ -347,8 +326,7 @@ bool GodotJavaWrapper::is_activity_resumed() {
void GodotJavaWrapper::vibrate(int p_duration_ms) {
if (_vibrate) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms);
}
}
@@ -356,8 +334,7 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) {
void GodotJavaWrapper::create_new_godot_instance(List<String> args) {
if (_create_new_godot_instance) {
JNIEnv *env = get_jni_env();
- ERR_FAIL_COND(env == nullptr);
-
+ ERR_FAIL_NULL(env);
jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF(""));
for (int i = 0; i < args.size(); i++) {
env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data()));
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index f04fda7c3d..0e96a4e1f3 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -28,9 +28,6 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-// note, swapped java and godot around in the file name so all the java
-// wrappers are together
-
#ifndef JAVA_GODOT_WRAPPER_H
#define JAVA_GODOT_WRAPPER_H
@@ -51,27 +48,27 @@ private:
GodotJavaViewWrapper *_godot_view = nullptr;
- jmethodID _on_video_init = 0;
- jmethodID _restart = 0;
- jmethodID _finish = 0;
- jmethodID _set_keep_screen_on = 0;
- jmethodID _alert = 0;
- jmethodID _get_GLES_version_code = 0;
- jmethodID _get_clipboard = 0;
- jmethodID _set_clipboard = 0;
- jmethodID _has_clipboard = 0;
- jmethodID _request_permission = 0;
- jmethodID _request_permissions = 0;
- jmethodID _get_granted_permissions = 0;
- jmethodID _init_input_devices = 0;
- jmethodID _get_surface = 0;
- jmethodID _is_activity_resumed = 0;
- jmethodID _vibrate = 0;
- jmethodID _get_input_fallback_mapping = 0;
- jmethodID _on_godot_setup_completed = 0;
- jmethodID _on_godot_main_loop_started = 0;
- jmethodID _get_class_loader = 0;
- jmethodID _create_new_godot_instance = 0;
+ 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;
+ jmethodID _request_permission = nullptr;
+ jmethodID _request_permissions = nullptr;
+ jmethodID _get_granted_permissions = 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;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -83,7 +80,7 @@ public:
jobject get_class_loader();
GodotJavaViewWrapper *get_godot_view();
- void on_video_init(JNIEnv *p_env = nullptr);
+ 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);
@@ -108,4 +105,4 @@ public:
void create_new_godot_instance(List<String> args);
};
-#endif /* !JAVA_GODOT_WRAPPER_H */
+#endif // JAVA_GODOT_WRAPPER_H
diff --git a/platform/android/jni_utils.cpp b/platform/android/jni_utils.cpp
index e2573d10f8..193ef61264 100644
--- a/platform/android/jni_utils.cpp
+++ b/platform/android/jni_utils.cpp
@@ -123,10 +123,10 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a
for (int j = 0; j < keys.size(); j++) {
Variant var = dict[keys[j]];
- jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true);
- env->SetObjectArrayElement(jvalues, j, v.val.l);
- if (v.obj) {
- env->DeleteLocalRef(v.obj);
+ jvalret valret = _variant_to_jvalue(env, var.get_type(), &var, true);
+ env->SetObjectArrayElement(jvalues, j, valret.val.l);
+ if (valret.obj) {
+ env->DeleteLocalRef(valret.obj);
}
}
@@ -186,7 +186,7 @@ String _get_class_name(JNIEnv *env, jclass cls, bool *array) {
if (array) {
jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z");
jboolean isarr = env->CallBooleanMethod(cls, isArray);
- (*array) = isarr ? true : false;
+ (*array) = isarr != 0;
}
String name = jstring_to_string(clsName, env);
env->DeleteLocalRef(clsName);
diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp
index a65e7c6724..225a1132fe 100644
--- a/platform/android/net_socket_android.cpp
+++ b/platform/android/net_socket_android.cpp
@@ -32,10 +32,10 @@
#include "thread_jandroid.h"
-jobject NetSocketAndroid::net_utils = 0;
-jclass NetSocketAndroid::cls = 0;
-jmethodID NetSocketAndroid::_multicast_lock_acquire = 0;
-jmethodID NetSocketAndroid::_multicast_lock_release = 0;
+jobject NetSocketAndroid::net_utils = nullptr;
+jclass NetSocketAndroid::cls = nullptr;
+jmethodID NetSocketAndroid::_multicast_lock_acquire = nullptr;
+jmethodID NetSocketAndroid::_multicast_lock_release = nullptr;
void NetSocketAndroid::setup(jobject p_net_utils) {
JNIEnv *env = get_jni_env();
diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h
index 3e919497ea..97a611cb04 100644
--- a/platform/android/net_socket_android.h
+++ b/platform/android/net_socket_android.h
@@ -74,4 +74,4 @@ public:
~NetSocketAndroid();
};
-#endif
+#endif // NET_SOCKET_ANDROID_H
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 25daf1ca90..4469c7a0f7 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -40,9 +40,11 @@
#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"
@@ -76,9 +78,7 @@ public:
};
void OS_Android::alert(const String &p_alert, const String &p_title) {
- GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
- ERR_FAIL_COND(!godot_java);
-
+ ERR_FAIL_NULL(godot_java);
godot_java->alert(p_alert, p_title);
}
@@ -95,7 +95,7 @@ void OS_Android::initialize_core() {
}
#endif
FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);
- FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM);
+ FileAccess::make_default<FileAccessFilesystemJAndroid>(FileAccess::ACCESS_FILESYSTEM);
#ifdef TOOLS_ENABLED
DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES);
@@ -106,9 +106,8 @@ void OS_Android::initialize_core() {
DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES);
}
#endif
-
DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA);
- DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM);
+ DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_FILESYSTEM);
NetSocketAndroid::make_default();
}
@@ -164,7 +163,7 @@ Vector<String> OS_Android::get_granted_permissions() const {
Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
p_library_handle = dlopen(p_path.utf8().get_data(), RTLD_NOW);
- ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + ".");
+ ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + ".");
if (r_resolved_path != nullptr) {
*r_resolved_path = p_path;
@@ -177,6 +176,79 @@ String OS_Android::get_name() const {
return "Android";
}
+String OS_Android::get_system_property(const char *key) const {
+ static String value;
+ char value_str[PROP_VALUE_MAX];
+ if (__system_property_get(key, value_str)) {
+ value = String(value_str);
+ }
+ return value;
+}
+
+String OS_Android::get_distribution_name() const {
+ if (!get_system_property("ro.havoc.version").is_empty()) {
+ return "Havoc OS";
+ } else if (!get_system_property("org.pex.version").is_empty()) { // Putting before "Pixel Experience", because it's derivating from it.
+ return "Pixel Extended";
+ } else if (!get_system_property("org.pixelexperience.version").is_empty()) {
+ return "Pixel Experience";
+ } else if (!get_system_property("ro.potato.version").is_empty()) {
+ return "POSP";
+ } else if (!get_system_property("ro.xtended.version").is_empty()) {
+ return "Project-Xtended";
+ } else if (!get_system_property("org.evolution.version").is_empty()) {
+ return "Evolution X";
+ } else if (!get_system_property("ro.corvus.version").is_empty()) {
+ return "Corvus-Q";
+ } else if (!get_system_property("ro.pa.version").is_empty()) {
+ return "Paranoid Android";
+ } else if (!get_system_property("ro.crdroid.version").is_empty()) {
+ return "crDroid Android";
+ } else if (!get_system_property("ro.syberia.version").is_empty()) {
+ return "Syberia Project";
+ } else if (!get_system_property("ro.arrow.version").is_empty()) {
+ return "ArrowOS";
+ } else if (!get_system_property("ro.lineage.version").is_empty()) { // Putting LineageOS last, just in case any derivative writes to "ro.lineage.version".
+ return "LineageOS";
+ }
+
+ if (!get_system_property("ro.modversion").is_empty()) { // Handles other Android custom ROMs.
+ return vformat("%s %s", get_name(), "Custom ROM");
+ }
+
+ // Handles stock Android.
+ return get_name();
+}
+
+String OS_Android::get_version() const {
+ const Vector<const char *> roms = { "ro.havoc.version", "org.pex.version", "org.pixelexperience.version",
+ "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]);
+ if (!rom_version.is_empty()) {
+ return rom_version;
+ }
+ }
+
+ static 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");
+ if (!sdk_version.is_empty()) {
+ if (!build.is_empty()) {
+ return vformat("%s.%s", sdk_version, build);
+ }
+ return sdk_version;
+ }
+
+ return "";
+}
+
MainLoop *OS_Android::get_main_loop() const {
return main_loop;
}
@@ -303,6 +375,33 @@ String OS_Android::get_system_dir(SystemDir p_dir, bool p_shared_storage) const
return godot_io_java->get_system_dir(p_dir, p_shared_storage);
}
+Error OS_Android::move_to_trash(const String &p_path) {
+ Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path);
+ if (da_ref.is_null()) {
+ return FAILED;
+ }
+
+ // Check if it's a directory
+ if (da_ref->dir_exists(p_path)) {
+ Error err = da_ref->change_dir(p_path);
+ if (err) {
+ return err;
+ }
+ // This is directory, let's erase its contents
+ err = da_ref->erase_contents_recursive();
+ if (err) {
+ return err;
+ }
+ // Remove the top directory
+ return da_ref->remove(p_path);
+ } else if (da_ref->file_exists(p_path)) {
+ // This is a file, let's remove it.
+ return da_ref->remove(p_path);
+ } else {
+ return FAILED;
+ }
+}
+
void OS_Android::set_display_size(const Size2i &p_size) {
display_size = p_size;
}
@@ -313,7 +412,7 @@ Size2i OS_Android::get_display_size() const {
void OS_Android::set_opengl_extensions(const char *p_gl_extensions) {
#if defined(GLES3_ENABLED)
- ERR_FAIL_COND(!p_gl_extensions);
+ ERR_FAIL_NULL(p_gl_extensions);
gl_extensions = p_gl_extensions;
#endif
}
@@ -337,7 +436,7 @@ void OS_Android::vibrate_handheld(int p_duration_ms) {
}
String OS_Android::get_config_path() const {
- return get_user_data_dir().plus_file("config");
+ return get_user_data_dir().path_join("config");
}
bool OS_Android::_check_internal_feature_support(const String &p_feature) {
@@ -345,15 +444,15 @@ bool OS_Android::_check_internal_feature_support(const String &p_feature) {
return true;
}
#if defined(__aarch64__)
- if (p_feature == "arm64-v8a") {
+ if (p_feature == "arm64-v8a" || p_feature == "arm64") {
return true;
}
#elif defined(__ARM_ARCH_7A__)
- if (p_feature == "armeabi-v7a" || p_feature == "armeabi") {
+ if (p_feature == "armeabi-v7a" || p_feature == "armeabi" || p_feature == "arm32") {
return true;
}
#elif defined(__arm__)
- if (p_feature == "armeabi") {
+ if (p_feature == "armeabi" || p_feature == "arm") {
return true;
}
#endif
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index f86c5b5212..d6546a3507 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -65,6 +65,8 @@ private:
GodotJavaWrapper *godot_java = nullptr;
GodotIOJavaWrapper *godot_io_java = nullptr;
+ String get_system_property(const char *key) const;
+
public:
static const char *ANDROID_EXEC_PATH;
@@ -93,6 +95,8 @@ public:
virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override;
virtual String get_name() const override;
+ virtual String get_distribution_name() const override;
+ virtual String get_version() const override;
virtual MainLoop *get_main_loop() const override;
void main_loop_begin();
@@ -122,6 +126,8 @@ public:
virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override;
+ virtual Error move_to_trash(const String &p_path) override;
+
void vibrate_handheld(int p_duration_ms) override;
virtual String get_config_path() const override;
@@ -135,4 +141,4 @@ public:
~OS_Android();
};
-#endif
+#endif // OS_ANDROID_H
diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp
index 158512803a..7a39e2003d 100644
--- a/platform/android/plugin/godot_plugin_jni.cpp
+++ b/platform/android/plugin/godot_plugin_jni.cpp
@@ -123,7 +123,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS
variant_params[i] = _jobject_to_variant(env, j_param);
args[i] = &variant_params[i];
env->DeleteLocalRef(j_param);
- };
+ }
singleton->emit_signalp(StringName(signal_name), args, count);
}
diff --git a/platform/android/string_android.h b/platform/android/string_android.h
index 2afaa86fec..79c71b5d04 100644
--- a/platform/android/string_android.h
+++ b/platform/android/string_android.h
@@ -30,6 +30,7 @@
#ifndef STRING_ANDROID_H
#define STRING_ANDROID_H
+
#include "core/string/ustring.h"
#include "thread_jandroid.h"
#include <jni.h>
diff --git a/platform/android/thread_jandroid.h b/platform/android/thread_jandroid.h
index 2073423f8d..3b000517fd 100644
--- a/platform/android/thread_jandroid.h
+++ b/platform/android/thread_jandroid.h
@@ -38,4 +38,4 @@ void init_thread_jandroid(JavaVM *p_jvm, JNIEnv *p_env);
void setup_android_thread();
JNIEnv *get_jni_env();
-#endif
+#endif // THREAD_JANDROID_H
diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp
index d3f3773344..27ba8da448 100644
--- a/platform/android/tts_android.cpp
+++ b/platform/android/tts_android.cpp
@@ -35,16 +35,16 @@
#include "string_android.h"
#include "thread_jandroid.h"
-jobject TTS_Android::tts = 0;
-jclass TTS_Android::cls = 0;
-
-jmethodID TTS_Android::_is_speaking = 0;
-jmethodID TTS_Android::_is_paused = 0;
-jmethodID TTS_Android::_get_voices = 0;
-jmethodID TTS_Android::_speak = 0;
-jmethodID TTS_Android::_pause_speaking = 0;
-jmethodID TTS_Android::_resume_speaking = 0;
-jmethodID TTS_Android::_stop_speaking = 0;
+jobject TTS_Android::tts = nullptr;
+jclass TTS_Android::cls = nullptr;
+
+jmethodID TTS_Android::_is_speaking = nullptr;
+jmethodID TTS_Android::_is_paused = nullptr;
+jmethodID TTS_Android::_get_voices = nullptr;
+jmethodID TTS_Android::_speak = nullptr;
+jmethodID TTS_Android::_pause_speaking = nullptr;
+jmethodID TTS_Android::_resume_speaking = nullptr;
+jmethodID TTS_Android::_stop_speaking = nullptr;
HashMap<int, Char16String> TTS_Android::ids;