diff options
Diffstat (limited to 'platform/android')
23 files changed, 733 insertions, 241 deletions
diff --git a/platform/android/detect.py b/platform/android/detect.py index 6da1e5f3d6..0accacb679 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -115,7 +115,8 @@ def configure(env): if 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" + "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" @@ -136,7 +137,8 @@ def configure(env): 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" + "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" @@ -164,7 +166,7 @@ def configure(env): elif env["target"] == "debug": env.Append(LINKFLAGS=["-O0"]) env.Append(CCFLAGS=["-O0", "-g", "-fno-limit-debug-info"]) - env.Append(CPPDEFINES=["_DEBUG", "DEBUG_ENABLED", "DEBUG_MEMORY_ENABLED"]) + env.Append(CPPDEFINES=["_DEBUG", "DEBUG_ENABLED"]) env.Append(CPPFLAGS=["-UNDEBUG"]) # Compiler configuration @@ -231,7 +233,10 @@ def configure(env): env.Append(CPPDEFINES=[("__ANDROID_API__", str(get_platform(env["ndk_platform"])))]) env.Append( - CCFLAGS="-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() + CCFLAGS=( + "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden" + " -fno-strict-aliasing".split() + ) ) env.Append(CPPDEFINES=["NO_STATVFS", "GLES_ENABLED"]) diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 7193519a52..3aa2fb5451 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -36,9 +36,6 @@ #include "java_godot_wrapper.h" #include "os_android.h" -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" #include "platform/android/vulkan/vulkan_context_android.h" @@ -155,12 +152,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, 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, bool p_multiline, 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); if (godot_io_java->has_vk()) { - godot_io_java->show_vk(p_existing_text, p_max_length, p_cursor_start, p_cursor_end); + godot_io_java->show_vk(p_existing_text, p_multiline, p_max_length, p_cursor_start, p_cursor_end); } else { ERR_PRINT("Virtual keyboard not available"); } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 4cae52fa76..5cdc69ee83 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -113,7 +113,7 @@ public: virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); + 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); virtual void virtual_keyboard_hide(); virtual int virtual_keyboard_get_height() const; diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index ed85256695..5e6cc3e4e2 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -29,6 +29,7 @@ /*************************************************************************/ #include "export.h" +#include "gradle_export_util.h" #include "core/io/image_loader.h" #include "core/io/marshalls.h" @@ -43,6 +44,7 @@ #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "main/splash.gen.h" #include "platform/android/export/gradle_export_util.h" #include "platform/android/logo.gen.h" #include "platform/android/plugin/godot_plugin_config.h" @@ -199,6 +201,9 @@ static const char *android_perms[] = { nullptr }; +static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable/splash.png"; +static const char *SPLASH_BG_COLOR_PATH = "res/drawable/splash_bg_color.png"; + struct LauncherIcon { const char *export_path; int dimensions; @@ -452,7 +457,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String name; bool first = true; for (int i = 0; i < basename.length(); i++) { - CharType c = basename[i]; + char32_t c = basename[i]; if (c >= '0' && c <= '9' && first) { continue; } @@ -483,7 +488,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { int segments = 0; bool first = true; for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (first && c == '.') { if (r_error) { *r_error = TTR("Package segments must be of non-zero length."); @@ -722,7 +727,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error 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; String dst_path = p_path.replace_first("res://", "assets/"); @@ -730,7 +735,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error ignore_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) { return OK; } @@ -767,6 +772,30 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } } + void _write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) { + String manifest_text = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " xmlns:tools=\"http://schemas.android.com/tools\">\n"; + + manifest_text += _get_screen_sizes_tag(p_preset); + manifest_text += _get_gles_tag(); + + Vector<String> perms; + _get_permissions(p_preset, p_give_internet, perms); + for (int i = 0; i < perms.size(); i++) { + manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", perms.get(i)); + } + + manifest_text += _get_xr_features_tag(p_preset); + manifest_text += _get_instrumentation_tag(p_preset); + String plugins_names = get_plugins_names(get_enabled_plugins(p_preset)); + manifest_text += _get_application_tag(p_preset, plugins_names); + manifest_text += "</manifest>\n"; + String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); + store_string_at_path(manifest_path, manifest_text); + } + void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet) { // Leaving the unused types commented because looking these constants up // again later would be annoying @@ -848,7 +877,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { if (string_flags & UTF8_FLAG) { } else { uint32_t len = decode_uint16(&p_manifest[string_at]); - Vector<CharType> ucstring; + Vector<char32_t> ucstring; ucstring.resize(len + 1); for (uint32_t j = 0; j < len; j++) { uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]); @@ -1309,7 +1338,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } else { String str; for (uint32_t i = 0; i < len; i++) { - CharType c = decode_uint16(&p_bytes[offset + i * 2]); + char32_t c = decode_uint16(&p_bytes[offset + i * 2]); if (c == 0) { break; } @@ -1408,23 +1437,143 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { //printf("end\n"); } - void _process_launcher_icons(const String &p_processing_file_name, const Ref<Image> &p_source_image, const LauncherIcon p_icon, Vector<uint8_t> &p_data) { - if (p_processing_file_name == p_icon.export_path) { - Ref<Image> working_image = p_source_image; + void _load_image_data(const Ref<Image> &p_splash_image, Vector<uint8_t> &p_data) { + Vector<uint8_t> png_buffer; + Error err = PNGDriverCommon::image_to_png(p_splash_image, png_buffer); + if (err == OK) { + p_data.resize(png_buffer.size()); + memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size()); + } else { + String err_str = String("Failed to convert splash image to png."); + WARN_PRINT(err_str.utf8().get_data()); + } + } + + void _process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) { + Ref<Image> working_image = p_source_image; - if (p_source_image->get_width() != p_icon.dimensions || p_source_image->get_height() != p_icon.dimensions) { - working_image = p_source_image->duplicate(); - working_image->resize(p_icon.dimensions, p_icon.dimensions, Image::Interpolation::INTERPOLATE_LANCZOS); + if (p_source_image->get_width() != dimension || p_source_image->get_height() != dimension) { + working_image = p_source_image->duplicate(); + working_image->resize(dimension, dimension, Image::Interpolation::INTERPOLATE_LANCZOS); + } + + Vector<uint8_t> png_buffer; + Error err = PNGDriverCommon::image_to_png(working_image, png_buffer); + if (err == OK) { + p_data.resize(png_buffer.size()); + memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size()); + } else { + String err_str = String("Failed to convert resized icon (") + p_file_name + ") to png."; + WARN_PRINT(err_str.utf8().get_data()); + } + } + + void load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) { + // TODO: Figure out how to handle remaining boot splash parameters (e.g: fullsize, filter) + String project_splash_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + + if (!project_splash_path.empty()) { + splash_image.instance(); + const Error err = ImageLoader::load_image(project_splash_path, splash_image); + if (err) { + splash_image.unref(); } + } - Vector<uint8_t> png_buffer; - Error err = PNGDriverCommon::image_to_png(working_image, png_buffer); - if (err == OK) { - p_data.resize(png_buffer.size()); - memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size()); - } else { - String err_str = String("Failed to convert resized icon (") + p_processing_file_name + ") to png."; - WARN_PRINT(err_str.utf8().get_data()); + if (splash_image.is_null()) { + // Use the default + splash_image = Ref<Image>(memnew(Image(boot_splash_png))); + } + + // Setup the splash bg color + bool bg_color_valid; + Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid); + if (!bg_color_valid) { + bg_color = boot_splash_bg_color; + } + + splash_bg_color_image.instance(); + splash_bg_color_image->create(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); + splash_bg_color_image->fill(bg_color); + } + + void 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"); + + icon.instance(); + foreground.instance(); + background.instance(); + + // Regular icon: user selection -> project icon -> default. + String path = static_cast<String>(p_preset->get(launcher_icon_option)).strip_edges(); + if (path.empty() || ImageLoader::load_image(path, icon) != OK) { + ImageLoader::load_image(project_icon_path, icon); + } + + // Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default). + path = static_cast<String>(p_preset->get(launcher_adaptive_icon_foreground_option)).strip_edges(); + if (path.empty() || ImageLoader::load_image(path, foreground) != OK) { + foreground = icon; + } + + // Adaptive background: user selection -> default. + path = static_cast<String>(p_preset->get(launcher_adaptive_icon_background_option)).strip_edges(); + if (!path.empty()) { + ImageLoader::load_image(path, background); + } + } + + void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) { + store_image(launcher_icon.export_path, data); + } + + void store_image(const String &export_path, const Vector<uint8_t> &data) { + String img_path = export_path.insert(0, "res://android/build/"); + store_file_at_path(img_path, data); + } + + void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, + const Ref<Image> &splash_image, + const Ref<Image> &splash_bg_color_image, + const Ref<Image> &main_image, + const Ref<Image> &foreground, + const Ref<Image> &background) { + // Store the splash image + if (splash_image.is_valid() && !splash_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_image, data); + store_image(SPLASH_IMAGE_EXPORT_PATH, data); + } + + // Store the splash bg color image + if (splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_bg_color_image, data); + store_image(SPLASH_BG_COLOR_PATH, data); + } + + // Prepare images to be resized for the icons. If some image ends up being uninitialized, + // the default image from the export template will be used. + + for (int i = 0; i < icon_densities_count; ++i) { + if (main_image.is_valid() && !main_image->empty()) { + Vector<uint8_t> data; + _process_launcher_icons(launcher_icons[i].export_path, main_image, launcher_icons[i].dimensions, data); + store_image(launcher_icons[i], data); + } + + if (foreground.is_valid() && !foreground->empty()) { + Vector<uint8_t> data; + _process_launcher_icons(launcher_adaptive_icon_foregrounds[i].export_path, foreground, + launcher_adaptive_icon_foregrounds[i].dimensions, data); + store_image(launcher_adaptive_icon_foregrounds[i], data); + } + + if (background.is_valid() && !background->empty()) { + Vector<uint8_t> data; + _process_launcher_icons(launcher_adaptive_icon_backgrounds[i].export_path, background, + launcher_adaptive_icon_backgrounds[i].dimensions, data); + store_image(launcher_adaptive_icon_backgrounds[i], data); } } } @@ -1442,7 +1591,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } public: - typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total); + 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 { @@ -1471,6 +1620,7 @@ public: 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"), 0)); Vector<PluginConfig> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { @@ -1891,6 +2041,13 @@ public: } } + if (int(p_preset->get("custom_template/export_format")) == 1 && /*AAB*/ + !bool(p_preset->get("custom_template/use_custom_build"))) { + valid = false; + err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled."); + err += "\n"; + } + r_error = err; return valid; } @@ -1898,6 +2055,7 @@ public: virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { List<String> list; list.push_back("apk"); + list.push_back("aab"); return list; } @@ -1924,7 +2082,21 @@ public: return have_plugins_changed || first_build; } - Error get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { + String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + 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); + return fullpath; + } + + Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + String fullpath = get_apk_expansion_fullpath(p_preset, p_path); + Error err = save_pack(p_preset, fullpath); + return err; + } + + void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { String cmdline = p_preset->get("command_line/extra_args"); Vector<String> command_line_strings = cmdline.strip_edges().split(" "); for (int i = 0; i < command_line_strings.size(); i++) { @@ -1938,17 +2110,8 @@ public: bool apk_expansion = p_preset->get("apk_expansion/enable"); if (apk_expansion) { - 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 = get_apk_expansion_fullpath(p_preset, p_path); String apk_expansion_public_key = p_preset->get("apk_expansion/public_key"); - Error err = save_pack(p_preset, fullpath); - - if (err != OK) { - EditorNode::add_io_error("Could not write expansion package file: " + apk_file_name); - return err; - } command_line_strings.push_back("--use_apk_expansion"); command_line_strings.push_back("--apk_expansion_md5"); @@ -1994,6 +2157,92 @@ public: copymem(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length); } } + } + + Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, String apk_path, EditorProgress ep) { + 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 jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); + if (!FileAccess::exists(jarsigner)) { + EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); + return OK; + } + + String keystore; + String password; + String user; + if (p_debug) { + keystore = p_preset->get("keystore/debug"); + password = p_preset->get("keystore/debug_password"); + user = p_preset->get("keystore/debug_user"); + + if (keystore.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"); + } + + if (ep.step("Signing debug APK...", 103)) { + return ERR_SKIP; + } + + } else { + keystore = release_keystore; + password = release_password; + user = release_username; + + if (ep.step("Signing release APK...", 103)) { + return ERR_SKIP; + } + } + + if (!FileAccess::exists(keystore)) { + EditorNode::add_io_error("Could not find keystore, unable to export."); + return ERR_FILE_CANT_OPEN; + } + + List<String> args; + args.push_back("-digestalg"); + args.push_back("SHA-256"); + args.push_back("-sigalg"); + args.push_back("SHA256withRSA"); + String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); + if (tsa_url != "") { + args.push_back("-tsa"); + args.push_back(tsa_url); + } + args.push_back("-verbose"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back("-storepass"); + args.push_back(password); + args.push_back(apk_path); + args.push_back(user); + int retval; + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); + return ERR_CANT_CREATE; + } + + if (ep.step("Verifying APK...", 104)) { + return ERR_SKIP; + } + + args.clear(); + args.push_back("-verify"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back(apk_path); + args.push_back("-verbose"); + + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); + return ERR_CANT_CREATE; + } return OK; } @@ -2001,41 +2250,102 @@ public: ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); String src_apk; + Error err; EditorProgress ep("export", "Exporting for Android", 105, true); bool use_custom_build = bool(p_preset->get("custom_template/use_custom_build")); + int export_format = int(p_preset->get("custom_template/export_format")); + bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); + bool _signed = p_preset->get("package/signed"); + bool apk_expansion = p_preset->get("apk_expansion/enable"); + Vector<String> enabled_abis = get_enabled_abis(p_preset); + + Ref<Image> splash_image; + Ref<Image> splash_bg_color_image; + load_splash_refs(splash_image, splash_bg_color_image); + + Ref<Image> main_image; + Ref<Image> foreground; + Ref<Image> background; + + load_icon_refs(p_preset, main_image, foreground, background); + + Vector<uint8_t> command_line_flags; + // Write command line flags into the command_line_flags variable. + get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); + + if (export_format == 1) { + if (!p_path.ends_with(".aab")) { + EditorNode::get_singleton()->show_warning(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.")); + return ERR_UNCONFIGURED; + } + } + if (export_format == 0 && !p_path.ends_with(".apk")) { + EditorNode::get_singleton()->show_warning( + TTR("Invalid filename! Android APK requires the *.apk extension.")); + return ERR_UNCONFIGURED; + } + if (export_format > 1 || export_format < 0) { + EditorNode::add_io_error("Unsupported export format!\n"); + return ERR_UNCONFIGURED; //TODO: is this the right error? + } if (use_custom_build) { - //re-generate build.gradle and AndroidManifest.xml - { //test that installed build version is alright - 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.")); - return ERR_UNCONFIGURED; - } - String version = f->get_line().strip_edges(); - 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)); - return ERR_UNCONFIGURED; - } + //test that installed build version is alright + 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.")); + return ERR_UNCONFIGURED; + } + String version = f->get_line().strip_edges(); + 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)); + return ERR_UNCONFIGURED; } + String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); + ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); // TODO: should we use "package/name" or "application/config/name"? String project_name = get_project_name(p_preset->get("package/name")); - // instead of calling _fix_resources - Error err = _create_project_name_strings_files(p_preset, project_name); + err = _create_project_name_strings_files(p_preset, project_name); //project name localization. if (err != OK) { EditorNode::add_io_error("Unable to overwrite res://android/build/res/*.xml files with project name"); } - //build project if custom build is enabled - String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); - - ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); + // Copies the project icon files into the appropriate Gradle project directory. + _copy_icons_to_gradle_project(p_preset, splash_image, splash_bg_color_image, main_image, foreground, background); + // Write an AndroidManifest.xml file into the Gradle project directory. + _write_tmp_manifest(p_preset, p_give_internet, p_debug); + + //stores all the project files inside the Gradle project directory. Also includes all ABIs + if (!apk_expansion) { + DirAccess *da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (da_res->dir_exists("res://android/build/assets")) { + DirAccess *da_assets = DirAccess::open("res://android/build/assets"); + da_assets->erase_contents_recursive(); + da_res->remove("res://android/build/assets"); + } + err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, NULL, ignore_so_file); + if (err != OK) { + EditorNode::add_io_error("Could not export project files to gradle project\n"); + return err; + } + } else { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } + store_file_at_path("res://android/build/assets/_cl_", command_line_flags); OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required - String build_command; + #ifdef WINDOWS_ENABLED build_command = "gradlew.bat"; #else @@ -2043,10 +2353,12 @@ public: #endif String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build"); - build_command = build_path.plus_file(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 enabled_abi_string = String("|").join(enabled_abis); Vector<PluginConfig> enabled_plugins = get_enabled_plugins(p_preset); String local_plugins_binaries = get_plugins_binaries(BINARY_TYPE_LOCAL, enabled_plugins); @@ -2058,8 +2370,20 @@ public: if (clean_build_required) { cmdline.push_back("clean"); } - cmdline.push_back("build"); + + String build_type = p_debug ? "Debug" : "Release"; + if (export_format == 1) { + String bundle_build_command = vformat("bundle%s", build_type); + cmdline.push_back(bundle_build_command); + } else if (export_format == 0) { + String apk_build_command = vformat("assemble%s", build_type); + cmdline.push_back(apk_build_command); + } + cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. + cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. + cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name. + cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs. cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies. cmdline.push_back("-Pplugins_remote_binaries=" + remote_plugins_binaries); // argument to specify the list of plugins remote dependencies. cmdline.push_back("-Pplugins_maven_repos=" + custom_maven_repos); // argument to specify the list of custom maven repos for the plugins dependencies. @@ -2077,35 +2401,54 @@ public: 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.")); return ERR_CANT_CREATE; } - if (p_debug) { - src_apk = build_path.plus_file("build/outputs/apk/debug/android_debug.apk"); - } else { - src_apk = build_path.plus_file("build/outputs/apk/release/android_release.apk"); + + List<String> copy_args; + String copy_command; + if (export_format == 1) { + copy_command = vformat("copyAndRename%sAab", build_type); + } else if (export_format == 0) { + copy_command = vformat("copyAndRename%sApk", build_type); } - if (!FileAccess::exists(src_apk)) { - EditorNode::get_singleton()->show_warning(TTR("No build apk generated at: ") + "\n" + src_apk); + copy_args.push_back(copy_command); + + copy_args.push_back("-p"); // argument to specify the start directory. + copy_args.push_back(build_path); // start directory. + + String export_filename = p_path.get_file(); + String export_path = p_path.get_base_dir(); + + copy_args.push_back("-Pexport_path=file:" + export_path); + copy_args.push_back("-Pexport_filename=" + export_filename); + + 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.")); return ERR_CANT_CREATE; } - - } else { + if (_signed) { + err = sign_apk(p_preset, p_debug, p_path, ep); + if (err != OK) { + return err; + } + } + return OK; + } + // This is the start of the Legacy build system + if (p_debug) + src_apk = p_preset->get("custom_template/debug"); + else + src_apk = p_preset->get("custom_template/release"); + src_apk = src_apk.strip_edges(); + if (src_apk == "") { if (p_debug) { - src_apk = p_preset->get("custom_template/debug"); + src_apk = find_export_template("android_debug.apk"); } else { - src_apk = p_preset->get("custom_template/release"); + src_apk = find_export_template("android_release.apk"); } - - src_apk = src_apk.strip_edges(); if (src_apk == "") { - if (p_debug) { - src_apk = find_export_template("android_debug.apk"); - } else { - src_apk = find_export_template("android_release.apk"); - } - if (src_apk == "") { - EditorNode::add_io_error("Package not found: " + src_apk); - return ERR_FILE_NOT_FOUND; - } + EditorNode::add_io_error("Package not found: " + src_apk); + return ERR_FILE_NOT_FOUND; } } @@ -2142,50 +2485,13 @@ public: zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); - bool _signed = p_preset->get("package/signed"); String cmdline = p_preset->get("command_line/extra_args"); String version_name = p_preset->get("version/name"); String package_name = p_preset->get("package/unique_name"); - bool apk_expansion = p_preset->get("apk_expansion/enable"); String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); - 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"); - - Vector<String> enabled_abis = get_enabled_abis(p_preset); - - String project_icon_path = ProjectSettings::get_singleton()->get("application/config/icon"); - - // Prepare images to be resized for the icons. If some image ends up being uninitialized, the default image from the export template will be used. - Ref<Image> launcher_icon_image; - Ref<Image> launcher_adaptive_icon_foreground_image; - Ref<Image> launcher_adaptive_icon_background_image; - - launcher_icon_image.instance(); - launcher_adaptive_icon_foreground_image.instance(); - launcher_adaptive_icon_background_image.instance(); - - // Regular icon: user selection -> project icon -> default. - String path = static_cast<String>(p_preset->get(launcher_icon_option)).strip_edges(); - if (path.empty() || ImageLoader::load_image(path, launcher_icon_image) != OK) { - ImageLoader::load_image(project_icon_path, launcher_icon_image); - } - - // Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default). - path = static_cast<String>(p_preset->get(launcher_adaptive_icon_foreground_option)).strip_edges(); - if (path.empty() || ImageLoader::load_image(path, launcher_adaptive_icon_foreground_image) != OK) { - launcher_adaptive_icon_foreground_image = launcher_icon_image; - } - - // Adaptive background: user selection -> default. - path = static_cast<String>(p_preset->get(launcher_adaptive_icon_background_option)).strip_edges(); - if (!path.empty()) { - ImageLoader::load_image(path, launcher_adaptive_icon_background_image); - } - Vector<String> invalid_abis(enabled_abis); while (ret == UNZ_OK) { //get filename @@ -2206,26 +2512,38 @@ public: unzCloseCurrentFile(pkg); //write - if (file == "AndroidManifest.xml") { - _fix_manifest(p_preset, data, p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG)); + _fix_manifest(p_preset, data, p_give_internet); } - if (file == "resources.arsc") { - if (!use_custom_build) { - _fix_resources(p_preset, data); - } + _fix_resources(p_preset, data); + } + + // Process the splash image + if (file == SPLASH_IMAGE_EXPORT_PATH && splash_image.is_valid() && !splash_image->empty()) { + _load_image_data(splash_image, data); + } + + // Process the splash bg color image + if (file == SPLASH_BG_COLOR_PATH && splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + _load_image_data(splash_bg_color_image, data); } for (int i = 0; i < icon_densities_count; ++i) { - if (launcher_icon_image.is_valid() && !launcher_icon_image->empty()) { - _process_launcher_icons(file, launcher_icon_image, launcher_icons[i], data); + if (main_image.is_valid() && !main_image->empty()) { + if (file == launcher_icons[i].export_path) { + _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); + } } - if (launcher_adaptive_icon_foreground_image.is_valid() && !launcher_adaptive_icon_foreground_image->empty()) { - _process_launcher_icons(file, launcher_adaptive_icon_foreground_image, launcher_adaptive_icon_foregrounds[i], data); + if (foreground.is_valid() && !foreground->empty()) { + if (file == launcher_adaptive_icon_foregrounds[i].export_path) { + _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); + } } - if (launcher_adaptive_icon_background_image.is_valid() && !launcher_adaptive_icon_background_image->empty()) { - _process_launcher_icons(file, launcher_adaptive_icon_background_image, launcher_adaptive_icon_backgrounds[i], data); + if (background.is_valid() && !background->empty()) { + if (file == launcher_adaptive_icon_backgrounds[i].export_path) { + _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); + } } } @@ -2283,18 +2601,26 @@ public: if (ep.step("Adding files...", 1)) { CLEANUP_AND_RETURN(ERR_SKIP); } - Error err = OK; + err = OK; if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; err = export_project_files(p_preset, ignore_apk_file, &ed, save_apk_so); - } else if (!apk_expansion) { - APKExportData ed; - ed.ep = &ep; - ed.apk = unaligned_apk; - err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } else { + if (apk_expansion) { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } else { + APKExportData ed; + ed.ep = &ep; + ed.apk = unaligned_apk; + err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } } if (err != OK) { @@ -2303,10 +2629,6 @@ public: CLEANUP_AND_RETURN(ERR_SKIP); } - Vector<uint8_t> command_line_flags; - // Write command line flags into the command_line_flags variable. - err = get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); - zip_fileinfo zipfi = get_zip_fileinfo(); zipOpenNewFileInZip(unaligned_apk, "assets/_cl_", @@ -2328,84 +2650,9 @@ public: } if (_signed) { - String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); - if (!FileAccess::exists(jarsigner)) { - EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); - CLEANUP_AND_RETURN(OK); - } - - String keystore; - String password; - String user; - if (p_debug) { - keystore = p_preset->get("keystore/debug"); - password = p_preset->get("keystore/debug_password"); - user = p_preset->get("keystore/debug_user"); - - if (keystore.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"); - } - - if (ep.step("Signing debug APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - } else { - keystore = release_keystore; - password = release_password; - user = release_username; - - if (ep.step("Signing release APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - } - - if (!FileAccess::exists(keystore)) { - EditorNode::add_io_error("Could not find keystore, unable to export."); - CLEANUP_AND_RETURN(ERR_FILE_CANT_OPEN); - } - - List<String> args; - args.push_back("-digestalg"); - args.push_back("SHA-256"); - args.push_back("-sigalg"); - args.push_back("SHA256withRSA"); - String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); - if (tsa_url != "") { - args.push_back("-tsa"); - args.push_back(tsa_url); - } - args.push_back("-verbose"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back("-storepass"); - args.push_back(password); - args.push_back(tmp_unaligned_path); - args.push_back(user); - int retval; - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); - } - - if (ep.step("Verifying APK...", 104)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - args.clear(); - args.push_back("-verify"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back(tmp_unaligned_path); - args.push_back("-verbose"); - - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); + err = sign_apk(p_preset, p_debug, tmp_unaligned_path, ep); + if (err != OK) { + CLEANUP_AND_RETURN(err); } } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 622860c307..95f870bc35 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -99,7 +99,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // 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. -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) { +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) { String dst_path = p_path.replace_first("res://", "res://android/build/assets/"); Error err = store_file_at_path(dst_path, p_data); return err; @@ -142,4 +142,104 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset return OK; } +String bool_to_string(bool v) { + return v ? "true" : "false"; +} + +String _get_gles_tag() { + bool min_gles3 = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name") == "GLES3" && + !ProjectSettings::get_singleton()->get("rendering/quality/driver/fallback_to_gles2"); + return min_gles3 ? " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n" : ""; +} + +String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) { + String manifest_screen_sizes = " <supports-screens \n tools:node=\"replace\""; + String sizes[] = { "small", "normal", "large", "xlarge" }; + size_t num_sizes = sizeof(sizes) / sizeof(sizes[0]); + for (size_t i = 0; i < num_sizes; i++) { + String feature_name = vformat("screen/support_%s", sizes[i]); + String feature_support = bool_to_string(p_preset->get(feature_name)); + String xml_entry = vformat("\n android:%sScreens=\"%s\"", sizes[i], feature_support); + manifest_screen_sizes += xml_entry; + } + manifest_screen_sizes += " />\n"; + return manifest_screen_sizes; +} + +String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { + String manifest_xr_features; + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + if (uses_xr) { + int dof_index = p_preset->get("xr_features/degrees_of_freedom"); // 0: none, 1: 3dof and 6dof, 2: 6dof + if (dof_index == 1) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"false\" android:version=\"1\" />\n"; + } else if (dof_index == 2) { + 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 == 1) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; + } else if (hand_tracking_index == 2) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"true\" />\n"; + } + } + 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_plugins_tag(const String &plugins_names) { + if (!plugins_names.empty()) { + return vformat(" <meta-data tools:node=\"replace\" android:name=\"plugins\" android:value=\"%s\" />\n", plugins_names); + } else { + return " <meta-data tools:node=\"remove\" android:name=\"plugins\" />\n"; + } +} + +String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + String orientation = (int)(p_preset->get("screen/orientation")) == 1 ? "portrait" : "landscape"; + String manifest_activity_text = vformat( + " <activity android:name=\"com.godot.game.GodotApp\" " + "tools:replace=\"android:screenOrientation\" " + "android:screenOrientation=\"%s\">\n", + orientation); + if (uses_xr) { + String focus_awareness = bool_to_string(p_preset->get("xr_features/focus_awareness")); + manifest_activity_text += vformat(" <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"%s\" />\n", focus_awareness); + } else { + manifest_activity_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.vr.focusaware\" />\n"; + } + manifest_activity_text += " </activity>\n"; + return manifest_activity_text; +} + +String _get_application_tag(const Ref<EditorExportPreset> &p_preset, const String &plugins_names) { + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + String manifest_application_text = + " <application android:label=\"@string/godot_project_name_string\"\n" + " android:allowBackup=\"false\" tools:ignore=\"GoogleAppIndexingWarning\"\n" + " android:icon=\"@mipmap/icon\">)\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_mode_metadata_name\" />\n"; + + manifest_application_text += _get_plugins_tag(plugins_names); + if (uses_xr) { + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.samsung.android.vr.application.mode\" android:value=\"vr_only\" />\n"; + } + manifest_application_text += _get_activity_tag(p_preset); + manifest_application_text += " </application>\n"; + return manifest_application_text; +} + #endif //GODOT_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 48c09552c1..e94681659c 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -38,7 +38,7 @@ <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" - android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:theme="@style/GodotAppSplashTheme" android:launchMode="singleTask" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 19202d2310..ceacfec9e1 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -80,8 +80,15 @@ android { ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~" } + ndk { + String[] export_abi_list = getExportEnabledABIs() + abiFilters export_abi_list + } + // Feel free to modify the application id to your own. applicationId getExportPackageName() + versionCode getExportVersionCode() + versionName getExportVersionName() minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk } @@ -123,3 +130,27 @@ android { } } } + +task copyAndRenameDebugApk(type: Copy) { + from "$buildDir/outputs/apk/debug/android_debug.apk" + into getExportPath() + rename "android_debug.apk", getExportFilename() +} + +task copyAndRenameReleaseApk(type: Copy) { + from "$buildDir/outputs/apk/release/android_release.apk" + into getExportPath() + rename "android_release.apk", getExportFilename() +} + +task copyAndRenameDebugAab(type: Copy) { + from "$buildDir/outputs/bundle/debug/build-debug.aab" + into getExportPath() + rename "build-debug.aab", getExportFilename() +} + +task copyAndRenameReleaseAab(type: Copy) { + from "$buildDir/outputs/bundle/release/build-release.aab" + into getExportPath() + rename "build-release.aab", getExportFilename() +} diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index acfdef531e..d1176e6196 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -28,8 +28,55 @@ ext.getExportPackageName = { -> return appId } +ext.getExportVersionCode = { -> + String versionCode = project.hasProperty("export_version_code") ? project.property("export_version_code") : "" + if (versionCode == null || versionCode.isEmpty()) { + versionCode = "1" + } + return Integer.parseInt(versionCode) +} + +ext.getExportVersionName = { -> + String versionName = project.hasProperty("export_version_name") ? project.property("export_version_name") : "" + if (versionName == null || versionName.isEmpty()) { + versionName = "1.0" + } + return versionName +} + final String PLUGIN_VALUE_SEPARATOR_REGEX = "\\|" +// get the list of ABIs the project should be exported to +ext.getExportEnabledABIs = { -> + String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""; + if (enabledABIs == null || enabledABIs.isEmpty()) { + enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|" + } + Set<String> exportAbiFilter = []; + for (String abi_name : enabledABIs.split(PLUGIN_VALUE_SEPARATOR_REGEX)) { + if (!abi_name.trim().isEmpty()){ + exportAbiFilter.add(abi_name); + } + } + return exportAbiFilter; +} + +ext.getExportPath = { + String exportPath = project.hasProperty("export_path") ? project.property("export_path") : "" + if (exportPath == null || exportPath.isEmpty()) { + exportPath = "." + } + return exportPath +} + +ext.getExportFilename = { + String exportFilename = project.hasProperty("export_filename") ? project.property("export_filename") : "" + if (exportFilename == null || exportFilename.isEmpty()) { + exportFilename = "godot_android" + } + return exportFilename +} + /** * Parse the project properties for the 'plugins_maven_repos' property and return the list * of maven repos. diff --git a/platform/android/java/app/res/drawable/splash.png b/platform/android/java/app/res/drawable/splash.png Binary files differnew file mode 100644 index 0000000000..7bddd4325a --- /dev/null +++ b/platform/android/java/app/res/drawable/splash.png diff --git a/platform/android/java/app/res/drawable/splash_bg_color.png b/platform/android/java/app/res/drawable/splash_bg_color.png Binary files differnew file mode 100644 index 0000000000..004b6fd508 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_bg_color.png diff --git a/platform/android/java/app/res/drawable/splash_drawable.xml b/platform/android/java/app/res/drawable/splash_drawable.xml new file mode 100644 index 0000000000..2794a40817 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_drawable.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/splash_bg_color" /> + + <item> + <bitmap + android:gravity="center" + android:src="@drawable/splash" /> + </item> + +</layer-list> diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml new file mode 100644 index 0000000000..26912538d3 --- /dev/null +++ b/platform/android/java/app/res/values/themes.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/> + + <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme"> + <item name="android:windowBackground">@drawable/splash_drawable</item> + </style> +</resources> 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 1af5950cbe..51df70969e 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -32,9 +32,16 @@ package com.godot.game; import org.godotengine.godot.FullScreenGodotApp; +import android.os.Bundle; + /** * Template activity for Godot Android custom builds. * Feel free to extend and modify this class for your custom logic. */ public class GodotApp extends FullScreenGodotApp { + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(R.style.GodotAppMainTheme); + super.onCreate(savedInstanceState); + } } diff --git a/platform/android/java/lib/res/values/dimens.xml b/platform/android/java/lib/res/values/dimens.xml new file mode 100644 index 0000000000..9034dbbcc1 --- /dev/null +++ b/platform/android/java/lib/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="text_edit_height">48dp</dimen> +</resources> 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 138c2de94c..5aa48d87da 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -34,6 +34,8 @@ import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; /** @@ -43,13 +45,18 @@ import androidx.fragment.app.FragmentActivity; * within an Android app. */ public abstract class FullScreenGodotApp extends FragmentActivity { - protected Godot godotFragment; + @Nullable + private Godot godotFragment; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.godot_app_layout); - godotFragment = new Godot(); + 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(); } @@ -76,4 +83,17 @@ public abstract class FullScreenGodotApp extends FragmentActivity { } return super.onKeyMultiple(inKeyCode, repeatCount, event); } + + /** + * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}. + */ + @NonNull + protected Godot initGodotInstance() { + return new Godot(); + } + + @Nullable + protected final Godot getGodotFragment() { + return godotFragment; + } } 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 1ae400abb5..524f32bf5e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -151,8 +151,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void setButtonPausedState(boolean paused) { mStatePaused = paused; - int stringResourceID = paused ? R.string.text_button_resume : - R.string.text_button_pause; + int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; mPauseButton.setText(stringResourceID); } @@ -221,7 +220,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // GodotEditText layout GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + (int)getResources().getDimension(R.dimen.text_edit_height))); // ...add to FrameLayout containerLayout.addView(editText); @@ -467,7 +467,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); 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 4dd228e53b..c2f3c88416 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -461,9 +461,9 @@ public class GodotIO { return (int)(metrics.density * 160f); } - public void showKeyboard(String p_existing_text, int p_max_input_length, int p_cursor_start, int p_cursor_end) { + 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_max_input_length, p_cursor_start, p_cursor_end); + edit.showKeyboard(p_existing_text, p_multiline, 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/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index c0defd008e..c95339c583 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 @@ -36,6 +36,7 @@ import android.content.Context; import android.os.Handler; import android.os.Message; import android.text.InputFilter; +import android.text.InputType; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; @@ -58,7 +59,8 @@ public class GodotEditText extends EditText { private GodotTextInputWrapper mInputWrapper; private EditHandler sHandler = new EditHandler(this); private String mOriginText; - private int mMaxInputLength; + private int mMaxInputLength = Integer.MAX_VALUE; + private boolean mMultiline = false; private static class EditHandler extends Handler { private final WeakReference<GodotEditText> mEdit; @@ -95,7 +97,11 @@ public class GodotEditText extends EditText { protected void initView() { setPadding(0, 0, 0, 0); - setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE); + } + + public boolean isMultiline() { + return mMultiline; } private void handleMessage(final Message msg) { @@ -115,6 +121,12 @@ public class GodotEditText extends EditText { edit.mInputWrapper.setSelection(false); } + int inputType = InputType.TYPE_CLASS_TEXT; + if (edit.isMultiline()) { + inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; + } + edit.setInputType(inputType); + edit.mInputWrapper.setOriginText(text); edit.addTextChangedListener(edit.mInputWrapper); final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); @@ -189,7 +201,7 @@ public class GodotEditText extends EditText { // =========================================================== // Methods // =========================================================== - public void showKeyboard(String p_existing_text, int p_max_input_length, int p_cursor_start, int p_cursor_end) { + public void showKeyboard(String p_existing_text, boolean p_multiline, 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; @@ -202,6 +214,8 @@ public class GodotEditText extends EditText { this.mMaxInputLength = maxInputLength - (p_existing_text.length() - p_cursor_end); } + this.mMultiline = p_multiline; + final Message msg = new Message(); msg.what = HANDLER_OPEN_IME_KEYBOARD; msg.obj = this; 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 9c7cf9f341..4dd1054738 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 @@ -123,7 +123,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene public void run() { for (int i = 0; i < count; ++i) { int key = newChars[i]; - if (key == '\n') { + if ((key == '\n') && !mEdit.isMultiline()) { // Return keys are handled through action events continue; } @@ -151,7 +151,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene }); } - if (pActionID == EditorInfo.IME_NULL) { + if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, true); GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, false); diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt index aeb4628d5d..7fa8e3b4e5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -71,7 +71,7 @@ internal class VkRenderer { */ fun onVkSurfaceChanged(surface: Surface, width: Int, height: Int) { GodotLib.resize(surface, width, height) - + for (plugin in pluginRegistry.getAllPlugins()) { plugin.onVkSurfaceChanged(surface, width, height) } diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 0a42adeaf2..4ccbc6b97e 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -53,7 +53,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); - _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;III)V"); + _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)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"); @@ -132,11 +132,11 @@ bool GodotIOJavaWrapper::has_vk() { return (_show_keyboard != 0) && (_hide_keyboard != 0); } -void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_max_input_length, int p_cursor_start, int p_cursor_end) { +void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (_show_keyboard) { JNIEnv *env = ThreadAndroid::get_env(); jstring jStr = env->NewStringUTF(p_existing.utf8().get_data()); - env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_max_input_length, p_cursor_start, p_cursor_end); + env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); } } diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 1742021379..6465ded985 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -70,7 +70,7 @@ public: int get_screen_dpi(); String get_unique_id(); bool has_vk(); - void show_vk(const String &p_existing, int p_max_input_length, int p_cursor_start, int p_cursor_end); + void show_vk(const String &p_existing, bool p_multiline, 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); diff --git a/platform/android/logo.png b/platform/android/logo.png Binary files differindex df445f6a9c..f44d360a25 100644 --- a/platform/android/logo.png +++ b/platform/android/logo.png |