浏览代码

Merge pull request #88297 from dsnopek/android-configure-gradle-path

Android: Allow using alternative Gradle build directory
Rémi Verschelde 1 年之前
父节点
当前提交
66b33c15e4

+ 69 - 14
editor/editor_node.cpp

@@ -170,6 +170,9 @@ static const String META_TEXT_TO_COPY = "text_to_copy";
 
 static const String EDITOR_NODE_CONFIG_SECTION = "EditorNode";
 
+static const String REMOVE_ANDROID_BUILD_TEMPLATE_MESSAGE = "The Android build template is already installed in this project and it won't be overwritten.\nRemove the \"%s\" directory manually before attempting this operation again.";
+static const String INSTALL_ANDROID_BUILD_TEMPLATE_MESSAGE = "This will set up your project for gradle Android builds by installing the source template to \"%s\".\nNote that in order to make gradle builds instead of using pre-built APKs, the \"Use Gradle Build\" option should be enabled in the Android export preset.";
+
 void EditorNode::disambiguate_filenames(const Vector<String> p_full_paths, Vector<String> &r_filenames) {
 	ERR_FAIL_COND_MSG(p_full_paths.size() != r_filenames.size(), vformat("disambiguate_filenames requires two string vectors of same length (%d != %d).", p_full_paths.size(), r_filenames.size()));
 
@@ -966,7 +969,7 @@ void EditorNode::_fs_changed() {
 					String config_error;
 					bool missing_templates;
 					if (export_defer.android_build_template) {
-						export_template_manager->install_android_template();
+						export_template_manager->install_android_template(export_preset);
 					}
 					if (!platform->can_export(export_preset, config_error, missing_templates, export_defer.debug)) {
 						ERR_PRINT(vformat("Cannot export project with preset \"%s\" due to configuration errors:\n%s", preset_name, config_error));
@@ -2525,7 +2528,16 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 }
 
 void EditorNode::_android_build_source_selected(const String &p_file) {
-	export_template_manager->install_android_template_from_file(p_file);
+	export_template_manager->install_android_template_from_file(p_file, android_export_preset);
+}
+
+void EditorNode::_android_export_preset_selected(int p_index) {
+	if (p_index >= 0) {
+		android_export_preset = EditorExport::get_singleton()->get_export_preset(choose_android_export_profile->get_item_id(p_index));
+	} else {
+		android_export_preset.unref();
+	}
+	install_android_build_template_message->set_text(vformat(TTR(INSTALL_ANDROID_BUILD_TEMPLATE_MESSAGE), export_template_manager->get_android_build_directory(android_export_preset)));
 }
 
 void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {
@@ -2811,14 +2823,45 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {
 		} break;
 		case FILE_INSTALL_ANDROID_SOURCE: {
 			if (p_confirmed) {
-				export_template_manager->install_android_template();
-			} else {
-				if (DirAccess::exists("res://android/build")) {
+				if (export_template_manager->is_android_template_installed(android_export_preset)) {
+					remove_android_build_template->set_text(vformat(TTR(REMOVE_ANDROID_BUILD_TEMPLATE_MESSAGE), export_template_manager->get_android_build_directory(android_export_preset)));
 					remove_android_build_template->popup_centered();
-				} else if (export_template_manager->can_install_android_template()) {
+				} else if (!export_template_manager->can_install_android_template(android_export_preset)) {
+					gradle_build_manage_templates->popup_centered();
+				} else {
+					export_template_manager->install_android_template(android_export_preset);
+				}
+			} else {
+				bool has_custom_gradle_build = false;
+				choose_android_export_profile->clear();
+				for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
+					Ref<EditorExportPreset> export_preset = EditorExport::get_singleton()->get_export_preset(i);
+					if (export_preset->get_platform()->get_class_name() == "EditorExportPlatformAndroid" && (bool)export_preset->get("gradle_build/use_gradle_build")) {
+						choose_android_export_profile->add_item(export_preset->get_name(), i);
+						String gradle_build_directory = export_preset->get("gradle_build/gradle_build_directory");
+						String android_source_template = export_preset->get("gradle_build/android_source_template");
+						if (!android_source_template.is_empty() || (gradle_build_directory != "" && gradle_build_directory != "res://android")) {
+							has_custom_gradle_build = true;
+						}
+					}
+				}
+				_android_export_preset_selected(choose_android_export_profile->get_item_count() >= 1 ? 0 : -1);
+
+				if (choose_android_export_profile->get_item_count() > 1 && has_custom_gradle_build) {
+					// If there's multiple options and at least one of them uses a custom gradle build then prompt the user to choose.
+					choose_android_export_profile->show();
 					install_android_build_template->popup_centered();
 				} else {
-					gradle_build_manage_templates->popup_centered();
+					choose_android_export_profile->hide();
+
+					if (export_template_manager->is_android_template_installed(android_export_preset)) {
+						remove_android_build_template->set_text(vformat(TTR(REMOVE_ANDROID_BUILD_TEMPLATE_MESSAGE), export_template_manager->get_android_build_directory(android_export_preset)));
+						remove_android_build_template->popup_centered();
+					} else if (export_template_manager->can_install_android_template(android_export_preset)) {
+						install_android_build_template->popup_centered();
+					} else {
+						gradle_build_manage_templates->popup_centered();
+					}
 				}
 			}
 		} break;
@@ -2831,7 +2874,7 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {
 			OS::get_singleton()->shell_show_in_file_manager(OS::get_singleton()->get_user_data_dir(), true);
 		} break;
 		case FILE_EXPLORE_ANDROID_BUILD_TEMPLATES: {
-			OS::get_singleton()->shell_show_in_file_manager(ProjectSettings::get_singleton()->get_resource_path().path_join("android"), true);
+			OS::get_singleton()->shell_show_in_file_manager(ProjectSettings::get_singleton()->globalize_path(export_template_manager->get_android_build_directory(android_export_preset).get_base_dir()), true);
 		} break;
 		case FILE_QUIT:
 		case RUN_PROJECT_MANAGER:
@@ -7216,14 +7259,26 @@ EditorNode::EditorNode() {
 	file_android_build_source->connect("file_selected", callable_mp(this, &EditorNode::_android_build_source_selected));
 	gui_base->add_child(file_android_build_source);
 
-	install_android_build_template = memnew(ConfirmationDialog);
-	install_android_build_template->set_text(TTR("This will set up your project for gradle Android builds by installing the source template to \"res://android/build\".\nYou can then apply modifications and build your own custom APK on export (adding modules, changing the AndroidManifest.xml, etc.).\nNote that in order to make gradle builds instead of using pre-built APKs, the \"Use Gradle Build\" option should be enabled in the Android export preset."));
-	install_android_build_template->set_ok_button_text(TTR("Install"));
-	install_android_build_template->connect("confirmed", callable_mp(this, &EditorNode::_menu_confirm_current));
-	gui_base->add_child(install_android_build_template);
+	{
+		VBoxContainer *vbox = memnew(VBoxContainer);
+		install_android_build_template_message = memnew(Label);
+		install_android_build_template_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
+		install_android_build_template_message->set_custom_minimum_size(Size2(300 * EDSCALE, 1));
+		vbox->add_child(install_android_build_template_message);
+
+		choose_android_export_profile = memnew(OptionButton);
+		choose_android_export_profile->connect("item_selected", callable_mp(this, &EditorNode::_android_export_preset_selected));
+		vbox->add_child(choose_android_export_profile);
+
+		install_android_build_template = memnew(ConfirmationDialog);
+		install_android_build_template->set_ok_button_text(TTR("Install"));
+		install_android_build_template->connect("confirmed", callable_mp(this, &EditorNode::_menu_confirm_current));
+		install_android_build_template->add_child(vbox);
+		install_android_build_template->set_min_size(Vector2(500.0 * EDSCALE, 0));
+		gui_base->add_child(install_android_build_template);
+	}
 
 	remove_android_build_template = memnew(ConfirmationDialog);
-	remove_android_build_template->set_text(TTR("The Android build template is already installed in this project and it won't be overwritten.\nRemove the \"res://android/build\" directory manually before attempting this operation again."));
 	remove_android_build_template->set_ok_button_text(TTR("Show in File Manager"));
 	remove_android_build_template->connect("confirmed", callable_mp(this, &EditorNode::_menu_option).bind(FILE_EXPLORE_ANDROID_BUILD_TEMPLATES));
 	gui_base->add_child(remove_android_build_template);

+ 5 - 0
editor/editor_node.h

@@ -80,6 +80,7 @@ class EditorBuildProfileManager;
 class EditorCommandPalette;
 class EditorDockManager;
 class EditorExport;
+class EditorExportPreset;
 class EditorExtensionManager;
 class EditorFeatureProfileManager;
 class EditorFileDialog;
@@ -382,6 +383,9 @@ private:
 	ConfirmationDialog *gradle_build_manage_templates = nullptr;
 	ConfirmationDialog *install_android_build_template = nullptr;
 	ConfirmationDialog *remove_android_build_template = nullptr;
+	Label *install_android_build_template_message = nullptr;
+	OptionButton *choose_android_export_profile = nullptr;
+	Ref<EditorExportPreset> android_export_preset;
 
 	PopupMenu *vcs_actions_menu = nullptr;
 	EditorFileDialog *file = nullptr;
@@ -528,6 +532,7 @@ private:
 	void _menu_option_confirm(int p_option, bool p_confirmed);
 
 	void _android_build_source_selected(const String &p_file);
+	void _android_export_preset_selected(int p_index);
 
 	void _request_screenshot();
 	void _screenshot(bool p_use_utc = false);

+ 56 - 16
editor/export/export_template_manager.cpp

@@ -38,6 +38,7 @@
 #include "editor/editor_paths.h"
 #include "editor/editor_settings.h"
 #include "editor/editor_string_names.h"
+#include "editor/export/editor_export.h"
 #include "editor/progress_dialog.h"
 #include "editor/themes/editor_scale.h"
 #include "scene/gui/file_dialog.h"
@@ -655,39 +656,78 @@ void ExportTemplateManager::_hide_dialog() {
 	hide();
 }
 
-bool ExportTemplateManager::can_install_android_template() {
+String ExportTemplateManager::get_android_build_directory(const Ref<EditorExportPreset> &p_preset) {
+	if (p_preset.is_valid()) {
+		String gradle_build_dir = p_preset->get("gradle_build/gradle_build_directory");
+		if (!gradle_build_dir.is_empty()) {
+			return gradle_build_dir.path_join("build");
+		}
+	}
+	return "res://android/build";
+}
+
+String ExportTemplateManager::get_android_source_zip(const Ref<EditorExportPreset> &p_preset) {
+	if (p_preset.is_valid()) {
+		String android_source_zip = p_preset->get("gradle_build/android_source_template");
+		if (!android_source_zip.is_empty()) {
+			return android_source_zip;
+		}
+	}
+
 	const String templates_dir = EditorPaths::get_singleton()->get_export_templates_dir().path_join(VERSION_FULL_CONFIG);
-	return FileAccess::exists(templates_dir.path_join("android_source.zip"));
+	return templates_dir.path_join("android_source.zip");
 }
 
-Error ExportTemplateManager::install_android_template() {
-	const String &templates_path = EditorPaths::get_singleton()->get_export_templates_dir().path_join(VERSION_FULL_CONFIG);
-	const String &source_zip = templates_path.path_join("android_source.zip");
+String ExportTemplateManager::get_android_template_identifier(const Ref<EditorExportPreset> &p_preset) {
+	// The template identifier is the Godot version for the default template, and the full path plus md5 hash for custom templates.
+	if (p_preset.is_valid()) {
+		String android_source_zip = p_preset->get("gradle_build/android_source_template");
+		if (!android_source_zip.is_empty()) {
+			return android_source_zip + String(" [") + FileAccess::get_md5(android_source_zip) + String("]");
+		}
+	}
+	return VERSION_FULL_CONFIG;
+}
+
+bool ExportTemplateManager::is_android_template_installed(const Ref<EditorExportPreset> &p_preset) {
+	return DirAccess::exists(get_android_build_directory(p_preset));
+}
+
+bool ExportTemplateManager::can_install_android_template(const Ref<EditorExportPreset> &p_preset) {
+	return FileAccess::exists(get_android_source_zip(p_preset));
+}
+
+Error ExportTemplateManager::install_android_template(const Ref<EditorExportPreset> &p_preset) {
+	const String source_zip = get_android_source_zip(p_preset);
 	ERR_FAIL_COND_V(!FileAccess::exists(source_zip), ERR_CANT_OPEN);
-	return install_android_template_from_file(source_zip);
+	return install_android_template_from_file(source_zip, p_preset);
 }
-Error ExportTemplateManager::install_android_template_from_file(const String &p_file) {
+
+Error ExportTemplateManager::install_android_template_from_file(const String &p_file, const Ref<EditorExportPreset> &p_preset) {
 	// To support custom Android builds, we install the Java source code and buildsystem
 	// from android_source.zip to the project's res://android folder.
 
 	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
 	ERR_FAIL_COND_V(da.is_null(), ERR_CANT_CREATE);
 
-	// Make res://android dir (if it does not exist).
-	da->make_dir("android");
+	String build_dir = get_android_build_directory(p_preset);
+	String parent_dir = build_dir.get_base_dir();
+
+	// Make parent of the build dir (if it does not exist).
+	da->make_dir_recursive(parent_dir);
 	{
-		// Add version, to ensure building won't work if template and Godot version don't match.
-		Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::WRITE);
+		// Add identifier, to ensure building won't work if the current template doesn't match.
+		Ref<FileAccess> f = FileAccess::open(parent_dir.path_join(".build_version"), FileAccess::WRITE);
 		ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
-		f->store_line(VERSION_FULL_CONFIG);
+		f->store_line(get_android_template_identifier(p_preset));
 	}
 
 	// Create the android build directory.
-	Error err = da->make_dir_recursive("android/build");
+	Error err = da->make_dir_recursive(build_dir);
 	ERR_FAIL_COND_V(err != OK, err);
 	{
 		// Add an empty .gdignore file to avoid scan.
-		Ref<FileAccess> f = FileAccess::open("res://android/build/.gdignore", FileAccess::WRITE);
+		Ref<FileAccess> f = FileAccess::open(build_dir.path_join(".gdignore"), FileAccess::WRITE);
 		ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE);
 		f->store_line("");
 	}
@@ -735,11 +775,11 @@ Error ExportTemplateManager::install_android_template_from_file(const String &p_
 			unzCloseCurrentFile(pkg);
 
 			if (!dirs_tested.has(base_dir)) {
-				da->make_dir_recursive(String("android/build").path_join(base_dir));
+				da->make_dir_recursive(build_dir.path_join(base_dir));
 				dirs_tested.insert(base_dir);
 			}
 
-			String to_write = String("res://android/build").path_join(path);
+			String to_write = build_dir.path_join(path);
 			Ref<FileAccess> f = FileAccess::open(to_write, FileAccess::WRITE);
 			if (f.is_valid()) {
 				f->store_buffer(uncomp_data.ptr(), uncomp_data.size());

+ 9 - 3
editor/export/export_template_manager.h

@@ -33,6 +33,7 @@
 
 #include "scene/gui/dialogs.h"
 
+class EditorExportPreset;
 class ExportTemplateVersion;
 class FileDialog;
 class HTTPRequest;
@@ -121,10 +122,15 @@ protected:
 	static void _bind_methods();
 
 public:
-	bool can_install_android_template();
-	Error install_android_template();
+	static String get_android_build_directory(const Ref<EditorExportPreset> &p_preset);
+	static String get_android_source_zip(const Ref<EditorExportPreset> &p_preset);
+	static String get_android_template_identifier(const Ref<EditorExportPreset> &p_preset);
 
-	Error install_android_template_from_file(const String &p_file);
+	bool is_android_template_installed(const Ref<EditorExportPreset> &p_preset);
+	bool can_install_android_template(const Ref<EditorExportPreset> &p_preset);
+	Error install_android_template(const Ref<EditorExportPreset> &p_preset);
+
+	Error install_android_template_from_file(const String &p_file, const Ref<EditorExportPreset> &p_preset);
 
 	void popup_manager();
 

+ 10 - 2
platform/android/doc_classes/EditorExportPlatformAndroid.xml

@@ -37,14 +37,22 @@
 			A list of additional command line arguments, exported project will receive when started.
 		</member>
 		<member name="custom_template/debug" type="String" setter="" getter="">
-			Path to the custom export template. If left empty, default template is used.
+			Path to an APK file to use as a custom export template for debug exports. If left empty, default template is used.
+			[b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled.
 		</member>
 		<member name="custom_template/release" type="String" setter="" getter="">
-			Path to the custom export template. If left empty, default template is used.
+			Path to an APK file to use as a custom export template for release exports. If left empty, default template is used.
+			[b]Note:[/b] This is only used if [member EditorExportPlatformAndroid.gradle_build/use_gradle_build] is disabled.
+		</member>
+		<member name="gradle_build/android_source_template" type="String" setter="" getter="">
+			Path to a ZIP file holding the source for the export template used in a Gradle build. If left empty, the default template is used.
 		</member>
 		<member name="gradle_build/export_format" type="int" setter="" getter="">
 			Export format for Gradle build.
 		</member>
+		<member name="gradle_build/gradle_build_directory" type="String" setter="" getter="">
+			Path to the Gradle build directory. If left empty, then [code]res://android[/code] will be used.
+		</member>
 		<member name="gradle_build/min_sdk" type="String" setter="" getter="">
 			Minimal Android SDK version for Gradle build.
 		</member>

+ 84 - 48
platform/android/export/export_plugin.cpp

@@ -46,6 +46,7 @@
 #include "editor/editor_node.h"
 #include "editor/editor_paths.h"
 #include "editor/editor_settings.h"
+#include "editor/export/export_template_manager.h"
 #include "editor/import/resource_importer_texture_settings.h"
 #include "editor/themes/editor_scale.h"
 #include "main/splash.gen.h"
@@ -208,12 +209,14 @@ static const char *android_perms[] = {
 	nullptr
 };
 
+static const char *MISMATCHED_VERSIONS_MESSAGE = "Android build version mismatch:\n| Template installed: %s\n| Requested version: %s\nPlease reinstall Android build template from 'Project' menu.";
+
 static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi/splash.png";
 static const char *LEGACY_BUILD_SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi-v4/splash.png";
 static const char *SPLASH_BG_COLOR_PATH = "res/drawable-nodpi/splash_bg_color.png";
 static const char *LEGACY_BUILD_SPLASH_BG_COLOR_PATH = "res/drawable-nodpi-v4/splash_bg_color.png";
-static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash_drawable.xml";
-static const char *GDEXTENSION_LIBS_PATH = "res://android/build/libs/gdextensionlibs.json";
+static const char *SPLASH_CONFIG_PATH = "res/drawable/splash_drawable.xml";
+static const char *GDEXTENSION_LIBS_PATH = "libs/gdextensionlibs.json";
 
 static const int icon_densities_count = 6;
 static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192");
@@ -250,8 +253,8 @@ static const LauncherIcon launcher_adaptive_icon_backgrounds[icon_densities_coun
 static const int EXPORT_FORMAT_APK = 0;
 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 char *APK_ASSETS_DIRECTORY = "assets";
+static const char *AAB_ASSETS_DIRECTORY = "assetPacks/installTime/src/main/assets";
 
 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;
@@ -474,7 +477,8 @@ String EditorExportPlatformAndroid::get_valid_basename() const {
 }
 
 String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const {
-	return p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY;
+	String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
+	return gradle_build_directory.path_join(p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY);
 }
 
 bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, String *r_error) const {
@@ -774,11 +778,10 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
 		}
 		if (abi_index != -1) {
 			exported = true;
-			String base = "res://android/build/libs";
 			String type = export_data->debug ? "debug" : "release";
 			String abi = abis[abi_index].abi;
 			String filename = p_so.path.get_file();
-			String dst_path = base.path_join(type).path_join(abi).path_join(filename);
+			String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename);
 			Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path);
 			print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);
 			Error err = store_file_at_path(dst_path, data);
@@ -867,7 +870,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
 
 	manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug);
 	manifest_text += "</manifest>\n";
-	String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
+	String manifest_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join(vformat("src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")));
 
 	print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text);
 	store_string_at_path(manifest_path, manifest_text);
@@ -1628,15 +1631,6 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &
 	}
 }
 
-void EditorExportPlatformAndroid::store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) {
-	store_image(launcher_icon.export_path, data);
-}
-
-void EditorExportPlatformAndroid::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 EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
 		const String &processed_splash_config_xml,
 		const Ref<Image> &splash_image,
@@ -1644,26 +1638,30 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
 		const Ref<Image> &main_image,
 		const Ref<Image> &foreground,
 		const Ref<Image> &background) {
+	String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
+
 	// Store the splash configuration
 	if (!processed_splash_config_xml.is_empty()) {
 		print_verbose("Storing processed splash configuration: " + String("\n") + processed_splash_config_xml);
-		store_string_at_path(SPLASH_CONFIG_PATH, processed_splash_config_xml);
+		store_string_at_path(gradle_build_dir.path_join(SPLASH_CONFIG_PATH), processed_splash_config_xml);
 	}
 
 	// Store the splash image
 	if (splash_image.is_valid() && !splash_image->is_empty()) {
-		print_verbose("Storing splash image in " + String(SPLASH_IMAGE_EXPORT_PATH));
+		String splash_export_path = gradle_build_dir.path_join(SPLASH_IMAGE_EXPORT_PATH);
+		print_verbose("Storing splash image in " + splash_export_path);
 		Vector<uint8_t> data;
 		_load_image_data(splash_image, data);
-		store_image(SPLASH_IMAGE_EXPORT_PATH, data);
+		store_file_at_path(splash_export_path, data);
 	}
 
 	// Store the splash bg color image
 	if (splash_bg_color_image.is_valid() && !splash_bg_color_image->is_empty()) {
-		print_verbose("Storing splash background image in " + String(SPLASH_BG_COLOR_PATH));
+		String splash_bg_color_path = gradle_build_dir.path_join(SPLASH_BG_COLOR_PATH);
+		print_verbose("Storing splash background image in " + splash_bg_color_path);
 		Vector<uint8_t> data;
 		_load_image_data(splash_bg_color_image, data);
-		store_image(SPLASH_BG_COLOR_PATH, data);
+		store_file_at_path(splash_bg_color_path, data);
 	}
 
 	// Prepare images to be resized for the icons. If some image ends up being uninitialized,
@@ -1674,7 +1672,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
 			print_verbose("Processing launcher icon for dimension " + itos(launcher_icons[i].dimensions) + " into " + launcher_icons[i].export_path);
 			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);
