diff options
Diffstat (limited to 'platform/android')
105 files changed, 8668 insertions, 2118 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 d031d14499..e4d04f1df9 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -6,8 +6,10 @@ 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", "thread_jandroid.cpp", "net_socket_android.cpp", "java_godot_lib_jni.cpp", @@ -39,24 +41,31 @@ 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") if lib_arch_dir != "": - if env["target"] == "release": - lib_type_dir = "release" - else: # release_debug, debug + if env.dev_build: + lib_type_dir = "dev" + elif env.debug_features: lib_type_dir = "debug" + else: # Release + lib_type_dir = "release" + + if env.editor_build: + lib_tools_dir = "tools/" + else: + lib_tools_dir = "" - out_dir = "#platform/android/java/lib/libs/" + lib_type_dir + "/" + lib_arch_dir + out_dir = "#platform/android/java/lib/libs/" + lib_tools_dir + lib_type_dir + "/" + lib_arch_dir env_android.Command( out_dir + "/libgodot_android.so", "#bin/libgodot" + env["SHLIBSUFFIX"], Move("$TARGET", "$SOURCE") ) diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index 10f23b320b..c0b098cd7f 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,32 @@ 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, bool p_double_tap) { + 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); + ev->set_double_tap(p_double_tap); + Input::get_singleton()->parse_input_event(ev); + } + } +} + +void AndroidInputHandler::_release_all_touch() { + _parse_all_touch(false, false); + touch.clear(); +} + +void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const Vector<TouchPos> &p_points, bool p_double_tap) { 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 +152,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, p_double_tap); } 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 +187,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 +225,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 +355,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 +410,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..4da8a910c0 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, bool p_double_tap); + + 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, bool p_double_tap); + 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/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index 96b7b48e48..ac8d6585d3 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -63,7 +63,7 @@ class JavaClass : public RefCounted { ARG_TYPE_MASK = (1 << 16) - 1 }; - Map<StringName, Variant> constant_map; + RBMap<StringName, Variant> constant_map; struct MethodInfo { bool _static = false; @@ -174,7 +174,7 @@ class JavaClass : public RefCounted { bool _call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret); friend class JavaClassWrapper; - Map<StringName, List<MethodInfo>> methods; + HashMap<StringName, List<MethodInfo>> methods; jclass _class; #endif @@ -207,7 +207,7 @@ class JavaClassWrapper : public Object { GDCLASS(JavaClassWrapper, Object); #ifdef ANDROID_ENABLED - Map<String, Ref<JavaClass>> class_cache; + RBMap<String, Ref<JavaClass>> class_cache; friend class JavaClass; jclass activityClass; jmethodID findClass; diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 74ca10e5e2..895bc70103 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -48,13 +48,13 @@ class JNISingleton : public Object { }; jobject instance; - Map<StringName, MethodData> method_map; + RBMap<StringName, MethodData> method_map; #endif public: virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { #ifdef ANDROID_ENABLED - Map<StringName, MethodData>::Element *E = method_map.find(p_method); + RBMap<StringName, MethodData>::Element *E = method_map.find(p_method); // Check the method we're looking for is in the JNISingleton map and that // the arguments match. @@ -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; @@ -150,9 +150,8 @@ public: env->DeleteLocalRef(arr); } break; -#ifndef _MSC_VER -#warning This is missing 64 bits arrays, I have no idea how to do it in JNI -#endif + // TODO: This is missing 64 bits arrays, I have no idea how to do it in JNI. + case Variant::DICTIONARY: { jobject obj = env->CallObjectMethodA(instance, E->get().method, v); ret = _jobject_to_variant(env, obj); diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 8495d2cc18..6b22a0ffa1 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -75,13 +75,11 @@ void AudioDriverOpenSL::_buffer_callback( void AudioDriverOpenSL::_buffer_callbacks( SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); 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 }; @@ -208,7 +199,7 @@ void AudioDriverOpenSL::_record_buffer_callback(SLAndroidSimpleBufferQueueItf qu } void AudioDriverOpenSL::_record_buffer_callbacks(SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); ad->_record_buffer_callback(queueItf); } @@ -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 3319d5890c..6eb8ba34ed 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -1,7 +1,12 @@ import os import sys import platform -from distutils.version import LooseVersion +import subprocess + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from SCons import Environment def is_active(): @@ -13,202 +18,123 @@ 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 [ - ("tools", False), + ("arch", "arm64"), # Default for convenience. + ("target", "template_debug"), ] -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: "Environment"): + # 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 - - if env["target"].startswith("release"): - if env["optimize"] == "speed": # optimize for speed (default) - env.Append(LINKFLAGS=["-O2"]) - env.Append(CCFLAGS=["-O2", "-fomit-frame-pointer"]) - 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"]) - 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"]) + # 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"): @@ -221,165 +147,50 @@ 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" - - # 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"]) - - # Disable exceptions and rtti on non-tools (template) builds - if env["tools"]: - env.Append(CXXFLAGS=["-frtti"]) - elif env["builtin_icu"]: - env.Append(CXXFLAGS=["-frtti", "-fno-exceptions"]) - else: - env.Append(CXXFLAGS=["-fno-rtti", "-fno-exceptions"]) - # 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"])))]) + toolchain_path = ndk_root + "/toolchains/llvm/prebuilt/" + host_subpath + compiler_path = toolchain_path + "/bin" + + 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 on template builds + if not env.editor_build: + env.Append(CXXFLAGS=["-fno-exceptions"]) env.Append( CCFLAGS=( - "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden" - " -fno-strict-aliasing".split() + "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() ) ) - env.Append(CPPDEFINES=["NO_STATVFS", "GLES_ENABLED"]) + env.Append(CPPDEFINES=["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"]) + env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED"]) env.Append(LIBS=["OpenSLES", "EGL", "GLESv2", "android", "log", "z", "dl"]) if env["vulkan"]: 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 7fb4f54fca..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; - -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) { - 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; } - - FileAccessAndroid *f = memnew(FileAccessAndroid); - bool exists = f->file_exists(sd); - memdelete(f); - - 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; - } + if (_make_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); - env->CallVoidMethod(io, _dir_close, res); - - 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 4f4a984b12..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 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); ///< 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 a7a8801bdc..08369e735d 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -34,6 +34,7 @@ #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" +#include "tts_android.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" @@ -42,7 +43,7 @@ #endif DisplayServerAndroid *DisplayServerAndroid::get_singleton() { - return (DisplayServerAndroid *)DisplayServer::get_singleton(); + return static_cast<DisplayServerAndroid *>(DisplayServer::get_singleton()); } bool DisplayServerAndroid::has_feature(Feature p_feature) const { @@ -63,6 +64,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -73,9 +75,37 @@ String DisplayServerAndroid::get_name() const { return "Android"; } +bool DisplayServerAndroid::tts_is_speaking() const { + return TTS_Android::is_speaking(); +} + +bool DisplayServerAndroid::tts_is_paused() const { + return TTS_Android::is_paused(); +} + +TypedArray<Dictionary> DisplayServerAndroid::tts_get_voices() const { + return TTS_Android::get_voices(); +} + +void DisplayServerAndroid::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + TTS_Android::speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerAndroid::tts_pause() { + TTS_Android::pause(); +} + +void DisplayServerAndroid::tts_resume() { + TTS_Android::resume(); +} + +void DisplayServerAndroid::tts_stop() { + TTS_Android::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); @@ -86,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(); @@ -97,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(); @@ -106,9 +136,21 @@ bool DisplayServerAndroid::clipboard_has() 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(); +} + +Rect2i DisplayServerAndroid::get_display_safe_area() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Rect2i()); + return godot_io_java->get_display_safe_area(); +} + 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; @@ -120,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"); @@ -147,20 +189,24 @@ Size2i DisplayServerAndroid::screen_get_size(int p_screen) const { } Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const { - GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, Rect2i()); - int xywh[4]; - godot_io_java->screen_get_usable_rect(xywh); - return Rect2i(xywh[0], xywh[1], xywh[2], xywh[3]); + Size2i display_size = OS_Android::get_singleton()->get_display_size(); + return Rect2i(0, 0, display_size.width, display_size.height); } 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_NULL_V(godot_io_java, 1.0f); + + return godot_io_java->get_scaled_density(); +} + float DisplayServerAndroid::screen_get_refresh_rate(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); if (!godot_io_java) { @@ -175,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"); } @@ -188,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(); @@ -199,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(); } @@ -230,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); } } } @@ -270,7 +316,7 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type, return 0; // Not supported. } case WINDOW_HANDLE: { - return (int64_t)((OS_Android *)OS::get_singleton())->get_godot_java()->get_activity(); + return reinterpret_cast<int64_t>(static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity()); } case WINDOW_VIEW: { return 0; // Not supported. @@ -394,8 +440,8 @@ Vector<String> DisplayServerAndroid::get_rendering_drivers_func() { return drivers; } -DisplayServer *DisplayServerAndroid::create_func(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) { - DisplayServer *ds = memnew(DisplayServerAndroid(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_resolution, r_error)); +DisplayServer *DisplayServerAndroid::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, Error &r_error) { + DisplayServer *ds = memnew(DisplayServerAndroid(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, r_error)); if (r_error != OK) { OS::get_singleton()->alert("Your video card driver does not support any of the supported Vulkan versions.", "Unable to initialize Video driver"); } @@ -410,14 +456,14 @@ 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); Size2i display_size = OS_Android::get_singleton()->get_display_size(); - if (context_vulkan->window_create(native_window, last_vsync_mode, display_size.width, display_size.height) == -1) { + if (context_vulkan->window_create(native_window, last_vsync_mode, display_size.width, display_size.height) != OK) { memdelete(context_vulkan); context_vulkan = nullptr; ERR_FAIL_MSG("Failed to reset Vulkan window."); @@ -436,10 +482,10 @@ 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) { +DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, Error &r_error) { rendering_driver = p_rendering_driver; // TODO: rendering_driver is broken, change when different drivers are supported again @@ -473,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) { @@ -483,7 +529,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis } Size2i display_size = OS_Android::get_singleton()->get_display_size(); - if (context_vulkan->window_create(native_window, p_vsync_mode, display_size.width, display_size.height) == -1) { + if (context_vulkan->window_create(native_window, p_vsync_mode, display_size.width, display_size.height) != OK) { memdelete(context_vulkan); context_vulkan = nullptr; ERR_FAIL_MSG("Failed to create Vulkan window."); @@ -534,6 +580,9 @@ void DisplayServerAndroid::process_gyroscope(const Vector3 &p_gyroscope) { } void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) { + if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon() || !OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_capture_pointer()) { + return; + } if (mouse_mode == p_mode) { return; } @@ -566,6 +615,9 @@ MouseButton DisplayServerAndroid::mouse_get_button_state() const { } void DisplayServerAndroid::cursor_set_shape(DisplayServer::CursorShape p_shape) { + if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon()) { + return; + } if (cursor_shape == p_shape) { return; } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 23077a6529..a6bc88e048 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -91,10 +91,22 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() 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; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; virtual bool clipboard_has() 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; virtual bool screen_is_kept_on() const override; @@ -106,10 +118,11 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; 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; @@ -181,7 +194,7 @@ public: virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; - static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, Error &r_error); static Vector<String> get_rendering_drivers_func(); static void register_android_driver(); @@ -191,7 +204,7 @@ public: virtual Point2i mouse_get_position() const override; virtual MouseButton mouse_get_button_state() const override; - DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + DisplayServerAndroid(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, Error &r_error); ~DisplayServerAndroid(); }; 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 df3693ba61..3bfdd3b881 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,11 +246,11 @@ 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 = (EditorExportPlatformAndroid *)ud; + EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); while (!ea->quit_request.is_set()) { // Check for plugins updates @@ -400,7 +401,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } - if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) { + if (EDITOR_GET("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { return; //adb not configured @@ -409,15 +410,16 @@ 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; if (!p_name.is_empty()) { aname = p_name; } else { - aname = ProjectSettings::get_singleton()->get("application/config/name"); + aname = GLOBAL_GET("application/config/name"); } if (aname.is_empty()) { @@ -429,7 +431,7 @@ String EditorExportPlatformAndroid::get_project_name(const String &p_name) const String EditorExportPlatformAndroid::get_package_name(const String &p_package) const { String pname = p_package; - String basename = ProjectSettings::get_singleton()->get("application/config/name"); + String basename = GLOBAL_GET("application/config/name"); basename = basename.to_lower(); String name; @@ -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; @@ -596,8 +597,8 @@ Vector<String> EditorExportPlatformAndroid::get_abis() { /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); @@ -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 { @@ -685,7 +686,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj ERR_PRINT(err); return FAILED; } - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); Vector<String> abis = get_abis(); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { @@ -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 + "'."); @@ -710,7 +711,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj } Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", "assets/"); store_in_apk(ed, dst_path, p_data, _should_compress_asset(p_path, p_data) ? Z_DEFLATED : 0); @@ -725,7 +726,7 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED, "Android .so file names must start with \"lib\", but got: " + p_so.path); Vector<String> abis = get_abis(); - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { int abi_index = abis.find(p_so.tags[i]); @@ -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; @@ -863,11 +865,12 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p bool classify_as_game = p_preset->get("package/classify_as_game"); bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall"); bool exclude_from_recents = p_preset->get("package/exclude_from_recents"); + bool is_resizeable = bool(GLOBAL_GET("display/window/size/resizable")); 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]); @@ -878,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); @@ -953,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") { @@ -968,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]); } @@ -980,6 +977,10 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]); } + if (tname == "activity" && attrname == "resizeableActivity") { + encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]); + } + if (tname == "supports-screens") { if (attrname == "smallScreens") { encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); @@ -995,16 +996,23 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } } - if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + // Hand tracking related configurations + if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { string_table.write[attr_value] = "com.oculus.handtracking.frequency"; } - } - if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { string_table.write[attr_value] = (hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"); } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_name") { + string_table.write[attr_value] = "com.oculus.handtracking.version"; + } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_value") { + string_table.write[attr_value] = "V2.0"; + } } iofs += 20; @@ -1387,7 +1395,7 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> & Vector<String> string_table; String package_name = p_preset->get("package/name"); - Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); + Dictionary appnames = GLOBAL_GET("application/config/name_localized"); for (uint32_t i = 0; i < string_count; i++) { uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]); @@ -1497,9 +1505,9 @@ void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_n } String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) { - bool scale_splash = ProjectSettings::get_singleton()->get("application/boot_splash/fullsize"); - bool apply_filter = ProjectSettings::get_singleton()->get("application/boot_splash/use_filter"); - String project_splash_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + bool scale_splash = GLOBAL_GET("application/boot_splash/fullsize"); + bool apply_filter = GLOBAL_GET("application/boot_splash/use_filter"); + String project_splash_path = GLOBAL_GET("application/boot_splash/image"); if (!project_splash_path.is_empty()) { splash_image.instantiate(); @@ -1520,7 +1528,7 @@ String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, R } if (scale_splash) { - Size2 screen_size = Size2(ProjectSettings::get_singleton()->get("display/window/size/viewport_width"), ProjectSettings::get_singleton()->get("display/window/size/viewport_height")); + Size2 screen_size = Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height")); int width, height; if (screen_size.width > screen_size.height) { // scale horizontally @@ -1543,7 +1551,7 @@ String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, R print_verbose("Creating splash background color image."); splash_bg_color_image.instantiate(); - splash_bg_color_image->create(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); + splash_bg_color_image->initialize_data(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); splash_bg_color_image->fill(bg_color); String processed_splash_config_xml = vformat(SPLASH_CONFIG_XML_CONTENT, bool_to_string(apply_filter)); @@ -1551,7 +1559,7 @@ String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, R } void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background) { - String project_icon_path = ProjectSettings::get_singleton()->get("application/config/icon"); + String project_icon_path = GLOBAL_GET("application/config/icon"); icon.instantiate(); foreground.instantiate(); @@ -1660,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) { @@ -1679,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"), "")); @@ -1707,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]"), "")); @@ -1746,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++; } } @@ -1815,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; } @@ -1837,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); @@ -1893,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); } @@ -1913,7 +1920,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, print_verbose(output); if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) { - int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port"); + int dbg_port = EDITOR_GET("network/debug/remote_port"); args.clear(); args.push_back("-s"); args.push_back(devices[p_device].id); @@ -1928,7 +1935,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, } if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) { - int fs_port = EditorSettings::get_singleton()->get("filesystem/file_server/port"); + int fs_port = EDITOR_GET("filesystem/file_server/port"); args.clear(); args.push_back("-s"); @@ -1958,7 +1965,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, args.push_back("shell"); args.push_back("am"); args.push_back("start"); - if ((bool)EditorSettings::get_singleton()->get("export/android/force_system_user") && devices[p_device].api_level >= 17) { // Multi-user introduced in Android 17 + if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device].api_level >= 17) { // Multi-user introduced in Android 17 args.push_back("--user"); args.push_back("0"); } @@ -1971,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); } @@ -1988,8 +1995,8 @@ String EditorExportPlatformAndroid::get_adb_path() { if (OS::get_singleton()->get_name() == "Windows") { 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); + String sdk_path = EDITOR_GET("export/android/android_sdk_path"); + return sdk_path.path_join("platform-tools/adb" + exe_ext); } String EditorExportPlatformAndroid::get_apksigner_path() { @@ -1998,12 +2005,12 @@ String EditorExportPlatformAndroid::get_apksigner_path() { exe_ext = ".bat"; } String apksigner_command_name = "apksigner" + exe_ext; - String sdk_path = EditorSettings::get_singleton()->get("export/android/android_sdk_path"); + String sdk_path = EDITOR_GET("export/android/android_sdk_path"); String apksigner_path = ""; Error errn; - String build_tools_dir = sdk_path.plus_file("build-tools"); - DirAccessRef da = DirAccess::open(build_tools_dir, &errn); + 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."); return apksigner_path; @@ -2015,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; @@ -2026,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). @@ -2080,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"); @@ -2092,7 +2099,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } if (!FileAccess::exists(dk)) { - dk = EditorSettings::get_singleton()->get("export/android/debug_keystore"); + dk = EDITOR_GET("export/android/debug_keystore"); if (!FileAccess::exists(dk)) { valid = false; err += TTR("Debug keystore not configured in the Editor Settings nor in the preset.") + "\n"; @@ -2113,14 +2120,14 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr err += TTR("Release keystore incorrectly configured in the export preset.") + "\n"; } - String sdk_path = EditorSettings::get_singleton()->get("export/android/android_sdk_path"); + String sdk_path = EDITOR_GET("export/android/android_sdk_path"); if (sdk_path.is_empty()) { err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n"; valid = false; } else { Error errn; // Check for the platform-tools directory. - DirAccessRef 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!"); @@ -2138,7 +2145,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } // Check for the build-tools directory. - DirAccessRef 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!"); @@ -2156,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) { @@ -2197,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"; } @@ -2252,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; } @@ -2322,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"); @@ -2331,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; } @@ -2344,9 +2394,9 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre user = p_preset->get("keystore/debug_user"); if (keystore.is_empty()) { - keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); - password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); - user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); + keystore = EDITOR_GET("export/android/debug_keystore"); + password = EDITOR_GET("export/android/debug_keystore_pass"); + user = EDITOR_GET("export/android/debug_keystore_user"); } if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) { @@ -2364,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; } @@ -2385,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; } @@ -2405,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; } @@ -2417,12 +2475,12 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } void EditorExportPlatformAndroid::_clear_assets_directory() { - DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); // Clear the APK assets directory if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { print_verbose("Clearing APK assets directory.."); - DirAccessRef da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(APK_ASSETS_DIRECTORY); } @@ -2430,7 +2488,7 @@ void EditorExportPlatformAndroid::_clear_assets_directory() { // Clear the AAB assets directory if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { print_verbose("Clearing AAB assets directory.."); - DirAccessRef da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(AAB_ASSETS_DIRECTORY); } @@ -2450,7 +2508,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() { ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); Vector<String> libs = json.get_data(); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); for (int i = 0; i < libs.size(); i++) { print_verbose("Removing previously installed library " + libs[i]); da->remove(libs[i]); @@ -2470,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); } @@ -2483,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); @@ -2516,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) { @@ -2539,16 +2596,15 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP //test that installed build version is alright { print_verbose("Checking build version.."); - FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); - if (!f) { - 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.")); + Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (f.is_null()) { + 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); - f->close(); 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; } } @@ -2561,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); @@ -2578,20 +2634,18 @@ 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) { - FileAccessRef fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); - JSON json; - fa->store_string(json.stringify(user_data.libs, "\t")); - fa->close(); + Ref<FileAccess> fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); + fa->store_string(JSON::stringify(user_data.libs, "\t")); } } else { 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; } } @@ -2608,14 +2662,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"; @@ -2668,15 +2728,15 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String debug_user = p_preset->get("keystore/debug_user"); if (debug_keystore.is_empty()) { - debug_keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); - debug_password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); - debug_user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); + debug_keystore = EDITOR_GET("export/android/debug_keystore"); + debug_password = EDITOR_GET("export/android/debug_keystore_pass"); + debug_user = EDITOR_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; } @@ -2689,10 +2749,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; } @@ -2704,7 +2764,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; } @@ -2724,7 +2784,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(); @@ -2734,7 +2794,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; } @@ -2756,7 +2816,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; } } @@ -2765,8 +2825,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2774,23 +2834,23 @@ 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; } int ret = unzGoToFirstFile(pkg); - zlib_filefunc_def io2 = io; - FileAccess *dst_f = nullptr; - io2.opaque = &dst_f; + 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); @@ -2807,6 +2867,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } bool skip = false; @@ -2838,20 +2901,22 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP _load_image_data(splash_bg_color_image, data); } - for (int i = 0; i < icon_densities_count; ++i) { - if (main_image.is_valid() && !main_image->is_empty()) { - if (file == launcher_icons[i].export_path) { - _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); + if (file.ends_with(".png") && file.contains("mipmap")) { + for (int i = 0; i < icon_densities_count; ++i) { + if (main_image.is_valid() && !main_image->is_empty()) { + if (file == launcher_icons[i].export_path) { + _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); + } } - } - if (foreground.is_valid() && !foreground->is_empty()) { - if (file == launcher_adaptive_icon_foregrounds[i].export_path) { - _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); + if (foreground.is_valid() && !foreground->is_empty()) { + if (file == launcher_adaptive_icon_foregrounds[i].export_path) { + _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); + } } - } - if (background.is_valid() && !background->is_empty()) { - if (file == launcher_adaptive_icon_backgrounds[i].export_path) { - _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); + if (background.is_valid() && !background->is_empty()) { + if (file == launcher_adaptive_icon_backgrounds[i].export_path) { + _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); + } } } } @@ -2902,7 +2967,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); } @@ -2920,7 +2985,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 { @@ -2933,7 +2998,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); } @@ -2969,15 +3034,13 @@ 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); } ret = unzGoToFirstFile(tmp_unaligned); - io2 = io; - dst_f = nullptr; - io2.opaque = &dst_f; + io2 = zipio_create_io(&io2_fa); zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); // Take files from the unaligned APK and write them out to the aligned one @@ -2991,6 +3054,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP char fname[16384]; char extra[16384]; ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); @@ -3049,29 +3115,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, Set<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 0f267cf13a..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, Set<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 ab915a5f85..8d016d3fac 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -75,8 +75,8 @@ String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_or // Utility method used to create a directory. Error create_directory(const String &p_dir) { if (!DirAccess::exists(p_dir)) { - DirAccessRef filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); Error err = filesystem_da->make_dir_recursive(p_dir); ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); } @@ -91,10 +91,9 @@ Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) { if (err != OK) { return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_buffer(p_data.ptr(), p_data.size()); - memdelete(fa); return OK; } @@ -109,10 +108,9 @@ Error store_string_at_path(const String &p_path, const String &p_data) { } return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_string(p_data); - memdelete(fa); return OK; } @@ -122,7 +120,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/"); print_verbose("Saving project files from " + p_path + " into " + dst_path); Error err = store_file_at_path(dst_path, p_data); @@ -152,15 +150,15 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales - DirAccessRef da = DirAccess::open("res://android/build/res"); - if (!da) { + Ref<DirAccess> da = DirAccess::open("res://android/build/res"); + if (da.is_null()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error("Unable to open Android resources directory."); } return ERR_CANT_OPEN; } da->list_dir_begin(); - Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); + Dictionary appnames = GLOBAL_GET("application/config/name_localized"); while (true) { String file = da->get_next(); if (file.is_empty()) { @@ -191,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) { @@ -234,30 +230,19 @@ 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; String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " - "tools:replace=\"android:screenOrientation,android:excludeFromRecents\" " + "tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" " "android:excludeFromRecents=\"%s\" " - "android:screenOrientation=\"%s\">\n", + "android:screenOrientation=\"%s\" " + "android:resizeableActivity=\"%s\">\n", bool_to_string(p_preset->get("package/exclude_from_recents")), - orientation); + orientation, + bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); if (uses_xr) { manifest_activity_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />\n"; } else { @@ -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( @@ -279,11 +264,12 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ " android:requestLegacyExternalStorage=\"%s\"\n" " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n" " tools:ignore=\"GoogleAppIndexingWarning\">\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_version_name\" />\n" " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_metadata_name\" />\n", 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; @@ -293,6 +279,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ manifest_application_text += vformat( " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", hand_tracking_frequency); + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; } } else { manifest_application_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n"; 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 c84a919b6b..d6cd62e9f5 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,12 +34,20 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -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://")) { @@ -47,33 +55,33 @@ 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; return OK; } -void FileAccessAndroid::close() { - if (!a) { +void FileAccessAndroid::_close() { + 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; @@ -84,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; } @@ -108,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; } @@ -116,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; @@ -132,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() { @@ -162,5 +170,5 @@ bool FileAccessAndroid::file_exists(const String &p_path) { } FileAccessAndroid::~FileAccessAndroid() { - close(); + _close(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index fb612cd008..55f8fbe0f4 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -35,44 +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 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 void close(); ///< close 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 4c4501729d..2d4c4763a2 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -40,6 +40,13 @@ android:name="xr_hand_tracking_metadata_name" android:value="xr_hand_tracking_metadata_value"/> + <!-- XR hand tracking version --> + <!-- This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> + <!-- Removed at export time if the xr mode is not VR or hand tracking is disabled. --> + <meta-data + android:name="xr_hand_tracking_version_name" + android:value="xr_hand_tracking_version_value"/> + <!-- Supported Meta devices --> <!-- This is removed by the exporter if the xr mode is not VR. --> <meta-data @@ -52,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 5d1a9d7b99..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 { @@ -33,6 +36,11 @@ allprojects { } } +configurations { + // Initializes a placeholder for the devImplementation dependency configuration. + devImplementation {} +} + dependencies { implementation libraries.kotlinStdLib implementation libraries.androidxFragment @@ -45,6 +53,7 @@ dependencies { // Custom build mode. In this scenario this project is the only one around and the Godot // library is available through the pre-generated godot-lib.*.aar android archive files. debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) + devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar']) releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) } @@ -66,12 +75,17 @@ dependencies { android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion compileOptions { sourceCompatibility versions.javaVersion targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + assetPacks = [":assetPacks:installTime"] defaultConfig { @@ -93,6 +107,8 @@ android { versionName getExportVersionName() minSdkVersion getExportMinSdkVersion() targetSdkVersion getExportTargetSdkVersion() + + missingDimensionStrategy 'products', 'template' } lintOptions { @@ -146,6 +162,18 @@ android { } } + dev { + initWith debug + // Signing and zip-aligning are skipped for prebuilt builds, but + // performed for custom builds. + zipAlignEnabled shouldZipAlign() + if (shouldSign()) { + signingConfig signingConfigs.debug + } else { + signingConfig null + } + } + release { // Signing and zip-aligning are skipped for prebuilt builds, but // performed for custom builds. @@ -167,6 +195,7 @@ android { assets.srcDirs = ['assets'] } debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] } @@ -183,6 +212,12 @@ task copyAndRenameDebugApk(type: Copy) { rename "android_debug.apk", getExportFilename() } +task copyAndRenameDevApk(type: Copy) { + from "$buildDir/outputs/apk/dev/android_dev.apk" + into getExportPath() + rename "android_dev.apk", getExportFilename() +} + task copyAndRenameReleaseApk(type: Copy) { from "$buildDir/outputs/apk/release/android_release.apk" into getExportPath() @@ -195,6 +230,12 @@ task copyAndRenameDebugAab(type: Copy) { rename "build-debug.aab", getExportFilename() } +task copyAndRenameDevAab(type: Copy) { + from "$buildDir/outputs/bundle/dev/build-dev.aab" + into getExportPath() + rename "build-dev.aab", getExportFilename() +} + task copyAndRenameReleaseAab(type: Copy) { from "$buildDir/outputs/bundle/release/build-release.aab" into getExportPath() diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index c238d1b361..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", ] @@ -76,7 +77,7 @@ ext.getGodotEditorVersion = { -> String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : "" if (editorVersion == null || editorVersion.isEmpty()) { // Try the library version first - editorVersion = getGodotLibraryVersion() + editorVersion = getGodotLibraryVersionName() if (editorVersion.isEmpty()) { // Fallback value. @@ -86,9 +87,24 @@ ext.getGodotEditorVersion = { -> return editorVersion } +ext.getGodotLibraryVersionCode = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionCode +} + +ext.getGodotLibraryVersionName = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionName +} + ext.generateGodotLibraryVersion = { List<String> requiredKeys -> // Attempt to read the version from the `version.py` file. - String libraryVersion = "" + String libraryVersionName = "" + int libraryVersionCode = 0 File versionFile = new File("../../../version.py") if (versionFile.isFile()) { @@ -109,15 +125,55 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys -> } if (requiredKeys.empty) { - libraryVersion = map.values().join(".") + 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"]) * 10 + } + + if (map.containsKey("minor")) { + libraryVersionCode += (Integer.parseInt(map["minor"]) * 1000) + } + + if (map.containsKey("major")) { + libraryVersionCode += (Integer.parseInt(map["major"]) * 100000) + } + } catch (NumberFormatException ignore) { + libraryVersionCode = 1 + } } } - if (libraryVersion.isEmpty()) { + if (libraryVersionName.isEmpty()) { // Fallback value in case we're unable to read the file. - libraryVersion = "custom_build" + libraryVersionName = "custom_build" } - return libraryVersion + + if (libraryVersionCode == 0) { + libraryVersionCode = 1 + } + + return [libraryVersionName, libraryVersionCode] } ext.getGodotLibraryVersion = { -> @@ -127,7 +183,10 @@ ext.getGodotLibraryVersion = { -> ext.getGodotPublishVersion = { -> List<String> requiredKeys = ["major", "minor", "patch", "status"] - return generateGodotLibraryVersion(requiredKeys) + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys) + return versionName } final String VALUE_SEPARATOR_REGEX = "\\|" 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 83bc68c992..5a91e5ce32 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,22 +28,28 @@ allprojects { } ext { - supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] - supportedTargets = ["release", "debug"] + supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] + supportedFlavors = ["editor", "template"] + supportedFlavorsBuildTypes = [ + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + "editor": ["dev", "debug"], + "template": ["dev", "debug", "release"] + ] - // Used by gradle to specify which architecture to build for by default when running `./gradlew build`. - // This command is usually used by Android Studio. + // Used by gradle to specify which architecture to build for by default when running + // `./gradlew build` (this command is usually used by Android Studio). // If building manually on the command line, it's recommended to use the - // `./gradlew generateGodotTemplates` build command instead after running the `scons` command. - // The defaultAbi must be one of the {supportedAbis} values. - defaultAbi = "arm64v8" + // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s). + // The {selectedAbis} values must be from the {supportedAbis} values. + selectedAbis = ["arm64"] } def rootDir = "../../.." def binDir = "$rootDir/bin/" -def getSconsTaskName(String buildType) { - return "compileGodotNativeLibs" + buildType.capitalize() +def getSconsTaskName(String flavor, String buildType, String abi) { + return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() } /** @@ -55,6 +64,17 @@ task copyDebugBinaryToBin(type: Copy) { } /** + * Copy the generated 'android_dev.apk' binary template into the Godot bin directory. + * Depends on the app build task to ensure the binary is generated prior to copying. + */ +task copyDevBinaryToBin(type: Copy) { + dependsOn ':app:assembleDev' + from('app/build/outputs/apk/dev') + into(binDir) + include('android_dev.apk') +} + +/** * Copy the generated 'android_release.apk' binary template into the Godot bin directory. * Depends on the app build task to ensure the binary is generated prior to copying. */ @@ -70,10 +90,10 @@ task copyReleaseBinaryToBin(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToAppModule(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into('app/libs/debug') - include('godot-lib.debug.aar') + include('godot-lib.template_debug.aar') } /** @@ -81,10 +101,32 @@ task copyDebugAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToBin(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into(binDir) - include('godot-lib.debug.aar') + include('godot-lib.template_debug.aar') +} + +/** + * Copy the Godot android library archive dev file into the app module dev libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToAppModule(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into('app/libs/dev') + include('godot-lib.template_debug.dev.aar') +} + +/** + * Copy the Godot android library archive dev file into the root bin directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToBin(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into(binDir) + include('godot-lib.template_debug.dev.aar') } /** @@ -92,10 +134,10 @@ task copyDebugAARToBin(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToAppModule(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into('app/libs/release') - include('godot-lib.release.aar') + include('godot-lib.template_release.aar') } /** @@ -103,15 +145,15 @@ task copyReleaseAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToBin(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into(binDir) - include('godot-lib.release.aar') + include('godot-lib.template_release.aar') } /** * Generate Godot custom build template by zipping the source files from the app directory, as well - * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'. + * as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'. * The zip file also includes some gradle tools to allow building of the custom build. */ task zipCustomBuild(type: Zip) { @@ -130,8 +172,13 @@ def templateExcludedBuildTask() { def excludedTasks = [] if (!isAndroidStudio()) { logger.lifecycle("Excluding Android studio build tasks") - for (String buildType : supportedTargets) { - excludedTasks += ":lib:" + getSconsTaskName(buildType) + for (String flavor : supportedFlavors) { + String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor] + for (String buildType : supportedBuildTypes) { + for (String abi : selectedAbis) { + excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi) + } + } } } return excludedTasks @@ -141,7 +188,7 @@ def templateBuildTasks() { def tasks = [] // Only build the apks and aar files for which we have native shared libraries. - for (String target : supportedTargets) { + for (String target : supportedFlavorsBuildTypes["template"]) { File targetLibs = new File("lib/libs/" + target) if (targetLibs != null && targetLibs.isDirectory() @@ -167,6 +214,45 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } +task copyEditorDebugBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDebug' + from('editor/build/outputs/apk/debug') + into(binDir) + include('android_editor.apk') +} + +task copyEditorDevBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDev' + from('editor/build/outputs/apk/dev') + into(binDir) + include('android_editor_dev.apk') +} + +/** + * Generate the Godot Editor Android apk. + * + * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running + * this gradle task. The task will only build the apk(s) for which the shared libraries is + * available. + */ +task generateGodotEditor { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + + def tasks = [] + + for (String target : supportedFlavorsBuildTypes["editor"]) { + File targetLibs = new File("lib/libs/tools/" + target) + if (targetLibs != null + && targetLibs.isDirectory() + && targetLibs.listFiles() != null + && targetLibs.listFiles().length > 0) { + tasks += "copyEditor${target.capitalize()}BinaryToBin" + } + } + + dependsOn = tasks +} + /** * Master task used to coordinate the tasks defined above to generate the set of Godot templates. */ @@ -190,8 +276,31 @@ task generateDevTemplate { finalizedBy 'zipCustomBuild' } +task clean(type: Delete) { + dependsOn 'cleanGodotEditor' + dependsOn 'cleanGodotTemplates' +} + /** - * Clean the generated artifacts. + * Clean the generated editor artifacts. + */ +task cleanGodotEditor(type: Delete) { + // Delete the generated native tools libs + delete("lib/libs/tools") + + // Delete the library generated AAR files + delete("lib/build/outputs/aar") + + // Delete the generated binary apks + delete("editor/build/outputs/apk") + + // Delete the Godot editor apks in the Godot bin directory + delete("$binDir/android_editor.apk") + delete("$binDir/android_editor_dev.apk") +} + +/** + * Clean the generated template artifacts. */ task cleanGodotTemplates(type: Delete) { // Delete the generated native libs @@ -208,10 +317,15 @@ task cleanGodotTemplates(type: Delete) { // Delete the Godot templates in the Godot bin directory delete("$binDir/android_debug.apk") + delete("$binDir/android_dev.apk") delete("$binDir/android_release.apk") delete("$binDir/android_source.zip") + delete("$binDir/godot-lib.template_debug.aar") + delete("$binDir/godot-lib.template_debug.dev.aar") + delete("$binDir/godot-lib.template_release.aar") + + // Cover deletion for the libs using the previous naming scheme delete("$binDir/godot-lib.debug.aar") + delete("$binDir/godot-lib.dev.aar") delete("$binDir/godot-lib.release.aar") - - finalizedBy getTasksByName("clean", true) } diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle new file mode 100644 index 0000000000..9152492e9d --- /dev/null +++ b/platform/android/java/editor/build.gradle @@ -0,0 +1,101 @@ +// Gradle build config for Godot Engine's Android port. +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 { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion + + 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 generateVersionCode() + versionName generateVersionName() + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + + missingDimensionStrategy 'products', 'editor' + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = versions.javaVersion + } + + buildTypes { + dev { + initWith debug + applicationIdSuffix ".dev" + } + + debug { + initWith release + + // Need to swap with the release signing config when this is ready for public release. + signingConfig signingConfigs.debug + } + + release { + // This buildtype is disabled below. + // The editor can't be used with target=release only, as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + } + } + + packagingOptions { + // 'doNotStrip' is enabled for development within Android Studio + if (shouldNotStrip()) { + doNotStrip '**/*.so' + } + } + + // Disable 'release' buildtype. + // The editor can't be used with target=release only, as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + variantFilter { variant -> + if (variant.buildType.name == "release") { + setIgnore(true) + } + } + + applicationVariants.all { variant -> + variant.outputs.all { output -> + def suffix = variant.name == "dev" ? "_dev" : "" + output.outputFileName = "android_editor${suffix}.apk" + } + } +} diff --git a/platform/android/java/editor/src/dev/res/values/strings.xml b/platform/android/java/editor/src/dev/res/values/strings.xml new file mode 100644 index 0000000000..45fae3fd39 --- /dev/null +++ b/platform/android/java/editor/src/dev/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4.x (dev)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6aa5f06f31 --- /dev/null +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="org.godotengine.editor" + android:installLocation="auto"> + + <supports-screens + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="false" + android:xlargeScreens="true" /> + + <uses-feature + android:glEsVersion="0x00020000" + android:required="true" /> + + <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 + android:allowBackup="false" + android:icon="@mipmap/icon" + android:label="@string/godot_editor_name_string" + tools:ignore="GoogleAppIndexingWarning" + android:requestLegacyExternalStorage="true"> + + <activity + android:name=".GodotProjectManager" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:exported="true" + 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" /> + </intent-filter> + </activity> + + <activity + android:name=".GodotEditor" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + 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 + android:name=".GodotGame" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:label="@string/godot_project_name_string" + android:process=":GodotGame" + android:launchMode="singleTask" + android: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> + +</manifest> 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/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 44f0a3eb3e..b9536a7066 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotInstrumentation.java */ +/* GodotGame.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,23 +28,15 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.editor -import android.app.Instrumentation; -import android.content.Intent; -import android.os.Bundle; +/** + * Drives the 'run project' window of the Godot Editor. + */ +class GodotGame : GodotEditor() { + override fun overrideOrientationRequest() = false -public class GodotInstrumentation extends Instrumentation { - private Intent intent; + override fun enableLongPressGestures() = false - @Override - public void onCreate(Bundle arguments) { - intent = arguments.getParcelable("intent"); - start(); - } - - @Override - public void onStart() { - startActivitySync(intent); - } + override fun enablePanAndScaleGestures() = false } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt new file mode 100644 index 0000000000..bcf4659603 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* GodotProjectManager.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 + +/** + * 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 + * [GodotEditor] activity. + */ +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 new file mode 100644 index 0000000000..837a5d62e1 --- /dev/null +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -0,0 +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 120a40a31d..c9e2a5d7d2 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() @@ -18,14 +20,13 @@ def pathToRootDir = "../../../../" android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools - ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk - manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersion()] + manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersionName()] } namespace = "org.godotengine.godot" @@ -35,6 +36,22 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + + buildTypes { + dev { + initWith debug + } + } + + flavorDimensions "products" + productFlavors { + editor {} + template {} + } + lintOptions { abortOnError false disable 'MissingTranslation', 'UnusedResources' @@ -58,24 +75,59 @@ android { aidl.srcDirs = ['aidl'] assets.srcDirs = ['assets'] } + debug.jniLibs.srcDirs = ['libs/debug'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] + + // Editor jni library + editorDebug.jniLibs.srcDirs = ['libs/tools/debug'] + editorDev.jniLibs.srcDirs = ['libs/tools/dev'] + } + + // Disable 'editorRelease'. + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + variantFilter { variant -> + if (variant.name == "editorRelease") { + setIgnore(true) + } } libraryVariants.all { variant -> - variant.outputs.all { output -> - output.outputFileName = "godot-lib.${variant.name}.aar" + def flavorName = variant.getFlavorName() + if (flavorName == null || flavorName == "") { + throw new GradleException("Invalid product flavor: $flavorName") } - def buildType = variant.buildType.name.capitalize() + def buildType = variant.buildType.name + if (buildType == null || buildType == "" || !supportedFlavorsBuildTypes[flavorName].contains(buildType)) { + throw new GradleException("Invalid build type: $buildType") + } - def releaseTarget = buildType.toLowerCase() - if (releaseTarget == null || releaseTarget == "") { - throw new GradleException("Invalid build type: " + buildType) + boolean devBuild = buildType == "dev" + + def sconsTarget = flavorName + if (sconsTarget == "template") { + switch (buildType) { + case "release": + sconsTarget += "_release" + break + case "debug": + case "dev": + default: + sconsTarget += "_debug" + break; + } } - if (!supportedAbis.contains(defaultAbi)) { - throw new GradleException("Invalid default abi: " + defaultAbi) + // Update the name of the generated library + def outputSuffix = "${sconsTarget}" + if (devBuild) { + outputSuffix = "${outputSuffix}.dev" + } + variant.outputs.all { output -> + output.outputFileName = "godot-lib.${outputSuffix}.aar" } // Find scons' executable path @@ -88,13 +140,11 @@ android { for (ext in sconsExts) { String sconsNameExt = sconsName + ext logger.lifecycle("Checking $sconsNameExt") - sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! break } - // Check all the options in path List<File> allOptions = org.gradle.internal.os.OperatingSystem.current().findAllInPath(sconsNameExt) if (!allOptions.isEmpty()) { @@ -103,27 +153,32 @@ android { break } } - if (sconsExecutableFile == null) { throw new GradleException("Unable to find executable path for the '$sconsName' command.") } else { logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") } - // Creating gradle task to generate the native libraries for the default abi. - def taskName = getSconsTaskName(buildType) - tasks.create(name: taskName, type: Exec) { - executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() - } + for (String selectedAbi : selectedAbis) { + if (!supportedAbis.contains(selectedAbi)) { + throw new GradleException("Invalid selected abi: $selectedAbi") + } - // Schedule the tasks so the generated libs are present before the aar file is packaged. - tasks["merge${buildType}JniLibFolders"].dependsOn taskName + // Creating gradle task to generate the native libraries for the selected abi. + def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) + tasks.create(name: taskName, type: Exec) { + executable sconsExecutableFile.absolutePath + args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + } + + // Schedule the tasks so the generated libs are present before the aar file is packaged. + tasks["merge${flavorName.capitalize()}${buildType.capitalize()}JniLibFolders"].dependsOn taskName + } } // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187). // publishing { -// singleVariant("release") { +// singleVariant("templateRelease") { // withSourcesJar() // withJavadocJar() // } 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 e8ffbb9481..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; @@ -65,16 +66,13 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God } else { Log.v(TAG, "Creating new Godot fragment instance."); godotFragment = initGodotInstance(); - if (godotFragment == null) { - throw new IllegalStateException("Godot instance must be non-null."); - } - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } } @Override public void onDestroy() { + Log.v(TAG, "Destroying Godot app..."); super.onDestroy(); onGodotForceQuit(godotFragment); } @@ -82,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 78848c109a..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,8 +34,11 @@ import static android.content.Context.MODE_PRIVATE; import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import org.godotengine.godot.tts.GodotTTS; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -47,7 +50,6 @@ import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -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,8 +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 GodotIO io; + public GodotNetUtils netUtils; + public GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -247,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)); @@ -259,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(); @@ -300,6 +313,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } } } + return true; } public void setKeepScreenOn(final boolean p_enabled) { @@ -333,19 +347,35 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public void restart() { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotRestartRequested(this); + } + }); } public void alert(final String message, final String title) { + alert(message, title, null); + } + + private void alert(@StringRes int messageResId, @StringRes int titleResId, @Nullable Runnable okCallback) { + Resources res = getResources(); + alert(res.getString(messageResId), res.getString(titleResId), okCallback); + } + + private void alert(final String message, final String title, @Nullable Runnable okCallback) { final Activity activity = getActivity(); runOnUiThread(() -> { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setMessage(message).setTitle(title); builder.setPositiveButton( "OK", - (dialog, id) -> dialog.cancel()); + (dialog, id) -> { + if (okCallback != null) { + okCallback.run(); + } + dialog.cancel(); + }); AlertDialog dialog = builder.create(); dialog.show(); }); @@ -455,23 +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 @@ -508,17 +543,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC use_debug_opengl = true; } else if (command_line[i].equals("--use_immersive")) { use_immersive = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - UiChangeListener(); - } + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + UiChangeListener(); } else if (command_line[i].equals("--use_apk_expansion")) { use_apk_expansion = true; } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { @@ -665,14 +697,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public String getClipboard() { - String copiedText = ""; - - if (mClipboard.hasPrimaryClip()) { - ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0); - copiedText = item.getText().toString(); - } - - return copiedText; + ClipData clipData = mClipboard.getPrimaryClip(); + if (clipData == null) + return ""; + CharSequence text = clipData.getItemAt(0).getText(); + if (text == null) + return ""; + return text.toString(); } public void setClipboard(String p_text) { @@ -698,7 +729,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + if (use_immersive) { Window window = getActivity().getWindow(); window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | @@ -718,15 +749,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final View decorView = getActivity().getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } }); } @@ -859,9 +888,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void forceQuit() { // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each // native Godot components that is started in Godot#onVideoInit. - if (godotHost != null) { - godotHost.onGodotForceQuit(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotForceQuit(this); + } + }); } private boolean obbIsCorrupted(String f, String main_pack_md5) { @@ -885,9 +916,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) { - String s = Integer.toHexString(0xFF & messageDigest[i]); - + for (byte b : messageDigest) { + String s = Integer.toHexString(0xFF & b); if (s.length() == 1) { s = "0" + s; } @@ -1010,12 +1040,22 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); } + public void initInputDevices() { mRenderView.initInputDevices(); } @Keep - private GodotRenderView getRenderView() { // used by native side to get renderView + public GodotRenderView getRenderView() { // used by native side to get renderView return mRenderView; } + + @Keep + private void createNewGodotInstance(String[] args) { + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onNewGodotInstanceRequested(args); + } + }); + } } 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 61093d54de..3dfc37f6b0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,7 +29,8 @@ /*************************************************************************/ package org.godotengine.godot; -import org.godotengine.godot.input.GodotGestureHandler; +import org.godotengine.godot.gl.GLSurfaceView; +import org.godotengine.godot.gl.GodotRenderer; import org.godotengine.godot.input.GodotInputHandler; import org.godotengine.godot.utils.GLUtils; import org.godotengine.godot.xr.XRMode; @@ -43,9 +44,7 @@ import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; -import android.opengl.GLSurfaceView; import android.os.Build; -import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; @@ -74,9 +73,7 @@ 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; public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) { super(context); @@ -84,10 +81,9 @@ 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); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } init(xrMode, false); } @@ -131,7 +127,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); } @@ -155,19 +150,40 @@ 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 */ @Keep public void setPointerIcon(int pointerType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); } } @Override public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { - return pointerIcon; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPointerIcon(); + } + return super.onResolvePointerIcon(me, pointerIndex); } private void init(XRMode xrMode, boolean translucent) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 8e8f993369..2e7b67194f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -60,8 +60,16 @@ public interface GodotHost { default void onGodotForceQuit(Godot instance) {} /** - * Invoked on the GL thread when the Godot instance wants to be restarted. It's up to the host + * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host * to perform the appropriate action(s). */ default void onGodotRestartRequested(Godot instance) {} + + /** + * Invoked on the UI thread when a new Godot instance is requested. It's up to the host to + * perform the appropriate action(s). + * + * @param args Arguments used to initialize the new instance. + */ + default void onNewGodotInstanceRequested(String[] args) {} } 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 b151e7eec1..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,8 +36,8 @@ 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; import android.os.Build; import android.os.Environment; @@ -45,12 +45,11 @@ 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; // Wrapper for native library @@ -58,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; @@ -71,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) { @@ -222,8 +128,30 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); - return (int)(metrics.density * 160f); + return activity.getResources().getDisplayMetrics().densityDpi; + } + + /** + * Returns bucketized density values. + */ + public float getScaledDensity() { + 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) { @@ -234,7 +162,7 @@ public class GodotIO { return fallback; } - public int[] screenGetUsableRect() { + public int[] getDisplaySafeArea() { DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); Display display = activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -256,9 +184,29 @@ 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 int[] getDisplayCutouts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + return new int[0]; + DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + if (cutout == null) + return new int[0]; + List<Rect> rects = cutout.getBoundingRects(); + int cutouts = rects.size(); + int[] result = new int[cutouts * 4]; + int index = 0; + for (Rect rect : rects) { + result[index++] = rect.left; + result[index++] = rect.top; + result[index++] = rect.width(); + result[index++] = rect.height(); + } + return result; + } + + 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 29e4b4b29e..33896ecb95 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -30,7 +30,14 @@ 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; @@ -40,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"); } @@ -49,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. @@ -61,14 +74,14 @@ 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. * @param p_surface * @param p_width * @param p_height - * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) */ public static native void resize(Surface p_surface, int p_width, int p_height); @@ -79,79 +92,76 @@ 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(); /** * Invoked on the GL thread to draw the current frame. - * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onDrawFrame(GL10) */ - public static native void step(); + public static native boolean step(); /** - * Forward touch events from the main thread to the GL thread. + * TTS callback. */ - 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 ttsCallback(int event, int id, int pos); /** - * Forward hover events from the main thread to the GL thread. + * Forward touch events. */ - public static native void hover(int type, float x, float y); + public static native void dispatchTouchEvent(int event, int pointer, int pointerCount, float[] positions, boolean doubleTap); /** - * Forward double_tap events from the main thread to the GL thread. + * Dispatch mouse events */ - public static native void doubleTap(int buttonMask, int x, int y); + public static native void dispatchMouseEvent(int event, int buttonMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative); - /** - * Forward scroll events from the main thread to the GL thread. - */ - public static native void scroll(int x, int y); + public static native void magnify(float x, float y, float factor); + + 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..0becf00d93 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,19 +47,16 @@ 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; public GodotVulkanRenderView(Context context, Godot godot) { super(context); 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); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); startRenderer(mRenderer); @@ -106,7 +101,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,19 +124,40 @@ 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 */ @Keep public void setPointerIcon(int pointerType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); } } @Override public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { - return pointerIcon; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPointerIcon(); + } + return super.onResolvePointerIcon(me, pointerIndex); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java new file mode 100644 index 0000000000..af16cfce74 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java @@ -0,0 +1,566 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.gl; + +import android.opengl.GLDebugHelper; +import android.opengl.GLException; + +import java.io.IOException; +import java.io.Writer; + +import javax.microedition.khronos.egl.EGL; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +class EGLLogWrapper implements EGL11 { + private EGL10 mEgl10; + Writer mLog; + boolean mLogArgumentNames; + boolean mCheckError; + private int mArgCount; + + + public EGLLogWrapper(EGL egl, int configFlags, Writer log) { + mEgl10 = (EGL10) egl; + mLog = log; + mLogArgumentNames = + (GLDebugHelper.CONFIG_LOG_ARGUMENT_NAMES & configFlags) != 0; + mCheckError = + (GLDebugHelper.CONFIG_CHECK_GL_ERROR & configFlags) != 0; + } + + public boolean eglChooseConfig(EGLDisplay display, int[] attrib_list, + EGLConfig[] configs, int config_size, int[] num_config) { + begin("eglChooseConfig"); + arg("display", display); + arg("attrib_list", attrib_list); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglChooseConfig(display, attrib_list, configs, + config_size, num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public boolean eglCopyBuffers(EGLDisplay display, EGLSurface surface, + Object native_pixmap) { + begin("eglCopyBuffers"); + arg("display", display); + arg("surface", surface); + arg("native_pixmap", native_pixmap); + end(); + + boolean result = mEgl10.eglCopyBuffers(display, surface, native_pixmap); + returns(result); + checkError(); + return result; + } + + public EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, + EGLContext share_context, int[] attrib_list) { + begin("eglCreateContext"); + arg("display", display); + arg("config", config); + arg("share_context", share_context); + arg("attrib_list", attrib_list); + end(); + + EGLContext result = mEgl10.eglCreateContext(display, config, + share_context, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePbufferSurface(EGLDisplay display, + EGLConfig config, int[] attrib_list) { + begin("eglCreatePbufferSurface"); + arg("display", display); + arg("config", config); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePbufferSurface(display, config, + attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePixmapSurface(EGLDisplay display, + EGLConfig config, Object native_pixmap, int[] attrib_list) { + begin("eglCreatePixmapSurface"); + arg("display", display); + arg("config", config); + arg("native_pixmap", native_pixmap); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePixmapSurface(display, config, + native_pixmap, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreateWindowSurface(EGLDisplay display, + EGLConfig config, Object native_window, int[] attrib_list) { + begin("eglCreateWindowSurface"); + arg("display", display); + arg("config", config); + arg("native_window", native_window); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreateWindowSurface(display, config, + native_window, attrib_list); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroyContext(EGLDisplay display, EGLContext context) { + begin("eglDestroyContext"); + arg("display", display); + arg("context", context); + end(); + + boolean result = mEgl10.eglDestroyContext(display, context); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroySurface(EGLDisplay display, EGLSurface surface) { + begin("eglDestroySurface"); + arg("display", display); + arg("surface", surface); + end(); + + boolean result = mEgl10.eglDestroySurface(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, + int attribute, int[] value) { + begin("eglGetConfigAttrib"); + arg("display", display); + arg("config", config); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglGetConfigAttrib(display, config, attribute, + value); + arg("value", value); + returns(result); + checkError(); + return false; + } + + public boolean eglGetConfigs(EGLDisplay display, EGLConfig[] configs, + int config_size, int[] num_config) { + begin("eglGetConfigs"); + arg("display", display); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglGetConfigs(display, configs, config_size, + num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public EGLContext eglGetCurrentContext() { + begin("eglGetCurrentContext"); + end(); + + EGLContext result = mEgl10.eglGetCurrentContext(); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetCurrentDisplay() { + begin("eglGetCurrentDisplay"); + end(); + + EGLDisplay result = mEgl10.eglGetCurrentDisplay(); + returns(result); + + checkError(); + return result; + } + + public EGLSurface eglGetCurrentSurface(int readdraw) { + begin("eglGetCurrentSurface"); + arg("readdraw", readdraw); + end(); + + EGLSurface result = mEgl10.eglGetCurrentSurface(readdraw); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetDisplay(Object native_display) { + begin("eglGetDisplay"); + arg("native_display", native_display); + end(); + + EGLDisplay result = mEgl10.eglGetDisplay(native_display); + returns(result); + + checkError(); + return result; + } + + public int eglGetError() { + begin("eglGetError"); + end(); + + int result = mEgl10.eglGetError(); + returns(getErrorString(result)); + + return result; + } + + public boolean eglInitialize(EGLDisplay display, int[] major_minor) { + begin("eglInitialize"); + arg("display", display); + end(); + boolean result = mEgl10.eglInitialize(display, major_minor); + returns(result); + arg("major_minor", major_minor); + checkError(); + return result; + } + + public boolean eglMakeCurrent(EGLDisplay display, EGLSurface draw, + EGLSurface read, EGLContext context) { + begin("eglMakeCurrent"); + arg("display", display); + arg("draw", draw); + arg("read", read); + arg("context", context); + end(); + boolean result = mEgl10.eglMakeCurrent(display, draw, read, context); + returns(result); + checkError(); + return result; + } + + public boolean eglQueryContext(EGLDisplay display, EGLContext context, + int attribute, int[] value) { + begin("eglQueryContext"); + arg("display", display); + arg("context", context); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQueryContext(display, context, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public String eglQueryString(EGLDisplay display, int name) { + begin("eglQueryString"); + arg("display", display); + arg("name", name); + end(); + String result = mEgl10.eglQueryString(display, name); + returns(result); + checkError(); + return result; + } + + public boolean eglQuerySurface(EGLDisplay display, EGLSurface surface, + int attribute, int[] value) { + begin("eglQuerySurface"); + arg("display", display); + arg("surface", surface); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQuerySurface(display, surface, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public boolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) { + begin("eglSwapBuffers"); + arg("display", display); + arg("surface", surface); + end(); + boolean result = mEgl10.eglSwapBuffers(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglTerminate(EGLDisplay display) { + begin("eglTerminate"); + arg("display", display); + end(); + boolean result = mEgl10.eglTerminate(display); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitGL() { + begin("eglWaitGL"); + end(); + boolean result = mEgl10.eglWaitGL(); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitNative(int engine, Object bindTarget) { + begin("eglWaitNative"); + arg("engine", engine); + arg("bindTarget", bindTarget); + end(); + boolean result = mEgl10.eglWaitNative(engine, bindTarget); + returns(result); + checkError(); + return result; + } + + private void checkError() { + int eglError; + if ((eglError = mEgl10.eglGetError()) != EGL_SUCCESS) { + String errorMessage = "eglError: " + getErrorString(eglError); + logLine(errorMessage); + if (mCheckError) { + throw new GLException(eglError, errorMessage); + } + } + } + + private void logLine(String message) { + log(message + '\n'); + } + + private void log(String message) { + try { + mLog.write(message); + } catch (IOException e) { + // Ignore exception, keep on trying + } + } + + private void begin(String name) { + log(name + '('); + mArgCount = 0; + } + + private void arg(String name, String value) { + if (mArgCount++ > 0) { + log(", "); + } + if (mLogArgumentNames) { + log(name + "="); + } + log(value); + } + + private void end() { + log(");\n"); + flush(); + } + + private void flush() { + try { + mLog.flush(); + } catch (IOException e) { + mLog = null; + } + } + + private void arg(String name, int value) { + arg(name, Integer.toString(value)); + } + + private void arg(String name, Object object) { + arg(name, toString(object)); + } + + private void arg(String name, EGLDisplay object) { + if (object == EGL10.EGL_DEFAULT_DISPLAY) { + arg(name, "EGL10.EGL_DEFAULT_DISPLAY"); + } else if (object == EGL_NO_DISPLAY) { + arg(name, "EGL10.EGL_NO_DISPLAY"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLContext object) { + if (object == EGL10.EGL_NO_CONTEXT) { + arg(name, "EGL10.EGL_NO_CONTEXT"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLSurface object) { + if (object == EGL10.EGL_NO_SURFACE) { + arg(name, "EGL10.EGL_NO_SURFACE"); + } else { + arg(name, toString(object)); + } + } + + private void returns(String result) { + log(" returns " + result + ";\n"); + flush(); + } + + private void returns(int result) { + returns(Integer.toString(result)); + } + + private void returns(boolean result) { + returns(Boolean.toString(result)); + } + + private void returns(Object result) { + returns(toString(result)); + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } else { + return obj.toString(); + } + } + + private void arg(String name, int[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private void arg(String name, Object[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private String toString(int n, int[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private String toString(int n, Object[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private static String getHex(int value) { + return "0x" + Integer.toHexString(value); + } + + public static String getErrorString(int error) { + switch (error) { + case EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return getHex(error); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java new file mode 100644 index 0000000000..8449c08b88 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -0,0 +1,1939 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.gl; + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + * <p> + * A GLSurfaceView provides the following features: + * <p> + * <ul> + * <li>Manages a surface, which is a special piece of memory that can be + * composited into the Android view system. + * <li>Manages an EGL display, which enables OpenGL to render into a surface. + * <li>Accepts a user-provided Renderer object that does the actual rendering. + * <li>Renders on a dedicated thread to decouple rendering performance from the + * UI thread. + * <li>Supports both on-demand and continuous rendering. + * <li>Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls. + * </ul> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Using GLSurfaceView</h3> + * <p> + * Typically you use GLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLSurfaceView can be used as-is. For the most part + * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLSurfaceView + * using the {@link #setRenderer(Renderer)} call. + * <p> + * <h3>Initializing GLSurfaceView</h3> + * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * <ul> + * <li>{@link #setDebugFlags(int)} + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * <li>{@link #setGLWrapper(GLWrapper)} + * </ul> + * <p> + * <h4>Specifying the android.view.Surface</h4> + * By default GLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + * <p> + * <h4>Choosing an EGL Configuration</h4> + * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how many channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. + * <p> + * By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + * <p> + * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + * <p> + * <h4>Debug Behavior</h4> + * You can optionally modify the behavior of GLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + * <p> + * <h4>Setting a Renderer</h4> + * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + * <p> + * <h3>Rendering Mode</h3> + * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + * <p> + * <h3>Activity Life-cycle</h3> + * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients + * are required to call {@link #onPause()} when the activity stops and + * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate + * the OpenGL display. + * <p> + * <h3>Handling events</h3> + * <p> + * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + * <pre class="prettyprint"> + * class MyGLSurfaceView extends GLSurfaceView { + * + * private MyRenderer mMyRenderer; + * + * public void start() { + * mMyRenderer = ...; + * setRenderer(mMyRenderer); + * } + * + * public boolean onKeyDown(int keyCode, KeyEvent event) { + * if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + * queueEvent(new Runnable() { + * // This method will be called on the rendering + * // thread: + * public void run() { + * mMyRenderer.handleDpadCenter(); + * }}); + * return true; + * } + * return super.onKeyDown(keyCode, event); + * } + * } + * </pre> + * + */ +public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private final static String TAG = "GLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + * <p> + * Wrapping is typically used for debugging purposes. + * <p> + * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + * <p> + * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + * <p> + * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + * <p> + * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + * <p> + * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + * <p>This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + * <p>The following GLSurfaceView methods can only be called <em>before</em> + * setRenderer is called: + * <ul> + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * </ul> + * <p> + * The following GLSurfaceView methods can only be called <em>after</em> + * setRenderer is called: + * <ul> + * <li>{@link #getRenderMode()} + * <li>{@link #onPause()} + * <li>{@link #onResume()} + * <li>{@link #queueEvent(Runnable)} + * <li>{@link #requestRender()} + * <li>{@link #setRenderMode(int)} + * </ul> + * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + * <p>Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + * <pre class="prettyprint"> + * public MyView(Context context) { + * super(context); + * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context. + * setRenderer(new MyRenderer()); + * } + * </pre> + * <p>Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + * <p>If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p>This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + * <p> + * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + * <p> + * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceCreated(SurfaceHolder holder) { + mGLThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + mGLThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (mGLThread != null) { + mGLThread.requestRenderAndNotify(finishDrawing); + } + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Deprecated + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Since we are part of the framework we know only surfaceRedrawNeededAsync + // will be called. + } + + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + * + * This method should be called when it is no longer desirable for the + * GLSurfaceView to continue rendering, such as in response to + * {@link android.app.Activity#onStop Activity.onStop}. + * + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + * + * This method should typically be called in + * {@link android.app.Activity#onStart Activity.onStart}. + * + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + * <p>Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + * <pre class="prettyprint"> + * class MyGLWrapper implements GLWrapper { + * GL wrap(GL gl) { + * return new MyGLImplementation(gl); + * } + * static class MyGLImplementation implements GL,GL10,GL11,... { + * ... + * } + * } + * </pre> + * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + * <p> + * The renderer is responsible for making OpenGL calls to render a frame. + * <p> + * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link GLSurfaceView#setRenderer} to + * register the renderer with the GLSurfaceView. + * <p> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Threading</h3> + * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. + * <p> + * <h3>EGL Context Lost</h3> + * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + * <p> + * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + * <p> + * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + * <p> + * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + * <p> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + * <p> + * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + * <p> + * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + * <pre class="prettyprint"> + * void onSurfaceChanged(GL10 gl, int width, int height) { + * gl.glViewport(0, 0, width, height); + * // for a fixed camera, set the projection too + * float ratio = (float) width / height; + * gl.glMatrixMode(GL10.GL_PROJECTION); + * gl.glLoadIdentity(); + * gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + // -- GODOT start -- + /** + * Called to draw the current frame. + * <p> + * This method is responsible for drawing the current frame. + * <p> + * The implementation of this method typically looks like this: + * <pre class="prettyprint"> + * boolean onDrawFrame(GL10 gl) { + * gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + * //... other gl calls to render the scene ... + * return true; + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * + * @return true if the buffers should be swapped, false otherwise. + */ + boolean onDrawFrame(GL10 gl); + // -- GODOT end -- + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + EGLLogWrapper.getErrorString(error); + } + + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + // -- GODOT start -- + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } finally { + } + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceChanged(gl, w, h); + } finally { + } + } + sizeChanged = false; + } + + boolean swapBuffers = false; + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + swapBuffers = view.mRenderer.onDrawFrame(gl); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } finally {} + } + } + if (swapBuffers) { + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + // -- GODOT end -- + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference<GLSurfaceView> mThisWeakRef = + new WeakReference<GLSurfaceView>(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} + diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java index e3956ac459..5c4fd00f6d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java @@ -28,38 +28,38 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.godot.gl; +import org.godotengine.godot.GodotLib; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; -import org.godotengine.godot.utils.GLUtils; - -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Godot's renderer implementation. + * Godot's GL renderer implementation. */ -class GodotRenderer implements GLSurfaceView.Renderer { +public class GodotRenderer implements GLSurfaceView.Renderer { private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; - GodotRenderer() { + public GodotRenderer() { this.pluginRegistry = GodotPluginRegistry.getPluginRegistry(); } - public void onDrawFrame(GL10 gl) { + public boolean onDrawFrame(GL10 gl) { if (activityJustResumed) { GodotLib.onRendererResumed(); activityJustResumed = false; } - GodotLib.step(); + boolean swapBuffers = GodotLib.step(); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onGLDrawFrame(gl); } + + return swapBuffers; } public void onSurfaceChanged(GL10 gl, int width, int height) { @@ -76,13 +76,13 @@ class GodotRenderer implements GLSurfaceView.Renderer { } } - void onActivityResumed() { + public void onActivityResumed() { // We defer invoking GodotLib.onRendererResumed() until the first draw frame call. // This ensures we have a valid GL context and surface when we do so. activityJustResumed = true; } - void onActivityPaused() { + public void onActivityPaused() { GodotLib.onRendererPaused(); } } 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..a7a57621de --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -0,0 +1,276 @@ +/*************************************************************************/ +/* 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 nextDownIsDoubleTap = false + private var dragInProgress = false + private var scaleInProgress = false + private var contextClickInProgress = false + private var pointerCaptureInProgress = false + + override fun onDown(event: MotionEvent): Boolean { + GodotInputHandler.handleMotionEvent(event.source, MotionEvent.ACTION_DOWN, event.buttonState, event.x, event.y, nextDownIsDoubleTap) + nextDownIsDoubleTap = false + 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) { + nextDownIsDoubleTap = false + GodotInputHandler.handleMotionEvent(event) + } + return true + } + + override fun onDoubleTap(event: MotionEvent): Boolean { + nextDownIsDoubleTap = true + 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 c06d89b843..d2f3c5aed2 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 @@ -34,41 +34,70 @@ import static org.godotengine.godot.utils.GLUtils.DEBUG; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; -import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; +import android.content.Context; +import android.hardware.input.InputManager; 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; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputDeviceListener { - private final GodotRenderView mRenderView; - private final InputManagerCompat mInputManager; - - private final String tag = this.getClass().getSimpleName(); +public class GodotInputHandler implements InputManager.InputDeviceListener { + 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 = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext()); + 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; @@ -76,6 +105,10 @@ public class GodotInputHandler implements 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; @@ -86,7 +119,7 @@ public class GodotInputHandler implements 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) { @@ -95,10 +128,14 @@ public class GodotInputHandler implements 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; } @@ -116,11 +153,10 @@ public class GodotInputHandler implements 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; @@ -130,74 +166,71 @@ public class GodotInputHandler implements 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); } } @@ -212,17 +245,8 @@ public class GodotInputHandler implements 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; @@ -234,7 +258,7 @@ public class GodotInputHandler implements 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); } @@ -279,13 +303,12 @@ public class GodotInputHandler implements 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; } @@ -297,14 +320,14 @@ public class GodotInputHandler implements 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); @@ -329,13 +352,6 @@ public class GodotInputHandler implements 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) { @@ -401,39 +417,113 @@ public class GodotInputHandler implements 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, false); + } + + static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, boolean doubleTap) { + return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, 0, 0, doubleTap); + } + + static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleTap) { + if (isMouseEvent(eventSource)) { + return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleTap, false); + } + + return handleTouchEvent(eventAction, x, y, doubleTap); + } + + 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, 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, false); + } + + static boolean handleTouchEvent(int eventAction, float x, float y, boolean doubleTap) { + return handleTouchEvent(eventAction, 0, 1, new float[] { 0, x, y }, doubleTap); + } + + static boolean handleTouchEvent(int eventAction, int actionPointerId, int pointerCount, float[] positions, boolean doubleTap) { + 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, doubleTap); 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..01ad5ee415 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,40 +105,38 @@ 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); } } @Override public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { - if (mEdit == pTextView && isFullScreenEdit()) { + if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) { final String characters = pKeyEvent.getCharacters(); 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/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java deleted file mode 100644 index 21fdc658bb..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * 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.input; - -import android.content.Context; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -public interface InputManagerCompat { - /** - * Gets information about the input device with the specified id. - * - * @param id The device id - * @return The input device or null if not found - */ - InputDevice getInputDevice(int id); - - /** - * Gets the ids of all input devices in the system. - * - * @return The input device ids. - */ - int[] getInputDeviceIds(); - - /** - * Registers an input device listener to receive notifications about when - * input devices are added, removed or changed. - * - * @param listener The listener to register. - * @param handler The handler on which the listener should be invoked, or - * null if the listener should be invoked on the calling thread's - * looper. - */ - void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener, - Handler handler); - - /** - * Unregisters an input device listener. - * - * @param listener The listener to unregister. - */ - void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener); - - /* - * The following three calls are to simulate V16 behavior on pre-Jellybean - * devices. If you don't call them, your callback will never be called - * pre-API 16. - */ - - /** - * Pass the motion events to the InputManagerCompat. This is used to - * optimize for polling for controllers. If you do not pass these events in, - * polling will cause regular object creation. - * - * @param event the motion event from the app - */ - void onGenericMotionEvent(MotionEvent event); - - /** - * Tell the V9 input manager that it should stop polling for disconnected - * devices. You can call this during onPause in your activity, although you - * might want to call it whenever your game is not active (or whenever you - * don't care about being notified of new input devices) - */ - void onPause(); - - /** - * Tell the V9 input manager that it should start polling for disconnected - * devices. You can call this during onResume in your activity, although you - * might want to call it less often (only when the gameplay is actually - * active) - */ - void onResume(); - - interface InputDeviceListener { - /** - * Called whenever the input manager detects that a device has been - * added. This will only be called in the V9 version when a motion event - * is detected. - * - * @param deviceId The id of the input device that was added. - */ - void onInputDeviceAdded(int deviceId); - - /** - * Called whenever the properties of an input device have changed since - * they were last queried. This will not be called for the V9 version of - * the API. - * - * @param deviceId The id of the input device that changed. - */ - void onInputDeviceChanged(int deviceId); - - /** - * Called whenever the input manager detects that a device has been - * removed. For the V9 version, this can take some time depending on the - * poll rate. - * - * @param deviceId The id of the input device that was removed. - */ - void onInputDeviceRemoved(int deviceId); - } - - /** - * Use this to construct a compatible InputManager. - */ - class Factory { - /** - * Constructs and returns a compatible InputManger - * - * @param context the Context that will be used to get the system - * service from - * @return a compatible implementation of InputManager - */ - public static InputManagerCompat getInputManager(Context context) { - return new InputManagerV16(context); - } - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java deleted file mode 100644 index 0dbc13c77b..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * 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.input; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.input.InputManager; -import android.os.Build; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -import java.util.HashMap; -import java.util.Map; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class InputManagerV16 implements InputManagerCompat { - private final InputManager mInputManager; - private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners; - - public InputManagerV16(Context context) { - mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); - mListeners = new HashMap<>(); - } - - @Override - public InputDevice getInputDevice(int id) { - return mInputManager.getInputDevice(id); - } - - @Override - public int[] getInputDeviceIds() { - return mInputManager.getInputDeviceIds(); - } - - static class V16InputDeviceListener implements InputManager.InputDeviceListener { - final InputManagerCompat.InputDeviceListener mIDL; - - public V16InputDeviceListener(InputDeviceListener idl) { - mIDL = idl; - } - - @Override - public void onInputDeviceAdded(int deviceId) { - mIDL.onInputDeviceAdded(deviceId); - } - - @Override - public void onInputDeviceChanged(int deviceId) { - mIDL.onInputDeviceChanged(deviceId); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - mIDL.onInputDeviceRemoved(deviceId); - } - } - - @Override - public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { - V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); - mInputManager.registerInputDeviceListener(v16Listener, handler); - mListeners.put(listener, v16Listener); - } - - @Override - public void unregisterInputDeviceListener(InputDeviceListener listener) { - V16InputDeviceListener curListener = mListeners.remove(listener); - if (null != curListener) { - mInputManager.unregisterInputDeviceListener(curListener); - } - } - - @Override - public void onGenericMotionEvent(MotionEvent event) { - // unused in V16 - } - - @Override - public void onPause() { - // unused in V16 - } - - @Override - public void onResume() { - // unused in V16 - } -} 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/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt new file mode 100644 index 0000000000..c6b242a4b6 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* FileAccessFlags.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 + +/** + * Android representation of Godot native access flags. + */ +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), + + /** + * Opens the file for write operations. + * The file is created if it does not exist, and truncated if it does. + */ + WRITE(2), + + /** + * 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), + + /** + * 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); + + fun getMode(): String { + return when (this) { + READ -> "r" + WRITE -> "w" + READ_WRITE, WRITE_READ -> "rw" + } + } + + fun shouldTruncate(): Boolean { + return when (this) { + READ, READ_WRITE -> false + WRITE, WRITE_READ -> 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/input/GodotGestureHandler.java b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt index ac13cad23e..5af694ad99 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotGestureHandler.java */ +/* FileData.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,69 +28,66 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot.input; +package org.godotengine.godot.io.file -import org.godotengine.godot.GodotLib; -import org.godotengine.godot.GodotRenderView; - -import android.view.GestureDetector; -import android.view.MotionEvent; +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile +import java.nio.channels.FileChannel /** - * Handles gesture input related events for the {@link GodotRenderView} view. - * https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener + * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions. */ -public class GodotGestureHandler extends GestureDetector.SimpleOnGestureListener { - private final GodotRenderView mRenderView; +internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) { - public GodotGestureHandler(GodotRenderView godotView) { - mRenderView = godotView; - } + companion object { + private val TAG = FileData::class.java.simpleName - private void queueEvent(Runnable task) { - mRenderView.queueOnRenderThread(task); - } + fun fileExists(path: String): Boolean { + return try { + File(path).isFile + } catch (e: SecurityException) { + false + } + } - @Override - public boolean onDown(MotionEvent event) { - super.onDown(event); - //Log.i("GodotGesture", "onDown"); - return true; - } + fun fileLastModified(filepath: String): Long { + return try { + File(filepath).lastModified() + } catch (e: SecurityException) { + 0L + } + } - @Override - public boolean onSingleTapConfirmed(MotionEvent event) { - super.onSingleTapConfirmed(event); - return true; - } + fun delete(filepath: String): Boolean { + return try { + File(filepath).delete() + } catch (e: Exception) { + false + } + } - @Override - public void onLongPress(MotionEvent event) { - //Log.i("GodotGesture", "onLongPress"); + fun rename(from: String, to: String): Boolean { + return try { + val fromFile = File(from) + fromFile.renameTo(File(to)) + } catch (e: Exception) { + false + } + } } - @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; - } + override val fileChannel: FileChannel - @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; - } + init { + if (accessFlag == FileAccessFlags.WRITE) { + fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel + } else { + fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel + } - @Override - public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { - //Log.i("GodotGesture", "onFling"); - return true; + 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/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java new file mode 100644 index 0000000000..2239ddac8e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -0,0 +1,298 @@ +/*************************************************************************/ +/* GodotTTS.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.godot.tts; + +import org.godotengine.godot.GodotLib; + +import android.app.Activity; +import android.os.Bundle; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.Voice; + +import androidx.annotation.Keep; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + +/** + * Wrapper for Android Text to Speech API and custom utterance query implementation. + * <p> + * A [GodotTTS] provides the following features: + * <p> + * <ul> + * <li>Access to the Android Text to Speech API. + * <li>Utterance pause / resume functions, unsupported by Android TTS API. + * </ul> + */ +@Keep +public class GodotTTS extends UtteranceProgressListener { + // Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.h". + final private static int EVENT_START = 0; + final private static int EVENT_END = 1; + final private static int EVENT_CANCEL = 2; + final private static int EVENT_BOUNDARY = 3; + + final private TextToSpeech synth; + final private LinkedList<GodotUtterance> queue; + final private Object lock = new Object(); + private GodotUtterance lastUtterance; + + private boolean speaking; + private boolean paused; + + public GodotTTS(Activity p_activity) { + synth = new TextToSpeech(p_activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + private void updateTTS() { + if (!speaking && queue.size() > 0) { + int mode = TextToSpeech.QUEUE_FLUSH; + GodotUtterance message = queue.pollFirst(); + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(message.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(message.pitch); + synth.setSpeechRate(message.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f); + + lastUtterance = message; + lastUtterance.start = 0; + lastUtterance.offset = 0; + paused = false; + + synth.speak(message.text, mode, params, String.valueOf(message.id)); + speaking = true; + } + } + + /** + * Called by TTS engine when the TTS service is about to speak the specified range. + */ + @Override + public void onRangeStart(String utteranceId, int start, int end, int frame) { + synchronized (lock) { + if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) { + lastUtterance.offset = start; + GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start); + } + } + } + + /** + * Called by TTS engine when an utterance was canceled in progress. + */ + @Override + public void onStop(String utteranceId, boolean interrupted) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an utterance has begun to be spoken.. + */ + @Override + public void onStart(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0); + } + } + } + + /** + * Called by TTS engine when an utterance was successfully finished. + */ + @Override + public void onDone(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing. + */ + @Override + public void onError(String utteranceId, int errorCode) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing (pre API level 21 version). + */ + @Override + public void onError(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Adds an utterance to the queue. + */ + public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { + synchronized (lock) { + GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id); + queue.addLast(message); + + if (isPaused()) { + resumeSpeaking(); + } else { + updateTTS(); + } + } + } + + /** + * Puts the synthesizer into a paused state. + */ + public void pauseSpeaking() { + synchronized (lock) { + if (!paused) { + paused = true; + synth.stop(); + } + } + } + + /** + * Resumes the synthesizer if it was paused. + */ + public void resumeSpeaking() { + synchronized (lock) { + if (lastUtterance != null && paused) { + int mode = TextToSpeech.QUEUE_FLUSH; + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(lastUtterance.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(lastUtterance.pitch); + synth.setSpeechRate(lastUtterance.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f); + + lastUtterance.start = lastUtterance.offset; + lastUtterance.offset = 0; + paused = false; + + synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id)); + speaking = true; + } else { + paused = false; + } + } + } + + /** + * Stops synthesis in progress and removes all utterances from the queue. + */ + public void stopSpeaking() { + synchronized (lock) { + for (GodotUtterance u : queue) { + GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0); + } + queue.clear(); + + if (lastUtterance != null) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + } + lastUtterance = null; + + paused = false; + speaking = false; + + synth.stop(); + } + } + + /** + * Returns voice information. + */ + public String[] getVoices() { + Set<Voice> voices = synth.getVoices(); + String[] list = new String[voices.size()]; + int i = 0; + for (Voice v : voices) { + list[i++] = v.getLocale().toString() + ";" + v.getName(); + } + return list; + } + + /** + * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue. + */ + public boolean isSpeaking() { + return speaking; + } + + /** + * Returns true if the synthesizer is in a paused state. + */ + public boolean isPaused() { + return paused; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java new file mode 100644 index 0000000000..bde37e7315 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java @@ -0,0 +1,55 @@ +/*************************************************************************/ +/* GodotUtterance.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.godot.tts; + +/** + * A speech request for GodotTTS. + */ +class GodotUtterance { + final String text; + final String voice; + final int volume; + final float pitch; + final float rate; + final int id; + + int offset = -1; + int start = 0; + + GodotUtterance(String text, String voice, int volume, float pitch, float rate, int id) { + this.text = text; + this.voice = voice; + this.volume = volume; + this.pitch = pitch; + this.rate = rate; + this.id = id; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java index 39a57f587a..47df23fe1a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java @@ -43,8 +43,8 @@ public class Crypt { // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) - hexString.append(Integer.toHexString(0xFF & messageDigest[i])); + for (byte b : messageDigest) + hexString.append(Integer.toHexString(0xFF & b)); return hexString.toString(); } catch (Exception e) { 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/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java index 4c1c84affb..e35d4f5828 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGLExt; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java index 2b4369b8a6..deb9c4bb1d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGL14; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java index fbfe0a3a75..f087b7dc74 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java @@ -30,7 +30,7 @@ package org.godotengine.godot.xr.ovr; -import android.opengl.GLSurfaceView; +import org.godotengine.godot.gl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java index 9fde1961ea..445238b1c2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java @@ -30,10 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; - import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index ce1184a75c..5d62723170 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -30,9 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java index 420dda45a0..68329c5c49 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java @@ -30,8 +30,6 @@ package org.godotengine.godot.xr.regular; -import org.godotengine.godot.utils.GLUtils; - import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index 966c02f7d7..711f7cd502 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -16,3 +16,5 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${GODOT_ROOT_DIR}) + +add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED) diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle index 158bb2b98e..5e810ae1ba 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -1,11 +1,13 @@ // 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 { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk @@ -17,6 +19,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + packagingOptions { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' @@ -28,8 +34,6 @@ android { } } - ndkVersion versions.ndkVersion - externalNativeBuild { cmake { path "CMakeLists.txt" diff --git a/platform/android/java/scripts/publish-module.gradle b/platform/android/java/scripts/publish-module.gradle index 6b2aea5caf..32b749e493 100644 --- a/platform/android/java/scripts/publish-module.gradle +++ b/platform/android/java/scripts/publish-module.gradle @@ -7,20 +7,15 @@ version = PUBLISH_VERSION afterEvaluate { publishing { publications { - release(MavenPublication) { + templateRelease(MavenPublication) { + from components.templateRelease + // The coordinates of the library, being set from variables that // we'll set up later groupId ossrhGroupId artifactId PUBLISH_ARTIFACT_ID version PUBLISH_VERSION - // Two artifacts, the `aar` (or `jar`) and the sources - if (project.plugins.findPlugin("com.android.library")) { - from components.release - } else { - from components.java - } - // Mostly self-explanatory metadata pom { name = PUBLISH_ARTIFACT_ID diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 584b626900..466ffebf22 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -1,9 +1,25 @@ // 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' include ':lib' include ':nativeSrcsConfigs' +include ':editor' include ':assetPacks:installTime' project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime") diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 1805807f90..349ae704f9 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -34,16 +34,16 @@ #include "thread_jandroid.h" bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret) { - Map<StringName, List<MethodInfo>>::Element *M = methods.find(p_method); + HashMap<StringName, List<MethodInfo>>::Iterator M = methods.find(p_method); if (!M) { return false; } 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->get()) { + for (MethodInfo &E : M->value) { if (!p_instance && !E._static) { r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; continue; @@ -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 d6e3ad90b1..cea64a7f22 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -31,6 +31,8 @@ #include "java_godot_io_wrapper.h" #include "core/error/error_list.h" +#include "core/math/rect2.h" +#include "core/variant/variant.h" // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. @@ -51,13 +53,15 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _open_URI = p_env->GetMethodID(cls, "openURI", "(Ljava/lang/String;)I"); _get_cache_dir = p_env->GetMethodID(cls, "getCacheDir", "()Ljava/lang/String;"); _get_data_dir = p_env->GetMethodID(cls, "getDataDir", "()Ljava/lang/String;"); + _get_display_cutouts = p_env->GetMethodID(cls, "getDisplayCutouts", "()[I"), + _get_display_safe_area = p_env->GetMethodID(cls, "getDisplaySafeArea", "()[I"), _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); + _get_scaled_density = p_env->GetMethodID(cls, "getScaledDensity", "()F"); _get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D"); - _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _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"); @@ -76,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 { @@ -87,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 { @@ -98,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 { @@ -109,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 { @@ -120,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 { @@ -131,13 +135,23 @@ 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; } } +float GodotIOJavaWrapper::get_scaled_density() { + if (_get_scaled_density) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, 1.0f); + return env->CallFloatMethod(godot_io_instance, _get_scaled_density); + } else { + return 1.0f; + } +} + float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { if (_get_screen_refresh_rate) { JNIEnv *env = get_jni_env(); @@ -151,24 +165,44 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { return fallback; } -void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { - if (_screen_get_usable_rect) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _screen_get_usable_rect); - ERR_FAIL_COND(env->GetArrayLength(returnArray) != 4); - jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); - for (int i = 0; i < 4; i++) { - p_rect_xywh[i] = arrayBody[i]; - } - env->ReleaseIntArrayElements(returnArray, arrayBody, 0); +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); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_cutouts); + jint arrayLength = env->GetArrayLength(returnArray); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + int cutouts = arrayLength / 4; + for (int i = 0; i < cutouts; i++) { + int x = arrayBody[i * 4]; + int y = arrayBody[i * 4 + 1]; + int width = arrayBody[i * 4 + 2]; + int height = arrayBody[i * 4 + 3]; + Rect2 cutout(x, y, width, height); + result.append(cutout); } + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; +} + +Rect2i GodotIOJavaWrapper::get_display_safe_area() { + Rect2i result; + ERR_FAIL_NULL_V(_get_display_safe_area, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_safe_area); + ERR_FAIL_COND_V(env->GetArrayLength(returnArray) != 4, result); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + result = Rect2i(arrayBody[0], arrayBody[1], arrayBody[2], arrayBody[3]); + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; } 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 { @@ -177,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); } } @@ -200,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); } } @@ -208,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; @@ -218,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 38a2b710a9..24995147d4 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -28,15 +28,14 @@ /* 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 #include <android/log.h> #include <jni.h> +#include "core/math/rect2i.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++ @@ -48,11 +47,13 @@ private: jmethodID _open_URI = 0; jmethodID _get_cache_dir = 0; jmethodID _get_data_dir = 0; + jmethodID _get_display_cutouts = 0; + jmethodID _get_display_safe_area = 0; jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; + jmethodID _get_scaled_density = 0; jmethodID _get_screen_refresh_rate = 0; - jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; jmethodID _show_keyboard = 0; jmethodID _hide_keyboard = 0; @@ -72,11 +73,13 @@ public: String get_locale(); String get_model(); int get_screen_dpi(); + float get_scaled_density(); float get_screen_refresh_rate(float fallback); - void screen_get_usable_rect(int (&p_rect_xywh)[4]); + 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); @@ -85,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 249717921f..b5cb9d341d 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -43,12 +43,14 @@ #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" #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" +#include "tts_android.h" #include <android/input.h> #include <unistd.h> @@ -61,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; @@ -78,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); @@ -94,19 +93,21 @@ 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)); + 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) { // lets cleanup + if (java_class_wrapper) { + memdelete(java_class_wrapper); + } if (godot_io_java) { delete godot_io_java; } @@ -117,11 +118,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env delete input_handler; } if (os_android) { + os_android->main_loop_end(); delete os_android; } } -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; @@ -131,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; @@ -146,7 +148,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc } } - Error err = Main::setup("apk", cmdlen, (char **)cmdline, false); + Error err = Main::setup(OS_Android::ANDROID_EXEC_PATH, cmdlen, (char **)cmdline, false); if (cmdline) { if (j_cmdline) { for (int i = 0; i < cmdlen; ++i) { @@ -157,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) { @@ -209,9 +213,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jcl } } -JNIEXPORT void 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) { + TTS_Android::_java_utterance_callback(event, id, pos); +} + +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { if (step.get() == -1) { - return; + return true; } if (step.get() == 0) { @@ -220,12 +228,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl Main::setup2(Thread::get_caller_id()); input_handler = new AndroidInputHandler(); step.increment(); - return; + return true; } if (step.get() == 1) { if (!Main::start()) { - return; // should exit instead and print the error + return true; // should exit instead and print the error } godot_java->on_godot_setup_completed(env); @@ -239,12 +247,25 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl DisplayServerAndroid::get_singleton()->process_magnetometer(magnetometer); DisplayServerAndroid::get_singleton()->process_gyroscope(gyroscope); - if (os_android->main_loop_iterate()) { + bool should_swap_buffers = false; + if (os_android->main_loop_iterate(&should_swap_buffers)) { godot_java->force_quit(env); } + + return should_swap_buffers; +} + +// 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); } -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_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray position, jboolean p_double_tap) { if (step.get() <= 0) { return; } @@ -252,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, p_double_tap); } // 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 @@ -375,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) { @@ -418,12 +409,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path) { String js = jstring_to_string(path, env); - return env->NewStringUTF(ProjectSettings::get_singleton()->get(js).operator String().utf8().get_data()); + return env->NewStringUTF(GLOBAL_GET(js).operator String().utf8().get_data()); } 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); @@ -434,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); @@ -467,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); } @@ -496,6 +486,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI return; } + // We force redraw to ensure we render at least once when resuming the app. + Main::force_redraw(); if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_RESUMED); } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 927b44ddb6..f3f2646bfb 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,21 +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 void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +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, jboolean p_double_tap); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_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); @@ -71,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..762840a4b1 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -34,41 +34,52 @@ 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); _cls = (jclass)env->NewGlobalRef(env->GetObjectClass(godot_view)); - if (android_get_device_api_level() >= __ANDROID_API_O__) { + int android_device_api_level = android_get_device_api_level(); + if (android_device_api_level >= __ANDROID_API_N__) { + _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); + } + if (android_device_api_level >= __ANDROID_API_O__) { _request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V"); _release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V"); - _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); } } +bool GodotJavaViewWrapper::can_update_pointer_icon() const { + return _set_pointer_icon != nullptr; +} + +bool GodotJavaViewWrapper::can_capture_pointer() const { + return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr; +} + 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 +87,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..b398c73cac 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> @@ -50,6 +50,9 @@ private: public: GodotJavaViewWrapper(jobject godot_view); + bool can_update_pointer_icon() const; + bool can_capture_pointer() const; + void request_pointer_capture(); void release_pointer_capture(); void set_pointer_icon(int pointer_type); @@ -57,4 +60,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 754267c834..416b98c895 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"); @@ -77,13 +77,24 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); + _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V"); + _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); } GodotJavaWrapper::~GodotJavaWrapper() { - // nothing to do here for now + if (godot_view) { + delete godot_view; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + env->DeleteGlobalRef(godot_instance); + env->DeleteGlobalRef(godot_class); + env->DeleteGlobalRef(activity); + env->DeleteGlobalRef(activity_class); } jobject GodotJavaWrapper::get_activity() { @@ -95,9 +106,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 { @@ -108,8 +117,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; @@ -117,26 +125,29 @@ jobject GodotJavaWrapper::get_class_loader() { } GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { - if (_godot_view != nullptr) { - return _godot_view; + if (godot_view != nullptr) { + return godot_view; } - JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 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; + if (_get_render_view) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, nullptr); + jobject godot_render_view = env->CallObjectMethod(godot_instance, _get_render_view); + if (!env->IsSameObject(godot_render_view, nullptr)) { + godot_view = new GodotJavaViewWrapper(godot_render_view); + } + } + 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) { @@ -153,7 +164,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); } } @@ -163,8 +174,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); } } @@ -174,8 +184,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); } } @@ -183,8 +192,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); } } @@ -192,8 +200,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); @@ -202,24 +209,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 { @@ -230,8 +234,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 { @@ -240,28 +243,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; @@ -271,8 +272,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 { @@ -283,8 +283,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; @@ -295,14 +294,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); @@ -315,8 +312,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); } } @@ -324,8 +320,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; @@ -335,8 +330,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; @@ -346,8 +340,19 @@ 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); } } + +void GodotJavaWrapper::create_new_godot_instance(List<String> args) { + if (_create_new_godot_instance) { + JNIEnv *env = get_jni_env(); + 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())); + } + env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 42ae91480f..fb9c4c77fc 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -28,15 +28,13 @@ /* 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 #include <android/log.h> #include <jni.h> +#include "core/templates/list.h" #include "java_godot_view_wrapper.h" #include "string_android.h" @@ -48,28 +46,30 @@ private: jclass godot_class; jclass activity_class; - GodotJavaViewWrapper *_godot_view = nullptr; + 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 _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; + jmethodID _get_render_view = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -81,7 +81,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); @@ -103,6 +103,7 @@ public: bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); + 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..2b0ee50570 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); } } @@ -167,9 +167,8 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a v.obj = arr; } break; -#ifndef _MSC_VER -#warning This is missing 64 bits arrays, I have no idea how to do it in JNI -#endif + + // TODO: This is missing 64 bits arrays, I have no idea how to do it in JNI. default: { v.val.i = 0; @@ -186,7 +185,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); @@ -266,33 +265,33 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj) { if (name == "[D") { jdoubleArray arr = (jdoubleArray)obj; int fCount = env->GetArrayLength(arr); - PackedFloat32Array sarr; - sarr.resize(fCount); + PackedFloat64Array packed_array; + packed_array.resize(fCount); - real_t *w = sarr.ptrw(); + double *w = packed_array.ptrw(); for (int i = 0; i < fCount; i++) { double n; env->GetDoubleArrayRegion(arr, i, 1, &n); w[i] = n; } - return sarr; + return packed_array; } if (name == "[F") { jfloatArray arr = (jfloatArray)obj; int fCount = env->GetArrayLength(arr); - PackedFloat32Array sarr; - sarr.resize(fCount); + PackedFloat32Array packed_array; + packed_array.resize(fCount); - real_t *w = sarr.ptrw(); + float *w = packed_array.ptrw(); for (int i = 0; i < fCount; i++) { float n; env->GetFloatArrayRegion(arr, i, 1, &n); w[i] = n; } - return sarr; + return packed_array; } if (name == "[Ljava.lang.Object;") { 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 b17b0f3139..4469c7a0f7 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -35,16 +35,22 @@ #include "drivers/unix/file_access_unix.h" #include "main/main.h" #include "platform/android/display_server_android.h" +#include "scene/main/scene_tree.h" +#include "servers/rendering_server.h" #include "dir_access_jandroid.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "net_socket_android.h" #include <dlfcn.h> +#include <sys/system_properties.h> #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" +const char *OS_Android::ANDROID_EXEC_PATH = "apk"; + String _remove_symlink(const String &dir) { // Workaround for Android 6.0+ using a symlink. // Save the current directory. @@ -72,29 +78,36 @@ 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); } void OS_Android::initialize_core() { OS_Unix::initialize_core(); +#ifdef TOOLS_ENABLED + FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); +#else if (use_apk_expansion) { FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); } else { FileAccess::make_default<FileAccessAndroid>(FileAccess::ACCESS_RESOURCES); } +#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); +#else if (use_apk_expansion) { DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES); } else { 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(); } @@ -125,7 +138,7 @@ void OS_Android::finalize() { } OS_Android *OS_Android::get_singleton() { - return (OS_Android *)OS::get_singleton(); + return static_cast<OS_Android *>(OS::get_singleton()); } GodotJavaWrapper *OS_Android::get_godot_java() { @@ -148,9 +161,14 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +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; + } + return OK; } @@ -158,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; } @@ -168,16 +259,27 @@ void OS_Android::main_loop_begin() { } } -bool OS_Android::main_loop_iterate() { +bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) { if (!main_loop) { return false; } DisplayServerAndroid::get_singleton()->process_events(); - return Main::iteration(); + uint64_t current_frames_drawn = Engine::get_singleton()->get_frames_drawn(); + bool exit = Main::iteration(); + + if (r_should_swap_buffers) { + *r_should_swap_buffers = !is_in_low_processor_usage_mode() || RenderingServer::get_singleton()->has_changed() || current_frames_drawn != Engine::get_singleton()->get_frames_drawn(); + } + + return exit; } void OS_Android::main_loop_end() { if (main_loop) { + SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop); + if (scene_tree) { + scene_tree->quit(); + } main_loop->finalize(); } } @@ -197,7 +299,11 @@ Error OS_Android::shell_open(String p_uri) { } String OS_Android::get_resource_dir() const { +#ifdef TOOLS_ENABLED + return OS_Unix::get_resource_dir(); +#else return "/"; //android has its own filesystem for resources inside the APK +#endif } String OS_Android::get_locale() const { @@ -222,6 +328,14 @@ String OS_Android::get_data_path() const { return get_user_data_dir(); } +String OS_Android::get_executable_path() const { + // Since unix process creation is restricted on Android, we bypass + // OS_Unix::get_executable_path() so we can return ANDROID_EXEC_PATH. + // Detection of ANDROID_EXEC_PATH allows to handle process creation in an Android compliant + // manner. + return OS::get_executable_path(); +} + String OS_Android::get_user_data_dir() const { if (!data_dir_cache.is_empty()) { return data_dir_cache; @@ -261,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; } @@ -271,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 } @@ -294,20 +435,24 @@ void OS_Android::vibrate_handheld(int p_duration_ms) { godot_java->vibrate(p_duration_ms); } +String OS_Android::get_config_path() const { + return get_user_data_dir().path_join("config"); +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "mobile") { 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 @@ -343,5 +488,26 @@ OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_god DisplayServerAndroid::register_android_driver(); } +Error OS_Android::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments); + } else { + return OS_Unix::execute(p_path, p_arguments, r_pipe, r_exitcode, read_stderr, p_pipe_mutex, p_open_console); + } +} + +Error OS_Android::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments, r_child_id); + } else { + return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); + } +} + +Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { + godot_java->create_new_godot_instance(p_arguments); + return OK; +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index f523f172c6..d6546a3507 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -52,7 +52,7 @@ private: #endif #if defined(VULKAN_ENABLED) - ANativeWindow *native_window; + ANativeWindow *native_window = nullptr; #endif mutable String data_dir_cache; @@ -60,12 +60,16 @@ private: AudioDriverOpenSL audio_driver_android; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; - GodotJavaWrapper *godot_java; - GodotIOJavaWrapper *godot_io_java; + GodotJavaWrapper *godot_java = nullptr; + GodotIOJavaWrapper *godot_io_java = nullptr; + + String get_system_property(const char *key) const; public: + static const char *ANDROID_EXEC_PATH; + virtual void initialize_core() override; virtual void initialize() override; @@ -88,13 +92,15 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual 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(); - bool main_loop_iterate(); + bool main_loop_iterate(bool *r_should_swap_buffers = nullptr); void main_loop_end(); void main_loop_focusout(); void main_loop_focusin(); @@ -108,6 +114,7 @@ public: ANativeWindow *get_native_window() const; virtual Error shell_open(String p_uri) override; + virtual String get_executable_path() const override; virtual String get_user_data_dir() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; @@ -119,11 +126,19 @@ 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; + + virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; + virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; + virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; + virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); ~OS_Android(); }; -#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..498977ad49 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); } @@ -137,7 +137,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis // Retrieve the current list of gdnative libraries. Array singletons = Array(); if (ProjectSettings::get_singleton()->has_setting("gdnative/singletons")) { - singletons = ProjectSettings::get_singleton()->get("gdnative/singletons"); + singletons = GLOBAL_GET("gdnative/singletons"); } // Insert the libraries provided by the plugin 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.cpp b/platform/android/thread_jandroid.cpp index 61b471866f..9f87303341 100644 --- a/platform/android/thread_jandroid.cpp +++ b/platform/android/thread_jandroid.cpp @@ -63,7 +63,7 @@ static void term_thread() { void init_thread_jandroid(JavaVM *p_jvm, JNIEnv *p_env) { java_vm = p_jvm; env = p_env; - Thread::_set_platform_funcs(nullptr, nullptr, &init_thread, &term_thread); + Thread::_set_platform_functions({ .init = init_thread, .term = &term_thread }); } void setup_android_thread() { 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 new file mode 100644 index 0000000000..27ba8da448 --- /dev/null +++ b/platform/android/tts_android.cpp @@ -0,0 +1,189 @@ +/*************************************************************************/ +/* tts_android.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 "tts_android.h" + +#include "java_godot_wrapper.h" +#include "os_android.h" +#include "string_android.h" +#include "thread_jandroid.h" + +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; + +void TTS_Android::setup(jobject p_tts) { + JNIEnv *env = get_jni_env(); + + tts = env->NewGlobalRef(p_tts); + + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); + + _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); + _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); + _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); + _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); + _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); + _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); + _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); +} + +void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + if (ids.has(p_id)) { + int pos = 0; + if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-16 to UTF-32. + const Char16String &string = ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + } else if ((DisplayServer::TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ids.erase(p_id); + } + DisplayServer::get_singleton()->tts_post_utterance_event((DisplayServer::TTSUtteranceEvent)p_event, p_id, pos); + } +} + +bool TTS_Android::is_speaking() { + if (_is_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_speaking); + } else { + return false; + } +} + +bool TTS_Android::is_paused() { + if (_is_paused) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_paused); + } else { + return false; + } +} + +Array TTS_Android::get_voices() { + Array list; + if (_get_voices) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, list); + + jobject voices_object = env->CallObjectMethod(tts, _get_voices); + jobjectArray *arr = reinterpret_cast<jobjectArray *>(&voices_object); + + jsize len = env->GetArrayLength(*arr); + for (int i = 0; i < len; i++) { + jstring jStr = (jstring)env->GetObjectArrayElement(*arr, i); + String str = jstring_to_string(jStr, env); + Vector<String> tokens = str.split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + list.push_back(voice_d); + } + env->DeleteLocalRef(jStr); + } + } + return list; +} + +void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + ids[p_utterance_id] = p_text.utf16(); + + if (_speak) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); + jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); + env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt); + } +} + +void TTS_Android::pause() { + if (_pause_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _pause_speaking); + } +} + +void TTS_Android::resume() { + if (_resume_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _resume_speaking); + } +} + +void TTS_Android::stop() { + for (const KeyValue<int, Char16String> &E : ids) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); + } + ids.clear(); + + if (_stop_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _stop_speaking); + } +} diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h new file mode 100644 index 0000000000..bc0cdb8d55 --- /dev/null +++ b/platform/android/tts_android.h @@ -0,0 +1,67 @@ +/*************************************************************************/ +/* tts_android.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 TTS_ANDROID_H +#define TTS_ANDROID_H + +#include "core/string/ustring.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <jni.h> + +class TTS_Android { + static jobject tts; + static jclass cls; + + static jmethodID _is_speaking; + static jmethodID _is_paused; + static jmethodID _get_voices; + static jmethodID _speak; + static jmethodID _pause_speaking; + static jmethodID _resume_speaking; + static jmethodID _stop_speaking; + + static HashMap<int, Char16String> ids; + +public: + static void setup(jobject p_tts); + static void _java_utterance_callback(int p_event, int p_id, int p_pos); + + static bool is_speaking(); + static bool is_paused(); + static Array get_voices(); + static void speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt); + static void pause(); + static void resume(); + static void stop(); +}; + +#endif // TTS_ANDROID_H diff --git a/platform/android/vulkan/vulkan_context_android.cpp b/platform/android/vulkan/vulkan_context_android.cpp index 5945421e17..948292c3af 100644 --- a/platform/android/vulkan/vulkan_context_android.cpp +++ b/platform/android/vulkan/vulkan_context_android.cpp @@ -28,6 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#ifdef VULKAN_ENABLED + #include "vulkan_context_android.h" #ifdef USE_VOLK @@ -40,7 +42,7 @@ const char *VulkanContextAndroid::_get_platform_surface_extension() const { return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME; } -int VulkanContextAndroid::window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height) { +Error VulkanContextAndroid::window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height) { VkAndroidSurfaceCreateInfoKHR createInfo; createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; createInfo.pNext = nullptr; @@ -50,7 +52,7 @@ int VulkanContextAndroid::window_create(ANativeWindow *p_window, DisplayServer:: VkSurfaceKHR surface; VkResult err = vkCreateAndroidSurfaceKHR(get_instance(), &createInfo, nullptr, &surface); if (err != VK_SUCCESS) { - ERR_FAIL_V_MSG(-1, "vkCreateAndroidSurfaceKHR failed with error " + itos(err)); + ERR_FAIL_V_MSG(ERR_CANT_CREATE, "vkCreateAndroidSurfaceKHR failed with error " + itos(err)); } return _window_create(DisplayServer::MAIN_WINDOW_ID, p_vsync_mode, surface, p_width, p_height); @@ -63,3 +65,5 @@ bool VulkanContextAndroid::_use_validation_layers() { // On Android, we use validation layers automatically if they were explicitly linked with the app. return count > 0; } + +#endif // VULKAN_ENABLED diff --git a/platform/android/vulkan/vulkan_context_android.h b/platform/android/vulkan/vulkan_context_android.h index 43b5d62598..fe9a033e1c 100644 --- a/platform/android/vulkan/vulkan_context_android.h +++ b/platform/android/vulkan/vulkan_context_android.h @@ -31,6 +31,8 @@ #ifndef VULKAN_CONTEXT_ANDROID_H #define VULKAN_CONTEXT_ANDROID_H +#ifdef VULKAN_ENABLED + #include "drivers/vulkan/vulkan_context.h" struct ANativeWindow; @@ -39,7 +41,7 @@ class VulkanContextAndroid : public VulkanContext { virtual const char *_get_platform_surface_extension() const override; public: - int window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height); + Error window_create(ANativeWindow *p_window, DisplayServer::VSyncMode p_vsync_mode, int p_width, int p_height); VulkanContextAndroid() = default; ~VulkanContextAndroid() override = default; @@ -48,4 +50,6 @@ protected: bool _use_validation_layers() override; }; +#endif // VULKAN_ENABLED + #endif // VULKAN_CONTEXT_ANDROID_H |