diff options
Diffstat (limited to 'platform/android')
44 files changed, 480 insertions, 328 deletions
diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index e3f93086dc..63045237e9 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -49,14 +49,22 @@ void AndroidInputHandler::process_joy_event(AndroidInputHandler::JoypadEvent p_e } } -void AndroidInputHandler::_set_key_modifier_state(Ref<InputEventWithModifiers> ev) { - ev->set_shift_pressed(shift_mem); - ev->set_alt_pressed(alt_mem); - ev->set_meta_pressed(meta_mem); - ev->set_ctrl_pressed(control_mem); +void AndroidInputHandler::_set_key_modifier_state(Ref<InputEventWithModifiers> ev, Key p_keycode) { + if (p_keycode != Key::SHIFT) { + ev->set_shift_pressed(shift_mem); + } + if (p_keycode != Key::ALT) { + ev->set_alt_pressed(alt_mem); + } + if (p_keycode != Key::META) { + ev->set_meta_pressed(meta_mem); + } + if (p_keycode != Key::CTRL) { + ev->set_ctrl_pressed(control_mem); + } } -void AndroidInputHandler::process_key_event(int p_keycode, int p_physical_keycode, int p_unicode, bool p_pressed) { +void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed) { static char32_t prev_wc = 0; char32_t unicode = p_unicode; if ((p_unicode & 0xfffffc00) == 0xd800) { @@ -81,8 +89,18 @@ void AndroidInputHandler::process_key_event(int p_keycode, int p_physical_keycod 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 (unicode == '\b') { // 0x08 + keycode = Key::BACKSPACE; + } else if (unicode == '\t') { // 0x09 + keycode = Key::TAB; + } else if (unicode == '\n') { // 0x0A + keycode = Key::ENTER; + } else if (unicode == 0x1B) { + keycode = Key::ESCAPE; + } else if (unicode == 0x1F) { + keycode = Key::KEY_DELETE; + } else { + keycode = fix_keycode(unicode, physical_keycode); } switch (physical_keycode) { @@ -104,10 +122,11 @@ void AndroidInputHandler::process_key_event(int p_keycode, int p_physical_keycod ev->set_keycode(keycode); ev->set_physical_keycode(physical_keycode); - ev->set_unicode(unicode); + ev->set_key_label(fix_key_label(p_key_label, keycode)); + ev->set_unicode(fix_unicode(unicode)); ev->set_pressed(p_pressed); - _set_key_modifier_state(ev); + _set_key_modifier_state(ev, keycode); if (p_physical_keycode == AKEYCODE_BACK) { if (DisplayServerAndroid *dsa = Object::cast_to<DisplayServerAndroid>(DisplayServer::get_singleton())) { @@ -118,13 +137,22 @@ void AndroidInputHandler::process_key_event(int p_keycode, int p_physical_keycod Input::get_singleton()->parse_input_event(ev); } -void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_double_tap) { +void AndroidInputHandler::_cancel_all_touch() { + _parse_all_touch(false, false, true); + touch.clear(); +} + +void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index) { 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); + if (reset_index) { + ev->set_index(-1); + } else { + ev->set_index(touch[i].id); + } ev->set_pressed(p_pressed); ev->set_position(touch[i].pos); ev->set_double_tap(p_double_tap); @@ -185,7 +213,9 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const } } break; - case AMOTION_EVENT_ACTION_CANCEL: + case AMOTION_EVENT_ACTION_CANCEL: { + _cancel_all_touch(); + } break; case AMOTION_EVENT_ACTION_UP: { //release _release_all_touch(); } break; @@ -225,6 +255,12 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const } } +void AndroidInputHandler::_cancel_mouse_event_info(bool p_source_mouse_relative) { + buttons_state = BitField<MouseButtonMask>(); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + mouse_event_info.valid = false; +} + void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) { if (!mouse_event_info.valid) { return; @@ -232,7 +268,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even Ref<InputEventMouseButton> ev; ev.instantiate(); - _set_key_modifier_state(ev); + _set_key_modifier_state(ev, Key::NONE); if (p_source_mouse_relative) { ev->set_position(hover_prev_pos); ev->set_global_position(hover_prev_pos); @@ -266,7 +302,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an // https://developer.android.com/reference/android/view/MotionEvent.html#ACTION_HOVER_ENTER Ref<InputEventMouseMotion> ev; ev.instantiate(); - _set_key_modifier_state(ev); + _set_key_modifier_state(ev, Key::NONE); ev->set_position(p_event_pos); ev->set_global_position(p_event_pos); ev->set_relative(p_event_pos - hover_prev_pos); @@ -285,8 +321,11 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an _parse_mouse_event_info(event_buttons_mask, true, p_double_click, p_source_mouse_relative); } break; + case AMOTION_EVENT_ACTION_CANCEL: { + _cancel_mouse_event_info(p_source_mouse_relative); + } break; + 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; @@ -298,7 +337,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an Ref<InputEventMouseMotion> ev; ev.instantiate(); - _set_key_modifier_state(ev); + _set_key_modifier_state(ev, Key::NONE); if (p_source_mouse_relative) { ev->set_position(hover_prev_pos); ev->set_global_position(hover_prev_pos); @@ -317,7 +356,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an case AMOTION_EVENT_ACTION_SCROLL: { Ref<InputEventMouseButton> ev; ev.instantiate(); - _set_key_modifier_state(ev); + _set_key_modifier_state(ev, Key::NONE); if (p_source_mouse_relative) { ev->set_position(hover_prev_pos); ev->set_global_position(hover_prev_pos); @@ -344,7 +383,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an void AndroidInputHandler::_wheel_button_click(BitField<MouseButtonMask> event_buttons_mask, const Ref<InputEventMouseButton> &ev, MouseButton wheel_button, float factor) { Ref<InputEventMouseButton> evd = ev->duplicate(); - _set_key_modifier_state(evd); + _set_key_modifier_state(evd, Key::NONE); evd->set_button_index(wheel_button); evd->set_button_mask(BitField<MouseButtonMask>(event_buttons_mask.operator int64_t() ^ int64_t(mouse_button_to_mask(wheel_button)))); evd->set_factor(factor); @@ -358,7 +397,7 @@ void AndroidInputHandler::_wheel_button_click(BitField<MouseButtonMask> event_bu void AndroidInputHandler::process_magnify(Point2 p_pos, float p_factor) { Ref<InputEventMagnifyGesture> magnify_event; magnify_event.instantiate(); - _set_key_modifier_state(magnify_event); + _set_key_modifier_state(magnify_event, Key::NONE); magnify_event->set_position(p_pos); magnify_event->set_factor(p_factor); Input::get_singleton()->parse_input_event(magnify_event); @@ -367,7 +406,7 @@ void AndroidInputHandler::process_magnify(Point2 p_pos, float p_factor) { void AndroidInputHandler::process_pan(Point2 p_pos, Vector2 p_delta) { Ref<InputEventPanGesture> pan_event; pan_event.instantiate(); - _set_key_modifier_state(pan_event); + _set_key_modifier_state(pan_event, Key::NONE); pan_event->set_position(p_pos); pan_event->set_delta(p_delta); Input::get_singleton()->parse_input_event(pan_event); diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 34259efd81..2badd32636 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -76,7 +76,7 @@ private: MouseEventInfo mouse_event_info; Point2 hover_prev_pos; // needed to calculate the relative position on hover events - void _set_key_modifier_state(Ref<InputEventWithModifiers> ev); + void _set_key_modifier_state(Ref<InputEventWithModifiers> ev, Key p_keycode); static MouseButton _button_index_from_mask(BitField<MouseButtonMask> button_mask); static BitField<MouseButtonMask> _android_button_mask_to_godot_button_mask(int android_button_mask); @@ -87,17 +87,21 @@ private: void _release_mouse_event_info(bool p_source_mouse_relative = false); - void _parse_all_touch(bool p_pressed, bool p_double_tap); + void _cancel_mouse_event_info(bool p_source_mouse_relative = false); + + void _parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index = false); void _release_all_touch(); + void _cancel_all_touch(); + public: 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_physical_keycode, int p_unicode, bool p_pressed); + void process_key_event(int p_physical_keycode, int p_unicode, int p_key_label, bool p_pressed); }; #endif // ANDROID_INPUT_HANDLER_H diff --git a/platform/android/android_keys_utils.cpp b/platform/android/android_keys_utils.cpp index 0dc2d19cdd..f50437e82a 100644 --- a/platform/android/android_keys_utils.cpp +++ b/platform/android/android_keys_utils.cpp @@ -38,41 +38,3 @@ Key godot_code_from_android_code(unsigned int p_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 33e7929278..3a587dd680 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -165,13 +165,14 @@ static AndroidGodotCodePair android_godot_code_pairs[] = { { 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_EISU, Key::JIS_EISU }, // (212) JIS EISU key. { AKEYCODE_YEN, Key::YEN }, // (216) Japanese Yen key. + { AKEYCODE_KANA, Key::JIS_KANA }, // (218) JIS KANA key. { AKEYCODE_HELP, Key::HELP }, // (259) Help key. { AKEYCODE_REFRESH, Key::REFRESH }, // (285) Refresh key. { AKEYCODE_MAX, Key::UNKNOWN } }; 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/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 27cb84fb9d..5fc32132e3 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -44,7 +44,7 @@ void AudioDriverOpenSL::_buffer_callback( if (pause) { mix = false; } else { - mix = mutex.try_lock() == OK; + mix = mutex.try_lock(); } if (mix) { @@ -80,10 +80,6 @@ void AudioDriverOpenSL::_buffer_callbacks( ad->_buffer_callback(queueItf); } -const char *AudioDriverOpenSL::get_name() const { - return "Android"; -} - Error AudioDriverOpenSL::init() { SLresult res; SLEngineOption EngineOption[] = { @@ -204,7 +200,7 @@ void AudioDriverOpenSL::_record_buffer_callbacks(SLAndroidSimpleBufferQueueItf q ad->_record_buffer_callback(queueItf); } -Error AudioDriverOpenSL::capture_init_device() { +Error AudioDriverOpenSL::init_input_device() { SLDataLocator_IODevice loc_dev = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, @@ -271,15 +267,15 @@ Error AudioDriverOpenSL::capture_init_device() { return OK; } -Error AudioDriverOpenSL::capture_start() { +Error AudioDriverOpenSL::input_start() { if (OS::get_singleton()->request_permission("RECORD_AUDIO")) { - return capture_init_device(); + return init_input_device(); } return OK; } -Error AudioDriverOpenSL::capture_stop() { +Error AudioDriverOpenSL::input_stop() { SLuint32 state; SLresult res = (*recordItf)->GetRecordState(recordItf, &state); ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN); diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h index ae8c33fec0..6ea0f77def 100644 --- a/platform/android/audio_driver_opensl.h +++ b/platform/android/audio_driver_opensl.h @@ -84,23 +84,26 @@ class AudioDriverOpenSL : public AudioDriver { SLAndroidSimpleBufferQueueItf queueItf, void *pContext); - virtual Error capture_init_device(); + Error init_input_device(); public: - virtual const char *get_name() const; + virtual const char *get_name() const override { + return "Android"; + } - virtual Error init(); - virtual void start(); - virtual int get_mix_rate() const; - virtual SpeakerMode get_speaker_mode() const; - virtual void lock(); - virtual void unlock(); - virtual void finish(); + virtual Error init() override; + virtual void start() override; + virtual int get_mix_rate() const override; + virtual SpeakerMode get_speaker_mode() const override; - virtual void set_pause(bool p_pause); + virtual void lock() override; + virtual void unlock() override; + virtual void finish() override; - virtual Error capture_start(); - virtual Error capture_stop(); + virtual Error input_start() override; + virtual Error input_stop() override; + + void set_pause(bool p_pause); AudioDriverOpenSL(); }; diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 6b3bdb7fe6..af4ba1255b 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -212,7 +212,18 @@ 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 screen_scale = godot_io_java->get_scaled_density(); + + // Update the scale to avoid cropping. + Size2i screen_size = screen_get_size(p_screen); + if (screen_size != Size2i()) { + float width_scale = screen_size.width / (float)OS_Android::DEFAULT_WINDOW_WIDTH; + float height_scale = screen_size.height / (float)OS_Android::DEFAULT_WINDOW_HEIGHT; + screen_scale = MIN(screen_scale, MIN(width_scale, height_scale)); + } + + print_line("Selected screen scale: ", screen_scale); + return screen_scale; } float DisplayServerAndroid::screen_get_refresh_rate(int p_screen) const { @@ -645,6 +656,7 @@ void DisplayServerAndroid::_cursor_set_shape_helper(CursorShape p_shape, bool fo } void DisplayServerAndroid::cursor_set_shape(DisplayServer::CursorShape p_shape) { + ERR_FAIL_INDEX(p_shape, CURSOR_MAX); _cursor_set_shape_helper(p_shape); } @@ -653,6 +665,7 @@ DisplayServer::CursorShape DisplayServerAndroid::cursor_get_shape() const { } void DisplayServerAndroid::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { + ERR_FAIL_INDEX(p_shape, CURSOR_MAX); String cursor_path = p_cursor.is_valid() ? p_cursor->get_path() : ""; if (!cursor_path.is_empty()) { cursor_path = ProjectSettings::get_singleton()->globalize_path(cursor_path); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 587caf81bf..641258a26c 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -251,7 +251,8 @@ static const int EXPORT_FORMAT_AAB = 1; static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets"; static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets"; -static const int DEFAULT_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' +static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' +static const int VULKAN_MIN_SDK_VERSION = 24; static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' #ifndef ANDROID_ENABLED @@ -799,6 +800,12 @@ bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1; } +bool EditorExportPlatformAndroid::_uses_vulkan() { + String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile"); + bool uses_vulkan = (current_renderer == "forward_plus" || current_renderer == "mobile") && GLOBAL_GET("rendering/rendering_device/driver.android") == "vulkan"; + return uses_vulkan; +} + void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) { const char **aperms = android_perms; while (*aperms) { @@ -853,7 +860,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres } } - manifest_text += _get_xr_features_tag(p_preset); + manifest_text += _get_xr_features_tag(p_preset, _uses_vulkan()); 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")); @@ -899,10 +906,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p bool screen_support_large = p_preset->get("screen/support_large"); bool screen_support_xlarge = p_preset->get("screen/support_xlarge"); - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); - int hand_tracking_frequency_index = p_preset->get("xr_features/hand_tracking_frequency"); - bool backup_allowed = p_preset->get("user_data_backup/allow"); int app_category = p_preset->get("package/app_category"); bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall"); @@ -1027,6 +1030,10 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]); } + if (tname == "provider" && attrname == "authorities") { + string_table.write[attr_value] = get_package_name(package_name) + String(".fileprovider"); + } + if (tname == "supports-screens") { if (attrname == "smallScreens") { encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); @@ -1042,25 +1049,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } } - // 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") { - 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 == "value" && value == "xr_hand_tracking_version_value") { - string_table.write[attr_value] = "V2.0"; - } - } - iofs += 20; } @@ -1075,26 +1063,11 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p Vector<bool> feature_required_list; Vector<int> feature_versions; - if (xr_mode_index == XR_MODE_OPENXR) { - // Set degrees of freedom - feature_names.push_back("android.hardware.vr.headtracking"); + if (_uses_vulkan()) { + // Require vulkan hardware level 1 support + feature_names.push_back("android.hardware.vulkan.level"); feature_required_list.push_back(true); feature_versions.push_back(1); - - // Check for hand tracking - if (hand_tracking_index > XR_HAND_TRACKING_NONE) { - feature_names.push_back("oculus.software.handtracking"); - feature_required_list.push_back(hand_tracking_index == XR_HAND_TRACKING_REQUIRED); - feature_versions.push_back(-1); // no version attribute should be added. - } - - // Check for passthrough - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (passthrough_mode > XR_PASSTHROUGH_NONE) { - feature_names.push_back("com.oculus.feature.PASSTHROUGH"); - feature_required_list.push_back(passthrough_mode == XR_PASSTHROUGH_REQUIRED); - feature_versions.push_back(-1); - } } if (feature_names.size() > 0) { @@ -1716,6 +1689,7 @@ Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_enable void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const { r_features->push_back("etc2"); + r_features->push_back("astc"); Vector<ABI> abis = get_enabled_abis(p_preset); for (int i = 0; i < abis.size(); ++i) { @@ -1728,12 +1702,12 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "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)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_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)), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_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++) { @@ -2158,11 +2132,11 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_ 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_build/use_custom_build"); + const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); // Look for export templates (first official, and if defined custom templates). - if (!custom_build_enabled) { + if (!gradle_build_enabled) { String template_err; bool dvalid = false; bool rvalid = false; @@ -2269,7 +2243,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito valid = false; } - String target_sdk_version = p_preset->get("custom_build/target_sdk"); + String target_sdk_version = p_preset->get("gradle_build/target_sdk"); if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); } @@ -2293,7 +2267,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito 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"); + const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); // Validate the project configuration. bool apk_expansion = p_preset->get("apk_expansion/enable"); @@ -2322,11 +2296,11 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit err += etc_error; } - // Ensure that `Use Custom Build` is enabled if a plugin is selected. + // Ensure that `Use Gradle Build` is enabled if a plugin is selected. String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset)); - if (!enabled_plugins_names.is_empty() && !custom_build_enabled) { + if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { valid = false; - err += TTR("\"Use Custom Build\" must be enabled to use the plugins."); + err += TTR("\"Use Gradle Build\" must be enabled to use the plugins."); err += "\n"; } @@ -2334,6 +2308,12 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit int xr_mode_index = p_preset->get("xr_features/xr_mode"); int hand_tracking = p_preset->get("xr_features/hand_tracking"); int passthrough_mode = p_preset->get("xr_features/passthrough"); + if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { + valid = false; + err += TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); + err += "\n"; + } + if (xr_mode_index != XR_MODE_OPENXR) { if (hand_tracking > XR_HAND_TRACKING_NONE) { valid = false; @@ -2348,20 +2328,20 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit } } - if (int(p_preset->get("custom_build/export_format")) == EXPORT_FORMAT_AAB && - !custom_build_enabled) { + if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && + !gradle_build_enabled) { valid = false; - err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled."); + err += TTR("\"Export AAB\" is only valid when \"Use Gradle 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; + String min_sdk_str = p_preset->get("gradle_build/min_sdk"); + int min_sdk_int = VULKAN_MIN_SDK_VERSION; if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. - if (!custom_build_enabled) { + if (!gradle_build_enabled) { valid = false; - err += TTR("\"Min SDK\" can only be overridden when \"Use Custom Build\" is enabled."); + err += TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); err += "\n"; } if (!min_sdk_str.is_valid_int()) { @@ -2370,21 +2350,21 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit err += "\n"; } else { min_sdk_int = min_sdk_str.to_int(); - if (min_sdk_int < DEFAULT_MIN_SDK_VERSION) { + if (min_sdk_int < OPENGL_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 += vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION); err += "\n"; } } } // Check the target sdk version. - String target_sdk_str = p_preset->get("custom_build/target_sdk"); + String target_sdk_str = p_preset->get("gradle_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) { + if (!gradle_build_enabled) { valid = false; - err += TTR("\"Target SDK\" can only be overridden when \"Use Custom Build\" is enabled."); + err += TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); err += "\n"; } if (!target_sdk_str.is_valid_int()) { @@ -2407,6 +2387,18 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit err += "\n"; } + String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile"); + if (current_renderer == "forward_plus") { + // Warning only, so don't override `valid`. + err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer); + err += "\n"; + } + if (_uses_vulkan() && min_sdk_int < VULKAN_MIN_SDK_VERSION) { + // Warning only, so don't override `valid`. + err += vformat(TTR("\"Min SDK\" should be greater or equal to %d for the \"%s\" renderer."), VULKAN_MIN_SDK_VERSION, current_renderer); + err += "\n"; + } + r_error = err; return valid; } @@ -2492,12 +2484,12 @@ 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_build/export_format")); + int export_format = int(p_preset->get("gradle_build/export_format")); String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; String release_keystore = p_preset->get("keystore/release"); String release_username = p_preset->get("keystore/release_user"); String release_password = p_preset->get("keystore/release_password"); - String target_sdk_version = p_preset->get("custom_build/target_sdk"); + String target_sdk_version = p_preset->get("gradle_build/target_sdk"); if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); } @@ -2674,7 +2666,7 @@ String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformA } 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_build/export_format")); + int export_format = int(p_preset->get("gradle_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); } @@ -2687,7 +2679,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_build/use_custom_build")); + bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_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<ABI> enabled_abis = get_enabled_abis(p_preset); @@ -2697,7 +2689,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("- export path: " + p_path); print_verbose("- export format: " + itos(export_format)); print_verbose("- sign build: " + bool_to_string(should_sign)); - print_verbose("- custom build enabled: " + bool_to_string(use_custom_build)); + print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build)); print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion)); print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false)); print_verbose("- export filter: " + itos(p_preset->get_export_filter())); @@ -2737,14 +2729,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_UNCONFIGURED; } - if (use_custom_build) { - print_verbose("Starting custom build..."); + if (use_gradle_build) { + print_verbose("Starting gradle build..."); //test that installed build version is alright { print_verbose("Checking build version..."); Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); 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.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String version = f->get_line().strip_edges(); @@ -2814,11 +2806,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String package_name = get_package_name(p_preset->get("package/unique_name")); String version_code = itos(p_preset->get("version/code")); String version_name = p_preset->get("version/name"); - String min_sdk_version = p_preset->get("custom_build/min_sdk"); + String min_sdk_version = p_preset->get("gradle_build/min_sdk"); if (!min_sdk_version.is_valid_int()) { - min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION); + min_sdk_version = itos(VULKAN_MIN_SDK_VERSION); } - String target_sdk_version = p_preset->get("custom_build/target_sdk"); + String target_sdk_version = p_preset->get("gradle_build/target_sdk"); if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); } @@ -2944,7 +2936,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_CANT_CREATE; } - print_verbose("Successfully completed Android custom build."); + print_verbose("Successfully completed Android gradle build."); return OK; } // This is the start of the Legacy build system diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index bff769fcba..337a0228d0 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -74,7 +74,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { Vector<PluginConfigAndroid> plugins; String last_plugin_names; - uint64_t last_custom_build_time = 0; + uint64_t last_gradle_build_time = 0; SafeFlag plugins_changed; Mutex plugins_lock; Vector<Device> devices; @@ -172,6 +172,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Vector<ABI> get_enabled_abis(const Ref<EditorExportPreset> &p_preset); + static bool _uses_vulkan(); + 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); @@ -213,14 +215,14 @@ public: inline bool is_clean_build_required(Vector<PluginConfigAndroid> enabled_plugins) { String plugin_names = PluginConfigAndroid::get_plugins_names(enabled_plugins); - bool first_build = last_custom_build_time == 0; + bool first_build = last_gradle_build_time == 0; bool have_plugins_changed = false; if (!first_build) { have_plugins_changed = plugin_names != last_plugin_names; if (!have_plugins_changed) { for (int i = 0; i < enabled_plugins.size(); i++) { - if (enabled_plugins.get(i).last_updated > last_custom_build_time) { + if (enabled_plugins.get(i).last_updated > last_gradle_build_time) { have_plugins_changed = true; break; } @@ -228,7 +230,7 @@ public: } } - last_custom_build_time = OS::get_singleton()->get_unix_time(); + last_gradle_build_time = OS::get_singleton()->get_unix_time(); last_plugin_names = plugin_names; return have_plugins_changed || first_build; diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index e450f3edb3..b889d58199 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -166,7 +166,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // This method will only be called as an input to export_project_files. // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. -// This method will be called ONLY when custom build is enabled. +// This method will be called ONLY when gradle build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/"); @@ -254,13 +254,11 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_screen_sizes; } -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { +String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan) { String manifest_xr_features; int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OPENXR; if (uses_xr) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"true\" android:version=\"1\" />\n"; - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required if (hand_tracking_index == XR_HAND_TRACKING_OPTIONAL) { manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; @@ -275,27 +273,46 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"com.oculus.feature.PASSTHROUGH\" android:required=\"true\" />\n"; } } + + if (p_uses_vulkan) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"true\" android:version=\"1\" />\n"; + } return manifest_xr_features; } -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 _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr) { 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,android:resizeableActivity\" " + "tools:node=\"mergeOnlyAttributes\" " "android:excludeFromRecents=\"%s\" " "android:screenOrientation=\"%s\" " "android:resizeableActivity=\"%s\">\n", bool_to_string(p_preset->get("package/exclude_from_recents")), 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"; + + if (p_uses_xr) { + manifest_activity_text += " <intent-filter>\n" + " <action android:name=\"android.intent.action.MAIN\" />\n" + " <category android:name=\"android.intent.category.LAUNCHER\" />\n" + "\n" + " <!-- Enable access to OpenXR on Oculus mobile devices, no-op on other Android\n" + " platforms. -->\n" + " <category android:name=\"com.oculus.intent.category.VR\" />\n" + "\n" + " <!-- OpenXR category tag to indicate the activity starts in an immersive OpenXR mode. \n" + " See https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#android-runtime-category. -->\n" + " <category android:name=\"org.khronos.openxr.intent.category.IMMERSIVE_HMD\" />\n" + " </intent-filter>\n"; } else { - manifest_activity_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.vr.focusaware\" />\n"; + manifest_activity_text += " <intent-filter>\n" + " <action android:name=\"android.intent.action.MAIN\" />\n" + " <category android:name=\"android.intent.category.LAUNCHER\" />\n" + " </intent-filter>\n"; } + manifest_activity_text += " </activity>\n"; return manifest_activity_text; } @@ -316,9 +333,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ " android:hasFragileUserData=\"%s\"\n" " android:requestLegacyExternalStorage=\"%s\"\n" " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n" - " tools:ignore=\"GoogleAppIndexingWarning\">\n\n" - " <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", + " tools:ignore=\"GoogleAppIndexingWarning\">\n\n", bool_to_string(p_preset->get("user_data_backup/allow")), _get_app_category_label(app_category_index), bool_to_string(is_game), @@ -335,10 +350,8 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ 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"; } - manifest_application_text += _get_activity_tag(p_preset); + manifest_application_text += _get_activity_tag(p_preset, uses_xr); manifest_application_text += " </application>\n"; return manifest_application_text; } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 0fa857cb75..8a885a0d12 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -104,7 +104,7 @@ Error store_string_at_path(const String &p_path, const String &p_data); // This method will only be called as an input to export_project_files. // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. -// This method will be called ONLY when custom build is enabled. +// This method will be called ONLY when gradle build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); // Creates strings.xml files inside the gradle project for different locales. @@ -116,9 +116,9 @@ String _get_gles_tag(); String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset); -String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset); +String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_vulkan); -String _get_activity_tag(const Ref<EditorExportPreset> &p_preset); +String _get_activity_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses_xr); String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission); diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index 5df05580a5..1249f2219f 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -169,6 +169,10 @@ bool FileAccessAndroid::file_exists(const String &p_path) { return true; } +void FileAccessAndroid::close() { + _close(); +} + FileAccessAndroid::~FileAccessAndroid() { _close(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index 1d25a28d90..b8f45628e5 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -78,6 +78,8 @@ public: virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual void close() override; + ~FileAccessAndroid(); }; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index 7174d57344..ea8459d1ed 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -333,6 +333,12 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) { _file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J"); } +void FileAccessFilesystemJAndroid::close() { + if (is_open()) { + _close(); + } +} + FileAccessFilesystemJAndroid::FileAccessFilesystemJAndroid() { id = 0; } diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 7829ab7cf9..5e74d9de24 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -93,6 +93,8 @@ public: virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + virtual void close() override; + FileAccessFilesystemJAndroid(); ~FileAccessFilesystemJAndroid(); }; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 8c8608cbbb..ce4a2ecfe4 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -31,29 +31,6 @@ android:name="org.godotengine.editor.version" android:value="${godotEditorVersion}" /> - <!-- The following metadata values are replaced when Godot exports, modifying them here has no effect. --> - <!-- Do these changes in the export preset. Adding new ones is fine. --> - - <!-- XR hand tracking metadata --> - <!-- 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_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 - android:name="com.oculus.supportedDevices" - android:value="all" /> - <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" @@ -66,16 +43,9 @@ android:resizeableActivity="false" tools:ignore="UnusedAttribute" > - <!-- Focus awareness metadata is removed at export time if the xr mode is not VR. --> - <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> - <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> - - <!-- Enable access to OpenXR on Oculus mobile devices, no-op on other Android - platforms. --> - <category android:name="com.oculus.intent.category.VR" /> </intent-filter> </activity> diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 63b10e62b1..01b148aeef 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -50,7 +50,7 @@ dependencies { } else if (rootProject.findProject(":godot:lib")) { implementation project(":godot:lib") } else { - // Custom build mode. In this scenario this project is the only one around and the Godot + // Godot gradle 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']) @@ -153,7 +153,7 @@ android { debug { // Signing and zip-aligning are skipped for prebuilt builds, but - // performed for custom builds. + // performed for Godot gradle builds. zipAlignEnabled shouldZipAlign() if (shouldSign()) { signingConfig signingConfigs.debug @@ -165,7 +165,7 @@ android { dev { initWith debug // Signing and zip-aligning are skipped for prebuilt builds, but - // performed for custom builds. + // performed for Godot gradle builds. zipAlignEnabled shouldZipAlign() if (shouldSign()) { signingConfig signingConfigs.debug @@ -176,7 +176,7 @@ android { release { // Signing and zip-aligning are skipped for prebuilt builds, but - // performed for custom builds. + // performed for Godot gradle builds. zipAlignEnabled shouldZipAlign() if (shouldSign()) { signingConfig signingConfigs.release diff --git a/platform/android/java/app/gradle.properties b/platform/android/java/app/gradle.properties index 0ad8e611ca..d9f79b6818 100644 --- a/platform/android/java/app/gradle.properties +++ b/platform/android/java/app/gradle.properties @@ -1,5 +1,5 @@ -# Godot custom build Gradle settings. -# These properties apply when running custom build from the Godot editor. +# Godot gradle build settings. +# These properties apply when running a gradle build from the Godot editor. # NOTE: This should be kept in sync with 'godot/platform/android/java/gradle.properties' except # where otherwise specified. diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index ba53aefe7f..b4524a3f60 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -1,4 +1,4 @@ -// This is the root directory of the Godot custom build. +// This is the root directory of the Godot Android gradle build. pluginManagement { apply from: 'config.gradle' diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index a43e289b6b..1d2cc05715 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -35,7 +35,7 @@ import org.godotengine.godot.FullScreenGodotApp; import android.os.Bundle; /** - * Template activity for Godot Android custom builds. + * Template activity for Godot Android builds. * Feel free to extend and modify this class for your custom logic. */ public class GodotApp extends FullScreenGodotApp { diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 5a91e5ce32..cffe0a33d9 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -152,14 +152,14 @@ task copyReleaseAARToBin(type: Copy) { } /** - * Generate Godot custom build template by zipping the source files from the app directory, as well + * Generate Godot gradle build template by zipping the source files from the app directory, as well * 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. + * The zip file also includes some gradle tools to enable gradle builds from the Godot Editor. */ -task zipCustomBuild(type: Zip) { +task zipGradleBuild(type: Zip) { onlyIf { generateGodotTemplates.state.executed || generateDevTemplate.state.executed } doFirst { - logger.lifecycle("Generating Godot custom build template") + logger.lifecycle("Generating Godot gradle build template") } from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradlew', 'gradlew.bat', 'gradle/**'])) include '**/*' @@ -195,7 +195,7 @@ def templateBuildTasks() { && targetLibs.listFiles() != null && targetLibs.listFiles().length > 0) { String capitalizedTarget = target.capitalize() - // Copy the generated aar library files to the custom build directory. + // Copy the generated aar library files to the build directory. tasks += "copy" + capitalizedTarget + "AARToAppModule" // Copy the generated aar library files to the bin directory. tasks += "copy" + capitalizedTarget + "AARToBin" @@ -260,7 +260,7 @@ task generateGodotTemplates { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() dependsOn = templateBuildTasks() - finalizedBy 'zipCustomBuild' + finalizedBy 'zipGradleBuild' } /** @@ -273,7 +273,7 @@ task generateDevTemplate { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() dependsOn = templateBuildTasks() - finalizedBy 'zipCustomBuild' + finalizedBy 'zipGradleBuild' } task clean(type: Delete) { diff --git a/platform/android/java/editor/src/dev/res/values/strings.xml b/platform/android/java/editor/src/dev/res/values/strings.xml index 45fae3fd39..215f2c7d0a 100644 --- a/platform/android/java/editor/src/dev/res/values/strings.xml +++ b/platform/android/java/editor/src/dev/res/values/strings.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="godot_editor_name_string">Godot Editor 4.x (dev)</string> + <string name="godot_editor_name_string">Godot Editor 4 (dev)</string> </resources> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index f848089aa8..71385315ae 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -31,12 +31,11 @@ package org.godotengine.editor import android.Manifest +import android.app.ActivityManager +import android.content.Context 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.os.* import android.util.Log import android.widget.Toast import androidx.window.layout.WindowMetricsCalculator @@ -64,11 +63,18 @@ open class GodotEditor : FullScreenGodotApp() { private const val COMMAND_LINE_PARAMS = "command_line_params" + private const val EDITOR_ID = 777 private const val EDITOR_ARG = "--editor" private const val EDITOR_ARG_SHORT = "-e" + private const val EDITOR_PROCESS_NAME_SUFFIX = ":GodotEditor" + private const val GAME_ID = 667 + private const val GAME_PROCESS_NAME_SUFFIX = ":GodotGame" + + private const val PROJECT_MANAGER_ID = 555 private const val PROJECT_MANAGER_ARG = "--project-manager" private const val PROJECT_MANAGER_ARG_SHORT = "-p" + private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager" } private val commandLineParams = ArrayList<String>() @@ -102,9 +108,10 @@ open class GodotEditor : FullScreenGodotApp() { override fun getCommandLine() = commandLineParams - override fun onNewGodotInstanceRequested(args: Array<String>) { + override fun onNewGodotInstanceRequested(args: Array<String>): Int { // Parse the arguments to figure out which activity to start. var targetClass: Class<*> = GodotGame::class.java + var instanceId = GAME_ID // Whether we should launch the new godot instance in an adjacent window // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT @@ -115,12 +122,14 @@ open class GodotEditor : FullScreenGodotApp() { if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) { targetClass = GodotEditor::class.java launchAdjacent = false + instanceId = EDITOR_ID break } if (PROJECT_MANAGER_ARG == arg || PROJECT_MANAGER_ARG_SHORT == arg) { targetClass = GodotProjectManager::class.java launchAdjacent = false + instanceId = PROJECT_MANAGER_ID break } } @@ -139,6 +148,37 @@ open class GodotEditor : FullScreenGodotApp() { Log.d(TAG, "Starting $targetClass") startActivity(newInstance) } + return instanceId + } + + override fun onGodotForceQuit(godotInstanceId: Int): Boolean { + val processNameSuffix = when (godotInstanceId) { + GAME_ID -> { + GAME_PROCESS_NAME_SUFFIX + } + EDITOR_ID -> { + EDITOR_PROCESS_NAME_SUFFIX + } + PROJECT_MANAGER_ID -> { + PROJECT_MANAGER_PROCESS_NAME_SUFFIX + } + else -> "" + } + if (processNameSuffix.isBlank()) { + return false + } + + val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val runningProcesses = activityManager.runningAppProcesses + for (runningProcess in runningProcesses) { + if (runningProcess.processName.endsWith(processNameSuffix)) { + Log.v(TAG, "Killing Godot process ${runningProcess.processName}") + Process.killProcess(runningProcess.pid) + return true + } + } + + return false } // Get the screen's density scale diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml index 03fb6184d2..98bfe40179 100644 --- a/platform/android/java/editor/src/main/res/values/dimens.xml +++ b/platform/android/java/editor/src/main/res/values/dimens.xml @@ -1,5 +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> + <dimen name="editor_default_window_width">1024dp</dimen> </resources> diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index 837a5d62e1..216d02d9c7 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="godot_editor_name_string">Godot Editor 4.x</string> + <string name="godot_editor_name_string">Godot Editor 4</string> <string name="denied_storage_permission_error_msg">Missing storage access permission!</string> </resources> diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties index 5cd94e85d9..39a0dcda16 100644 --- a/platform/android/java/gradle.properties +++ b/platform/android/java/gradle.properties @@ -24,5 +24,5 @@ org.gradle.jvmargs=-Xmx4536m org.gradle.warning.mode=all # Disable resource optimizations for template release build. -# NOTE: This is turned on for custom build in order to improve the release build. +# NOTE: This is turned on for Godot Editor's gradle builds in order to improve the release build. android.enableResourceOptimizations=false diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 1f77e2fc34..f03a1dd47a 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -20,6 +20,16 @@ android:exported="false" /> + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/godot_provider_paths" /> + </provider> + </application> </manifest> diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml index f5a4ab1071..03752e092e 100644 --- a/platform/android/java/lib/res/values/strings.xml +++ b/platform/android/java/lib/res/values/strings.xml @@ -14,6 +14,7 @@ <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> + <string name="error_missing_vulkan_requirements_message">Warning - this device does not meet the requirements for Vulkan support</string> <!-- APK Expansion Strings --> @@ -48,7 +49,7 @@ <string name="state_failed_unlicensed">Download failed because you may not have purchased this app</string> <string name="state_failed_fetching_url">Download failed because the resources could not be found</string> <string name="state_failed_sdcard_full">Download failed because the external storage is full</string> - <string name="state_failed_cancelled">Download cancelled</string> + <string name="state_failed_cancelled">Download canceled</string> <string name="state_failed">Download failed</string> <string name="kilobytes_per_second">%1$s KB/s</string> diff --git a/platform/android/java/lib/res/xml/godot_provider_paths.xml b/platform/android/java/lib/res/xml/godot_provider_paths.xml new file mode 100644 index 0000000000..1255f576bf --- /dev/null +++ b/platform/android/java/lib/res/xml/godot_provider_paths.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + + <external-path + name="public" + path="." /> + + <external-files-path + name="app" + path="." /> +</paths> 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 863e67f1e9..677c9d8f13 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -74,28 +74,36 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God public void onDestroy() { Log.v(TAG, "Destroying Godot app..."); super.onDestroy(); - onGodotForceQuit(godotFragment); + terminateGodotInstance(godotFragment); } @Override public final void onGodotForceQuit(Godot instance) { + runOnUiThread(() -> { + terminateGodotInstance(instance); + }); + } + + private void terminateGodotInstance(Godot instance) { if (instance == godotFragment) { Log.v(TAG, "Force quitting Godot instance"); - ProcessPhoenix.forceQuit(this); + ProcessPhoenix.forceQuit(FullScreenGodotApp.this); } } @Override public final void onGodotRestartRequested(Godot instance) { - if (instance == godotFragment) { - // It's very hard to properly de-initialize Godot on Android to restart the game - // from scratch. Therefore, we need to kill the whole app process and relaunch it. - // - // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including - // releasing and reloading native libs or resetting their state somehow and clearing statics). - Log.v(TAG, "Restarting Godot instance..."); - ProcessPhoenix.triggerRebirth(this); - } + runOnUiThread(() -> { + if (instance == godotFragment) { + // It's very hard to properly de-initialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing static data). + Log.v(TAG, "Restarting Godot instance..."); + ProcessPhoenix.triggerRebirth(FullScreenGodotApp.this); + } + }); } @Override 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 905db13c85..a03da7292b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -83,6 +83,7 @@ import android.widget.Button; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.CallSuper; import androidx.annotation.Keep; @@ -258,13 +259,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC */ @Keep private boolean onVideoInit() { - final Activity activity = getActivity(); + final Activity activity = requireActivity(); containerLayout = new FrameLayout(activity); containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // GodotEditText layout GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + editText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, (int)getResources().getDimension(R.dimen.text_edit_height))); // ...add to FrameLayout containerLayout.addView(editText); @@ -275,11 +276,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return false; } - final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method"); - if (renderer.equals("gl_compatibility")) { - mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl); - } else { + if (usesVulkan()) { + if (!meetsVulkanRequirements(activity.getPackageManager())) { + Log.w(TAG, "Missing requirements for vulkan support!"); + } mRenderView = new GodotVulkanRenderView(activity, this); + } else { + // Fallback to openGl + mRenderView = new GodotGLRenderView(activity, this, xrMode, use_debug_opengl); } View view = mRenderView.getView(); @@ -317,6 +321,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return true; } + /** + * Returns true if `Vulkan` is used for rendering. + */ + private boolean usesVulkan() { + final String renderer = GodotLib.getGlobal("rendering/renderer/rendering_method"); + final String renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver"); + return ("forward_plus".equals(renderer) || "mobile".equals(renderer)) && "vulkan".equals(renderingDevice); + } + + /** + * Returns true if the device meets the base requirements for Vulkan support, false otherwise. + */ + private boolean meetsVulkanRequirements(@Nullable PackageManager packageManager) { + if (packageManager == null) { + return false; + } + + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1); + } + public void setKeepScreenOn(final boolean p_enabled) { runOnUiThread(() -> { if (p_enabled) { @@ -348,11 +372,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public void restart() { - runOnUiThread(() -> { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } - }); + if (godotHost != null) { + godotHost.onGodotRestartRequested(this); + } } public void alert(final String message, final String title) { @@ -889,11 +911,20 @@ 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. - runOnUiThread(() -> { - if (godotHost != null) { - godotHost.onGodotForceQuit(this); - } - }); + forceQuit(0); + } + + @Keep + private boolean forceQuit(int instanceId) { + if (godotHost == null) { + return false; + } + if (instanceId == 0) { + godotHost.onGodotForceQuit(this); + return true; + } else { + return godotHost.onGodotForceQuit(instanceId); + } } private boolean obbIsCorrupted(String f, String main_pack_md5) { @@ -1052,11 +1083,10 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } @Keep - private void createNewGodotInstance(String[] args) { - runOnUiThread(() -> { - if (godotHost != null) { - godotHost.onNewGodotInstanceRequested(args); - } - }); + private int createNewGodotInstance(String[] args) { + if (godotHost != null) { + return godotHost.onNewGodotInstanceRequested(args); + } + return 0; } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 256d04e3a5..7700b9b628 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -55,21 +55,35 @@ public interface GodotHost { default void onGodotMainLoopStarted() {} /** - * Invoked on the UI thread as the last step of the Godot instance clean up phase. + * Invoked on the render thread to terminate the given Godot instance. */ default void onGodotForceQuit(Godot instance) {} /** - * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host + * Invoked on the render thread to terminate the Godot instance with the given id. + * @param godotInstanceId id of the Godot instance to terminate. See {@code onNewGodotInstanceRequested} + * + * @return true if successful, false otherwise. + */ + default boolean onGodotForceQuit(int godotInstanceId) { + return false; + } + + /** + * Invoked on the render 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 + * Invoked on the render 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. + * + * @return the id of the new instance. See {@code onGodotForceQuit} */ - default void onNewGodotInstanceRequested(String[] args) {} + default int onNewGodotInstanceRequested(String[] args) { + return 0; + } } 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 aca0c4381b..edcd9c4d1f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -49,6 +49,9 @@ import android.view.Display; import android.view.DisplayCutout; import android.view.WindowInsets; +import androidx.core.content.FileProvider; + +import java.io.File; import java.util.List; import java.util.Locale; @@ -84,29 +87,42 @@ public class GodotIO { // MISCELLANEOUS OS IO ///////////////////////// - public int openURI(String p_uri) { + public int openURI(String uriString) { try { - String path = p_uri; - String type = ""; - if (path.startsWith("/")) { - //absolute path to filesystem, prepend file:// - path = "file://" + path; - if (p_uri.endsWith(".png") || p_uri.endsWith(".jpg") || p_uri.endsWith(".gif") || p_uri.endsWith(".webp")) { - type = "image/*"; + Uri dataUri; + String dataType = ""; + boolean grantReadUriPermission = false; + + if (uriString.startsWith("/") || uriString.startsWith("file://")) { + String filePath = uriString; + // File uris needs to be provided via the FileProvider + grantReadUriPermission = true; + if (filePath.startsWith("file://")) { + filePath = filePath.replace("file://", ""); } + + File targetFile = new File(filePath); + dataUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", targetFile); + dataType = activity.getContentResolver().getType(dataUri); + } else { + dataUri = Uri.parse(uriString); } Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - if (!type.equals("")) { - intent.setDataAndType(Uri.parse(path), type); + if (TextUtils.isEmpty(dataType)) { + intent.setData(dataUri); } else { - intent.setData(Uri.parse(path)); + intent.setDataAndType(dataUri, dataType); + } + if (grantReadUriPermission) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } activity.startActivity(intent); return 0; } catch (ActivityNotFoundException e) { + Log.e(TAG, "Unable to open uri " + uriString, e); return 1; } } @@ -150,7 +166,6 @@ public class GodotIO { } else { selectedScaledDensity = 0.75f; } - Log.d(TAG, "Selected scaled density: " + selectedScaledDensity); return selectedScaledDensity; } 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 31b512d2dd..75a01dc787 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -148,7 +148,7 @@ public class GodotLib { /** * Forward regular key events. */ - public static native void key(int p_keycode, int p_physical_keycode, int p_unicode, boolean p_pressed); + public static native void key(int p_physical_keycode, int p_unicode, int p_key_label, boolean p_pressed); /** * Forward game device's key events. 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 f2bd3e28e6..cedbbfb7c3 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 @@ -129,12 +129,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } } else { // 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); + final int key_label = event.getDisplayLabel(); + GodotLib.key(physical_keycode, unicode, key_label, false); }; return true; @@ -166,10 +164,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { GodotLib.joybutton(godotJoyId, button, true); } } else { - final int keycode = event.getUnicodeChar(0); final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); - GodotLib.key(keycode, physical_keycode, unicode, true); + final int key_label = event.getDisplayLabel(); + GodotLib.key(physical_keycode, unicode, key_label, true); } 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 fdfe20f32b..7b628e25ed 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, true); - GodotLib.key(0, KeyEvent.KEYCODE_DEL, 0, false); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true); + GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false); if (mHasSelection) { mHasSelection = false; @@ -110,13 +110,13 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene newChars[i - start] = pCharSequence.charAt(i); } for (int i = 0; i < count; ++i) { - int key = newChars[i]; - if ((key == '\n') && !(mEdit.getKeyboardType() == GodotEditText.VirtualKeyboardType.KEYBOARD_TYPE_MULTILINE)) { + final int character = newChars[i]; + if ((character == '\n') && !(mEdit.getKeyboardType() == GodotEditText.VirtualKeyboardType.KEYBOARD_TYPE_MULTILINE)) { // Return keys are handled through action events continue; } - GodotLib.key(key, 0, key, true); - GodotLib.key(key, 0, key, false); + GodotLib.key(0, character, 0, true); + GodotLib.key(0, character, 0, false); } } @@ -126,17 +126,17 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene final String characters = pKeyEvent.getCharacters(); for (int i = 0; i < characters.length(); i++) { - final int ch = characters.codePointAt(i); - GodotLib.key(ch, 0, ch, true); - GodotLib.key(ch, 0, ch, false); + final int character = characters.codePointAt(i); + GodotLib.key(0, character, 0, true); + GodotLib.key(0, character, 0, false); } } if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed mRenderView.queueOnRenderThread(() -> { - GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, true); - GodotLib.key(0, KeyEvent.KEYCODE_ENTER, 0, false); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true); + GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false); }); mRenderView.getView().requestFocus(); return true; 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 index 9649b0aecc..b9b7ebac6e 100644 --- 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 @@ -64,7 +64,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 override fun dirOpen(path: String): Int { - val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID + val assetsPath = getAssetsPath(path) try { val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID // Empty directories don't get added to the 'assets' directory, so @@ -99,7 +99,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. } override fun fileExists(path: String): Boolean { - val assetsPath = getAssetsPath(path) ?: return false + val assetsPath = getAssetsPath(path) try { val files = assetManager.list(assetsPath) ?: return false // Empty directories don't get added to the 'assets' directory, so diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java index 2cc37b627a..3ee3478fcb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -120,7 +120,7 @@ public final class ProcessPhoenix extends Activity { /** * 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 + * This can be used to avoid initialization 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 diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index e6cdd7932a..e7abe580f1 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -382,11 +382,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_physical_keycode, jint p_unicode, jboolean p_pressed) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed) { if (step.get() <= 0) { return; } - input_handler->process_key_event(p_keycode, p_physical_keycode, p_unicode, p_pressed); + input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_accelerometer(JNIEnv *env, jclass clazz, jfloat x, jfloat y, jfloat z) { @@ -488,7 +488,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { String permission = jstring_to_string(p_permission, env); if (permission == "android.permission.RECORD_AUDIO" && p_result) { - AudioDriver::get_singleton()->capture_start(); + AudioDriver::get_singleton()->input_start(); } if (os_android->get_main_loop()) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 3f07a8cfe1..0020ddffd2 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -49,7 +49,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jboolean p_double_tap); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_keycode, jint p_physical_keycode, jint p_unicode, jboolean p_pressed); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y); diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 03548d11f6..9d9d087896 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -60,7 +60,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", "()Z"); _restart = p_env->GetMethodID(godot_class, "restart", "()V"); - _finish = p_env->GetMethodID(godot_class, "forceQuit", "()V"); + _finish = p_env->GetMethodID(godot_class, "forceQuit", "(I)Z"); _set_keep_screen_on = p_env->GetMethodID(godot_class, "setKeepScreenOn", "(Z)V"); _alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V"); _get_GLES_version_code = p_env->GetMethodID(godot_class, "getGLESVersionCode", "()I"); @@ -77,7 +77,7 @@ 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"); + _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); // get some Activity method pointers... @@ -179,14 +179,15 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) { } } -void GodotJavaWrapper::force_quit(JNIEnv *p_env) { +bool GodotJavaWrapper::force_quit(JNIEnv *p_env, int p_instance_id) { if (_finish) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_NULL(p_env); - p_env->CallVoidMethod(godot_instance, _finish); + ERR_FAIL_NULL_V(p_env, false); + return p_env->CallBooleanMethod(godot_instance, _finish, p_instance_id); } + return false; } void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) { @@ -345,14 +346,16 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) { } } -void GodotJavaWrapper::create_new_godot_instance(List<String> args) { +int GodotJavaWrapper::create_new_godot_instance(List<String> args) { if (_create_new_godot_instance) { JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL(env); + ERR_FAIL_NULL_V(env, 0); jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); for (int i = 0; i < args.size(); i++) { env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data())); } - env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs); + return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs); + } else { + return 0; } } diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 5dad2a3eb9..1bd79584d8 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -85,7 +85,7 @@ public: 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); - void force_quit(JNIEnv *p_env = nullptr); + bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0); void set_keep_screen_on(bool p_enabled); void alert(const String &p_message, const String &p_title); int get_gles_version_code(); @@ -103,7 +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); + int create_new_godot_instance(List<String> args); }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 942bf0a904..725fea8d54 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -695,8 +695,8 @@ bool OS_Android::_check_internal_feature_support(const String &p_feature) { } OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion) { - display_size.width = 800; - display_size.height = 600; + display_size.width = DEFAULT_WINDOW_WIDTH; + display_size.height = DEFAULT_WINDOW_HEIGHT; use_apk_expansion = p_use_apk_expansion; @@ -739,9 +739,19 @@ Error OS_Android::create_process(const String &p_path, const List<String> &p_arg } Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { - godot_java->create_new_godot_instance(p_arguments); + int instance_id = godot_java->create_new_godot_instance(p_arguments); + if (r_child_id) { + *r_child_id = instance_id; + } return OK; } +Error OS_Android::kill(const ProcessID &p_pid) { + if (godot_java->force_quit(nullptr, p_pid)) { + return OK; + } + return OS_Unix::kill(p_pid); +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 68b6fefe33..53910b1498 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -86,6 +86,8 @@ private: public: static const char *ANDROID_EXEC_PATH; + static const int DEFAULT_WINDOW_WIDTH = 800; + static const int DEFAULT_WINDOW_HEIGHT = 600; virtual void initialize_core() override; virtual void initialize() override; @@ -156,6 +158,7 @@ public: 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 Error kill(const ProcessID &p_pid) 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); |