+			store_file_at_path(gradle_build_dir.path_join(launcher_icons[i].export_path), data);
 		}
 
 		if (foreground.is_valid() && !foreground->is_empty()) {
@@ -1682,7 +1680,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
 			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);
+			store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_foregrounds[i].export_path), data);
 		}
 
 		if (background.is_valid() && !background->is_empty()) {
@@ -1690,7 +1688,7 @@ void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<Editor
 			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);
+			store_file_at_path(gradle_build_dir.path_join(launcher_adaptive_icon_backgrounds[i].export_path), data);
 		}
 	}
 }
@@ -1798,7 +1796,9 @@ 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, "gradle_build/use_gradle_build"), false, false, true));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, true));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false));
+	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));
 	// 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.
@@ -1876,6 +1876,18 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
 	}
 }
 
+bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
+	if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {
+		// @todo These are experimental options - keep them hidden for now.
+		//return (bool)p_preset->get("gradle_build/use_gradle_build");
+		return false;
+	} else if (p_option == "custom_template/debug" || p_option == "custom_template/release") {
+		// The APK templates are ignored if Gradle build is enabled.
+		return !p_preset->get("gradle_build/use_gradle_build");
+	}
+	return true;
+}
+
 String EditorExportPlatformAndroid::get_name() const {
 	return "Android";
 }
@@ -2345,7 +2357,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 			err += template_err;
 		}
 	} else {
-		bool installed_android_build_template = FileAccess::exists("res://android/build/build.gradle");
+		bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle"));
 		if (!installed_android_build_template) {
 			r_missing_templates = !exists_export_template("android_source.zip", &err);
 			err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";
@@ -2492,6 +2504,19 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit
 		valid = false;
 	}
 
+	if (p_preset->get("gradle_build/use_gradle_build")) {
+		String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version");
+		Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ);
+		if (f.is_valid()) {
+			String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
+			String installed_version = f->get_line().strip_edges();
+			if (current_version != installed_version) {
+				err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version);
+				err += "\n";
+			}
+		}
+	}
+
 	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.
@@ -2734,34 +2759,37 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 	return OK;
 }
 
-void EditorExportPlatformAndroid::_clear_assets_directory() {
+void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) {
 	Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+	String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
 
 	// Clear the APK assets directory
-	if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) {
+	String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY);
+	if (da_res->dir_exists(apk_assets_directory)) {
 		print_verbose("Clearing APK assets directory...");
-		Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY);
+		Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory);
 		ERR_FAIL_COND(da_assets.is_null());
 
 		da_assets->erase_contents_recursive();
-		da_res->remove(APK_ASSETS_DIRECTORY);
+		da_res->remove(apk_assets_directory);
 	}
 
 	// Clear the AAB assets directory
-	if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) {
+	String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY);
+	if (da_res->dir_exists(aab_assets_directory)) {
 		print_verbose("Clearing AAB assets directory...");
-		Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY);
+		Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory);
 		ERR_FAIL_COND(da_assets.is_null());
 
 		da_assets->erase_contents_recursive();
-		da_res->remove(AAB_ASSETS_DIRECTORY);
+		da_res->remove(aab_assets_directory);
 	}
 }
 
-void EditorExportPlatformAndroid::_remove_copied_libs() {
+void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) {
 	print_verbose("Removing previously installed libraries...");
 	Error error;
-	String libs_json = FileAccess::get_file_as_string(GDEXTENSION_LIBS_PATH, &error);
+	String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error);
 	if (error || libs_json.is_empty()) {
 		print_verbose("No previously installed libraries found");
 		return;
@@ -2777,7 +2805,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() {
 		print_verbose("Removing previously installed library " + libs[i]);
 		da->remove(libs[i]);
 	}
-	da->remove(GDEXTENSION_LIBS_PATH);
+	da->remove(p_gdextension_libs_path);
 }
 
 String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {
@@ -2836,6 +2864,8 @@ String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(
 bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {
 	bool first_build = last_gradle_build_time == 0;
 	bool have_plugins_changed = false;
+	String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
+	bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir;
 
 	String plugin_names = _get_plugins_names(p_preset);
 
@@ -2855,9 +2885,10 @@ bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExpor
 	}
 
 	last_gradle_build_time = OS::get_singleton()->get_unix_time();
+	last_gradle_build_dir = gradle_build_dir;
 	last_plugin_names = plugin_names;
 
-	return have_plugins_changed || first_build;
+	return have_plugins_changed || has_build_dir_changed || first_build;
 }
 
 Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
@@ -2881,6 +2912,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 	EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
 
 	bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
+	String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";
 	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);
@@ -2940,15 +2972,17 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 		//test that installed build version is alright
 		{
 			print_verbose("Checking build version...");
-			Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ);
+			String gradle_base_directory = gradle_build_directory.get_base_dir();
+			Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ);
 			if (f.is_null()) {
 				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();
-			print_verbose("- build version: " + version);
-			if (version != VERSION_FULL_CONFIG) {
-				add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG));
+			String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
+			String installed_version = f->get_line().strip_edges();
+			print_verbose("- build version: " + installed_version);
+			if (installed_version != current_version) {
+				add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version));
 				return ERR_UNCONFIGURED;
 			}
 		}
@@ -2969,9 +3003,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 
 		// TODO: should we use "package/name" or "application/config/name"?
 		String project_name = get_project_name(p_preset->get("package/name"));
-		err = _create_project_name_strings_files(p_preset, project_name); //project name localization.
+		err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory); //project name localization.
 		if (err != OK) {
-			add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name."));
+			add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));
 		}
 		// Copies the project icon files into the appropriate Gradle project directory.
 		_copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background);
@@ -2979,12 +3013,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 		_write_tmp_manifest(p_preset, p_give_internet, p_debug);
 
 		//stores all the project files inside the Gradle project directory. Also includes all ABIs
-		_clear_assets_directory();
-		_remove_copied_libs();
+		_clear_assets_directory(p_preset);
+		String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);
+		_remove_copied_libs(gdextension_libs_path);
 		if (!apk_expansion) {
 			print_verbose("Exporting project files...");
 			CustomExportData user_data;
 			user_data.assets_directory = assets_directory;
+			user_data.libs_directory = gradle_build_directory.path_join("libs");
 			user_data.debug = p_debug;
 			if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
 				err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so);
@@ -3023,7 +3059,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 		build_command = "gradlew";
 #endif
 
-		String build_path = ProjectSettings::get_singleton()->get_resource_path().path_join("android/build");
+		String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory);
 		build_command = build_path.path_join(build_command);
 
 		String package_name = get_package_name(p_preset->get("package/unique_name"));

+ 5 - 6
platform/android/export/export_plugin.h

@@ -90,6 +90,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
 #endif // DISABLE_DEPRECATED
 	String last_plugin_names;
 	uint64_t last_gradle_build_time = 0;
+	String last_gradle_build_dir;
 
 	Vector<Device> devices;
 	SafeFlag devices_changed;
@@ -174,10 +175,6 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
 
 	void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background);
 
-	void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data);
-
-	void store_image(const String &export_path, const Vector<uint8_t> &data);
-
 	void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
 			const String &processed_splash_config_xml,
 			const Ref<Image> &splash_image,
@@ -198,6 +195,8 @@ public:
 
 	virtual void get_export_options(List<ExportOption> *r_options) const override;
 
+	virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override;
+
 	virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override;
 
 	virtual String get_name() const override;
@@ -248,9 +247,9 @@ public:
 
 	Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep);
 
-	void _clear_assets_directory();
+	void _clear_assets_directory(const Ref<EditorExportPreset> &p_preset);
 
-	void _remove_copied_libs();
+	void _remove_copied_libs(String p_gdextension_libs_path);
 
 	static String join_list(const List<String> &p_parts, const String &p_separator);
 	static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch);

+ 4 - 4
platform/android/export/gradle_export_util.cpp

@@ -191,14 +191,14 @@ String _android_xml_escape(const String &p_string) {
 }
 
 // Creates strings.xml files inside the gradle project for different locales.
-Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name) {
+Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir) {
 	print_verbose("Creating strings resources for supported locales for project " + project_name);
 	// Stores the string into the default values directory.
 	String processed_default_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(project_name));
-	store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string);
+	store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string);
 
 	// Searches the Gradle project res/ directory to find all supported locales
-	Ref<DirAccess> da = DirAccess::open("res://android/build/res");
+	Ref<DirAccess> da = DirAccess::open(p_gradle_build_dir.path_join("res"));
 	if (da.is_null()) {
 		if (OS::get_singleton()->is_stdout_verbose()) {
 			print_error("Unable to open Android resources directory.");
@@ -217,7 +217,7 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset
 			continue;
 		}
 		String locale = file.replace("values-", "").replace("-r", "_");
-		String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml";
+		String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
 		if (appnames.has(locale)) {
 			String locale_project_name = appnames[locale];
 			String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name));

+ 2 - 1
platform/android/export/gradle_export_util.h

@@ -63,6 +63,7 @@ static const int XR_MODE_OPENXR = 1;
 
 struct CustomExportData {
 	String assets_directory;
+	String libs_directory;
 	bool debug;
 	Vector<String> libs;
 };
@@ -94,7 +95,7 @@ Error store_string_at_path(const String &p_path, const String &p_data);
 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.
-Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name);
+Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name, const String &p_gradle_build_dir);
 
 String bool_to_string(bool v);