2
0
Эх сурвалжийг харах

Update the `GodotHost` interface to support signing and verifying Android apks

Update the export logic to enable apk generation and signing for Android editor builds

Note: Only legacy builds are supported. Gradle builds are not supported at this point in time.
Fredia Huya-Kouadio 1 жил өмнө
parent
commit
a5897d579b

+ 1 - 0
.pre-commit-config.yaml

@@ -162,6 +162,7 @@ repos:
             modules/gdscript/tests/scripts/parser/features/mixed_indentation_on_blank_lines\.gd$|
             modules/gdscript/tests/scripts/parser/warnings/empty_file_newline_comment\.notest\.gd$|
             modules/gdscript/tests/scripts/parser/warnings/empty_file_newline\.notest\.gd$|
+            platform/android/java/editor/src/main/java/com/android/.*|
             platform/android/java/lib/src/com/google/.*
           )
 

+ 2 - 1
COPYRIGHT.txt

@@ -70,7 +70,8 @@ Copyright: 2020, Manuel Prandini
  2007-2014, Juan Linietsky, Ariel Manzur
 License: Expat
 
-Files: ./platform/android/java/lib/aidl/com/android/*
+Files: ./platform/android/java/editor/src/main/java/com/android/*
+ ./platform/android/java/lib/aidl/com/android/*
  ./platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml
  ./platform/android/java/lib/src/com/google/android/*
  ./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java

+ 0 - 2
editor/editor_node.cpp

@@ -7319,9 +7319,7 @@ EditorNode::EditorNode() {
 #endif
 
 	settings_menu->add_item(TTR("Manage Editor Features..."), SETTINGS_MANAGE_FEATURE_PROFILES);
-#ifndef ANDROID_ENABLED
 	settings_menu->add_item(TTR("Manage Export Templates..."), SETTINGS_MANAGE_EXPORT_TEMPLATES);
-#endif
 #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
 	settings_menu->add_item(TTR("Configure FBX Importer..."), SETTINGS_MANAGE_FBX_IMPORTER);
 #endif

+ 4 - 0
editor/editor_paths.cpp

@@ -71,7 +71,11 @@ String EditorPaths::get_export_templates_dir() const {
 }
 
 String EditorPaths::get_debug_keystore_path() const {
+#ifdef ANDROID_ENABLED
+	return "assets://keystores/debug.keystore";
+#else
 	return get_data_dir().path_join("keystores/debug.keystore");
+#endif
 }
 
 String EditorPaths::get_project_settings_dir() const {

+ 0 - 2
editor/export/editor_export_platform.cpp

@@ -1861,7 +1861,6 @@ void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags
 bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
 	bool valid = true;
 
-#ifndef ANDROID_ENABLED
 	String templates_error;
 	valid = valid && has_valid_export_configuration(p_preset, templates_error, r_missing_templates, p_debug);
 
@@ -1886,7 +1885,6 @@ bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, S
 	if (!export_plugins_warning.is_empty()) {
 		r_error += export_plugins_warning;
 	}
-#endif
 
 	String project_configuration_error;
 	valid = valid && has_valid_project_configuration(p_preset, project_configuration_error);

+ 5 - 1
editor/export/export_template_manager.cpp

@@ -111,7 +111,9 @@ void ExportTemplateManager::_update_template_status() {
 		TreeItem *ti = installed_table->create_item(installed_root);
 		ti->set_text(0, version_string);
 
+#ifndef ANDROID_ENABLED
 		ti->add_button(0, get_editor_theme_icon(SNAME("Folder")), OPEN_TEMPLATE_FOLDER, false, TTR("Open the folder containing these templates."));
+#endif
 		ti->add_button(0, get_editor_theme_icon(SNAME("Remove")), UNINSTALL_TEMPLATE, false, TTR("Uninstall these templates."));
 	}
 }
@@ -921,11 +923,13 @@ ExportTemplateManager::ExportTemplateManager() {
 	current_installed_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
 	current_installed_hb->add_child(current_installed_path);
 
-	current_open_button = memnew(Button);
+#ifndef ANDROID_ENABLED
+	Button *current_open_button = memnew(Button);
 	current_open_button->set_text(TTR("Open Folder"));
 	current_open_button->set_tooltip_text(TTR("Open the folder containing installed templates for the current version."));
 	current_installed_hb->add_child(current_open_button);
 	current_open_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_folder).bind(VERSION_FULL_CONFIG));
+#endif
 
 	current_uninstall_button = memnew(Button);
 	current_uninstall_button->set_text(TTR("Uninstall"));

+ 0 - 1
editor/export/export_template_manager.h

@@ -58,7 +58,6 @@ class ExportTemplateManager : public AcceptDialog {
 
 	HBoxContainer *current_installed_hb = nullptr;
 	LineEdit *current_installed_path = nullptr;
-	Button *current_open_button = nullptr;
 	Button *current_uninstall_button = nullptr;
 
 	VBoxContainer *install_options_vb = nullptr;

+ 1 - 13
editor/export/project_export.cpp

@@ -1147,10 +1147,8 @@ void ProjectExportDialog::_export_project_to_path(const String &p_path) {
 }
 
 void ProjectExportDialog::_export_all_dialog() {
-#ifndef ANDROID_ENABLED
 	export_all_dialog->show();
 	export_all_dialog->popup_centered(Size2(300, 80));
-#endif
 }
 
 void ProjectExportDialog::_export_all_dialog_action(const String &p_str) {
@@ -1491,13 +1489,9 @@ ProjectExportDialog::ProjectExportDialog() {
 	set_ok_button_text(TTR("Export PCK/ZIP..."));
 	get_ok_button()->set_tooltip_text(TTR("Export the project resources as a PCK or ZIP package. This is not a playable build, only the project data without a Godot executable."));
 	get_ok_button()->set_disabled(true);
-#ifdef ANDROID_ENABLED
-	export_button = memnew(Button);
-	export_button->hide();
-#else
+
 	export_button = add_button(TTR("Export Project..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export");
 	export_button->set_tooltip_text(TTR("Export the project as a playable build (Godot executable and project data) for the selected preset."));
-#endif
 	export_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_project));
 	// Disable initially before we select a valid preset
 	export_button->set_disabled(true);
@@ -1510,14 +1504,8 @@ ProjectExportDialog::ProjectExportDialog() {
 	export_all_dialog->add_button(TTR("Debug"), true, "debug");
 	export_all_dialog->add_button(TTR("Release"), true, "release");
 	export_all_dialog->connect("custom_action", callable_mp(this, &ProjectExportDialog::_export_all_dialog_action));
-#ifdef ANDROID_ENABLED
-	export_all_dialog->hide();
 
-	export_all_button = memnew(Button);
-	export_all_button->hide();
-#else
 	export_all_button = add_button(TTR("Export All..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export");
-#endif
 	export_all_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_all_dialog));
 	export_all_button->set_disabled(true);
 

+ 7 - 3
platform/android/dir_access_jandroid.cpp

@@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) {
 	}
 }
 
-Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
+Error DirAccessJAndroid::make_dir(String p_dir) {
 	// Check if the directory exists already
 	if (dir_exists(p_dir)) {
 		return ERR_ALREADY_EXISTS;
@@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
 	}
 }
 
-Error DirAccessJAndroid::make_dir(String p_dir) {
-	return make_dir_recursive(p_dir);
+Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
+	Error err = make_dir(p_dir);
+	if (err != OK && err != ERR_ALREADY_EXISTS) {
+		ERR_FAIL_V_MSG(err, "Could not create directory: " + p_dir);
+	}
+	return OK;
 }
 
 Error DirAccessJAndroid::rename(String p_from, String p_to) {

+ 1 - 1
platform/android/dir_access_jandroid.h

@@ -84,7 +84,7 @@ public:
 
 	virtual bool is_link(String p_file) override { return false; }
 	virtual String read_link(String p_file) override { return p_file; }
-	virtual Error create_link(String p_source, String p_target) override { return FAILED; }
+	virtual Error create_link(String p_source, String p_target) override { return ERR_UNAVAILABLE; }
 
 	virtual uint64_t get_space_left() override;
 

+ 6 - 5
platform/android/export/export.cpp

@@ -42,16 +42,17 @@ void register_android_exporter_types() {
 }
 
 void register_android_exporter() {
-#ifndef ANDROID_ENABLED
-	EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
-	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
-	EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
-	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
 	EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
 	EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
 	EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));
+
+#ifndef ANDROID_ENABLED
+	EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
+	EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
 	EDITOR_DEF("export/android/force_system_user", false);
 
 	EDITOR_DEF("export/android/shutdown_adb_on_exit", true);

+ 64 - 28
platform/android/export/export_plugin.cpp

@@ -57,6 +57,10 @@
 #include "modules/svg/image_loader_svg.h"
 #endif
 
+#ifdef ANDROID_ENABLED
+#include "../os_android.h"
+#endif
+
 #include <string.h>
 
 static const char *android_perms[] = {
@@ -2417,6 +2421,10 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 			err += template_err;
 		}
 	} else {
+#ifdef ANDROID_ENABLED
+		err += TTR("Gradle build is not supported for the Android editor.") + "\n";
+		valid = false;
+#else
 		// Validate the custom gradle android source template.
 		bool android_source_template_valid = false;
 		const String android_source_template = p_preset->get("gradle_build/android_source_template");
@@ -2439,6 +2447,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 		}
 
 		valid = installed_android_build_template && !r_missing_templates;
+#endif
 	}
 
 	// Validate the rest of the export configuration.
@@ -2475,6 +2484,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 		err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
 	}
 
+#ifndef ANDROID_ENABLED
 	String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
 	if (java_sdk_path.is_empty()) {
 		err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
@@ -2547,6 +2557,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
 			valid = false;
 		}
 	}
+#endif
 
 	if (!err.is_empty()) {
 		r_error = err;
@@ -2717,23 +2728,9 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
 
 Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
 	int export_format = int(p_preset->get("gradle_build/export_format"));
-	String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
-	String release_keystore = _get_keystore_path(p_preset, false);
-	String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
-	String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
-	String target_sdk_version = p_preset->get("gradle_build/target_sdk");
-	if (!target_sdk_version.is_valid_int()) {
-		target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
-	}
-	String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
-	print_verbose("Starting signing of the " + export_label + " binary using " + apksigner);
-	if (apksigner == "<FAILED>") {
-		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting %s is unsigned."), export_label));
-		return OK;
-	}
-	if (!FileAccess::exists(apksigner)) {
-		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label));
-		return OK;
+	if (export_format == EXPORT_FORMAT_AAB) {
+		add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
+		return FAILED;
 	}
 
 	String keystore;
@@ -2750,15 +2747,15 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 			user = EDITOR_GET("export/android/debug_keystore_user");
 		}
 
-		if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) {
+		if (ep.step(TTR("Signing debug APK..."), 104)) {
 			return ERR_SKIP;
 		}
 	} else {
-		keystore = release_keystore;
-		password = release_password;
-		user = release_username;
+		keystore = _get_keystore_path(p_preset, false);
+		password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
+		user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
 
-		if (ep.step(vformat(TTR("Signing release %s..."), export_label), 104)) {
+		if (ep.step(TTR("Signing release APK..."), 104)) {
 			return ERR_SKIP;
 		}
 	}
@@ -2768,6 +2765,36 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 		return ERR_FILE_CANT_OPEN;
 	}
 
+	String apk_path = export_path;
+	if (apk_path.is_relative_path()) {
+		apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);
+	}
+	apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();
+
+	Error err;
+#ifdef ANDROID_ENABLED
+	err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);
+	if (err != OK) {
+		add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));
+		return err;
+	}
+#else
+	String target_sdk_version = p_preset->get("gradle_build/target_sdk");
+	if (!target_sdk_version.is_valid_int()) {
+		target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
+	}
+
+	String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
+	print_verbose("Starting signing of the APK binary using " + apksigner);
+	if (apksigner == "<FAILED>") {
+		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned."));
+		return OK;
+	}
+	if (!FileAccess::exists(apksigner)) {
+		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned."));
+		return OK;
+	}
+
 	String output;
 	List<String> args;
 	args.push_back("sign");
@@ -2778,7 +2805,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 	args.push_back("pass:" + password);
 	args.push_back("--ks-key-alias");
 	args.push_back(user);
-	args.push_back(export_path);
+	args.push_back(apk_path);
 	if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
 		// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
 		print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
@@ -2790,7 +2817,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 		print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
 	}
 	int retval;
-	Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+	err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
 	if (err != OK) {
 		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
 		return err;
@@ -2802,15 +2829,23 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
 		return ERR_CANT_CREATE;
 	}
+#endif
 
-	if (ep.step(vformat(TTR("Verifying %s..."), export_label), 105)) {
+	if (ep.step(TTR("Verifying APK..."), 105)) {
 		return ERR_SKIP;
 	}
 
+#ifdef ANDROID_ENABLED
+	err = OS_Android::get_singleton()->verify_apk(apk_path);
+	if (err != OK) {
+		add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));
+		return err;
+	}
+#else
 	args.clear();
 	args.push_back("verify");
 	args.push_back("--verbose");
-	args.push_back(export_path);
+	args.push_back(apk_path);
 	if (p_debug) {
 		print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
 	}
@@ -2823,10 +2858,11 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
 	}
 	print_verbose(output);
 	if (retval) {
-		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label));
+		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));
 		add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
 		return ERR_CANT_CREATE;
 	}
+#endif
 
 	print_verbose("Successfully completed signing build.");
 	return OK;
@@ -3319,7 +3355,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
 			src_apk = find_export_template("android_release.apk");
 		}
 		if (src_apk.is_empty()) {
-			add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk));
+			add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("%s export template not found: \"%s\"."), (p_debug ? "Debug" : "Release"), src_apk));
 			return ERR_FILE_NOT_FOUND;
 		}
 	}

+ 1 - 1
platform/android/file_access_android.h

@@ -86,7 +86,7 @@ public:
 
 	virtual uint64_t _get_modified_time(const String &p_file) override { return 0; }
 	virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
-	virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
+	virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
 
 	virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
 	virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }

+ 1 - 1
platform/android/file_access_filesystem_jandroid.h

@@ -101,7 +101,7 @@ public:
 
 	virtual uint64_t _get_modified_time(const String &p_file) override;
 	virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
-	virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
+	virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
 
 	virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
 	virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }

+ 15 - 13
platform/android/java/lib/THIRDPARTY.md → platform/android/java/THIRDPARTY.md

@@ -3,14 +3,6 @@
 This file list third-party libraries used in the Android source folder,
 with their provenance and, when relevant, modifications made to those files.
 
-## com.android.vending.billing
-
-- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main
-- Version: git (7a94c69, 2019)
-- License: Apache 2.0
-
-Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
-
 ## com.google.android.vending.expansion.downloader
 
 - Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
@@ -19,10 +11,10 @@ Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
 
 Overwrite all files under:
 
-- `src/com/google/android/vending/expansion/downloader`
+- `lib/src/com/google/android/vending/expansion/downloader`
 
 Some files have been modified for yet unclear reasons.
-See the `patches/com.google.android.vending.expansion.downloader.patch` file.
+See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file.
 
 ## com.google.android.vending.licensing
 
@@ -32,8 +24,18 @@ See the `patches/com.google.android.vending.expansion.downloader.patch` file.
 
 Overwrite all files under:
 
-- `aidl/com/android/vending/licensing`
-- `src/com/google/android/vending/licensing`
+- `lib/aidl/com/android/vending/licensing`
+- `lib/src/com/google/android/vending/licensing`
 
 Some files have been modified to silence linter errors or fix downstream issues.
-See the `patches/com.google.android.vending.licensing.patch` file.
+See the `lib/patches/com.google.android.vending.licensing.patch` file.
+
+## com.android.apksig
+
+- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
+- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024)
+- License: Apache 2.0
+
+Overwrite all files under:
+
+- `editor/src/main/java/com/android/apksig`

BIN
platform/android/java/editor/src/main/assets/keystores/debug.keystore


+ 17 - 1
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -50,6 +50,7 @@ import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsAnimationCompat
 import androidx.core.view.WindowInsetsCompat
 import com.google.android.vending.expansion.downloader.*
+import org.godotengine.godot.error.Error
 import org.godotengine.godot.input.GodotEditText
 import org.godotengine.godot.input.GodotInputHandler
 import org.godotengine.godot.io.directory.DirectoryAccessHandler
@@ -96,7 +97,6 @@ class Godot(private val context: Context) {
 		fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
 	}
 
-	private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
 	private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
 	private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
 	private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
@@ -1054,4 +1054,20 @@ class Godot(private val context: Context) {
 	private fun nativeDumpBenchmark(benchmarkFile: String) {
 		dumpBenchmark(fileAccessHandler, benchmarkFile)
 	}
+
+	@Keep
+	private fun nativeSignApk(inputPath: String,
+							  outputPath: String,
+							  keystorePath: String,
+							  keystoreUser: String,
+							  keystorePassword: String): Int {
+		val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE
+		return signResult.toNativeValue()
+	}
+
+	@Keep
+	private fun nativeVerifyApk(apkPath: String): Int {
+		val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
+		return verifyResult.toNativeValue()
+	}
 }

+ 17 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java

@@ -30,6 +30,7 @@
 
 package org.godotengine.godot;
 
+import org.godotengine.godot.error.Error;
 import org.godotengine.godot.plugin.GodotPlugin;
 import org.godotengine.godot.utils.BenchmarkUtils;
 
@@ -484,4 +485,20 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 		}
 		return Collections.emptySet();
 	}
+
+	@Override
+	public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
+		if (parentHost != null) {
+			return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword);
+		}
+		return Error.ERR_UNAVAILABLE;
+	}
+
+	@Override
+	public Error verifyApk(@NonNull String apkPath) {
+		if (parentHost != null) {
+			return parentHost.verifyApk(apkPath);
+		}
+		return Error.ERR_UNAVAILABLE;
+	}
 }

+ 28 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -30,10 +30,13 @@
 
 package org.godotengine.godot;
 
+import org.godotengine.godot.error.Error;
 import org.godotengine.godot.plugin.GodotPlugin;
 
 import android.app.Activity;
 
+import androidx.annotation.NonNull;
+
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -108,4 +111,29 @@ public interface GodotHost {
 	default Set<GodotPlugin> getHostPlugins(Godot engine) {
 		return Collections.emptySet();
 	}
+
+	/**
+	 * Signs the given Android apk
+	 *
+	 * @param inputPath Path to the apk that should be signed
+	 * @param outputPath Path for the signed output apk; can be the same as inputPath
+	 * @param keystorePath Path to the keystore to use for signing the apk
+	 * @param keystoreUser Keystore user credential
+	 * @param keystorePassword Keystore password credential
+	 *
+	 * @return {@link Error#OK} if signing is successful
+	 */
+	default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
+		return Error.ERR_UNAVAILABLE;
+	}
+
+	/**
+	 * Verifies the given Android apk is signed
+	 *
+	 * @param apkPath Path to the apk that should be verified
+	 * @return {@link Error#OK} if verification was successful
+	 */
+	default Error verifyApk(@NonNull String apkPath) {
+		return Error.ERR_UNAVAILABLE;
+	}
 }

+ 8 - 0
platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt

@@ -73,6 +73,14 @@ internal enum class StorageScope {
 		private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
 		private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
 
+		/**
+		 * Determine if the given path is accessible.
+		 */
+		fun canAccess(path: String?): Boolean {
+			val storageScope = identifyStorageScope(path)
+			return storageScope == APP || storageScope == SHARED
+		}
+
 		/**
 		 * Determines which [StorageScope] the given path falls under.
 		 */

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt

@@ -107,7 +107,7 @@ class FileAccessHandler(val context: Context) {
 		}
 	}
 
-	private val storageScopeIdentifier = StorageScope.Identifier(context)
+	internal val storageScopeIdentifier = StorageScope.Identifier(context)
 	private val files = SparseArray<DataAccess>()
 	private var lastFileId = STARTING_FILE_ID
 

+ 41 - 0
platform/android/java_godot_wrapper.cpp

@@ -84,6 +84,8 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
 	_get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;");
 	_has_feature = p_env->GetMethodID(godot_class, "hasFeature", "(Ljava/lang/String;)Z");
+	_sign_apk = p_env->GetMethodID(godot_class, "nativeSignApk", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I");
+	_verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
 }
 
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -424,3 +426,42 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const {
 		return false;
 	}
 }
+
+Error GodotJavaWrapper::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) {
+	if (_sign_apk) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+
+		jstring j_input_path = env->NewStringUTF(p_input_path.utf8().get_data());
+		jstring j_output_path = env->NewStringUTF(p_output_path.utf8().get_data());
+		jstring j_keystore_path = env->NewStringUTF(p_keystore_path.utf8().get_data());
+		jstring j_keystore_user = env->NewStringUTF(p_keystore_user.utf8().get_data());
+		jstring j_keystore_password = env->NewStringUTF(p_keystore_password.utf8().get_data());
+
+		int result = env->CallIntMethod(godot_instance, _sign_apk, j_input_path, j_output_path, j_keystore_path, j_keystore_user, j_keystore_password);
+
+		env->DeleteLocalRef(j_input_path);
+		env->DeleteLocalRef(j_output_path);
+		env->DeleteLocalRef(j_keystore_path);
+		env->DeleteLocalRef(j_keystore_user);
+		env->DeleteLocalRef(j_keystore_password);
+
+		return static_cast<Error>(result);
+	} else {
+		return ERR_UNCONFIGURED;
+	}
+}
+
+Error GodotJavaWrapper::verify_apk(const String &p_apk_path) {
+	if (_verify_apk) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+
+		jstring j_apk_path = env->NewStringUTF(p_apk_path.utf8().get_data());
+		int result = env->CallIntMethod(godot_instance, _verify_apk, j_apk_path);
+		env->DeleteLocalRef(j_apk_path);
+		return static_cast<Error>(result);
+	} else {
+		return ERR_UNCONFIGURED;
+	}
+}

+ 6 - 0
platform/android/java_godot_wrapper.h

@@ -75,6 +75,8 @@ private:
 	jmethodID _end_benchmark_measure = nullptr;
 	jmethodID _dump_benchmark = nullptr;
 	jmethodID _has_feature = nullptr;
+	jmethodID _sign_apk = nullptr;
+	jmethodID _verify_apk = nullptr;
 
 public:
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -116,6 +118,10 @@ public:
 
 	// Return true if the given feature is supported.
 	bool has_feature(const String &p_feature) const;
+
+	// Sign and verify apks
+	Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password);
+	Error verify_apk(const String &p_apk_path);
 };
 
 #endif // JAVA_GODOT_WRAPPER_H

+ 10 - 0
platform/android/os_android.cpp

@@ -775,6 +775,16 @@ void OS_Android::benchmark_dump() {
 #endif
 }
 
+#ifdef TOOLS_ENABLED
+Error OS_Android::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) {
+	return godot_java->sign_apk(p_input_path, p_output_path, p_keystore_path, p_keystore_user, p_keystore_password);
+}
+
+Error OS_Android::verify_apk(const String &p_apk_path) {
+	return godot_java->verify_apk(p_apk_path);
+}
+#endif
+
 bool OS_Android::_check_internal_feature_support(const String &p_feature) {
 	if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") {
 		return false;

+ 5 - 0
platform/android/os_android.h

@@ -91,6 +91,11 @@ public:
 	static const int DEFAULT_WINDOW_WIDTH = 800;
 	static const int DEFAULT_WINDOW_HEIGHT = 600;
 
+#ifdef TOOLS_ENABLED
+	Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password);
+	Error verify_apk(const String &p_apk_path);
+#endif
+
 	virtual void initialize_core() override;
 	virtual void initialize() override;
 

+ 0 - 2
platform/web/export/export.cpp

@@ -40,7 +40,6 @@ void register_web_exporter_types() {
 }
 
 void register_web_exporter() {
-#ifndef ANDROID_ENABLED
 	EDITOR_DEF("export/web/http_host", "localhost");
 	EDITOR_DEF("export/web/http_port", 8060);
 	EDITOR_DEF("export/web/use_tls", false);
@@ -49,7 +48,6 @@ void register_web_exporter() {
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1"));
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_key", PROPERTY_HINT_GLOBAL_FILE, "*.key"));
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem"));
-#endif
 
 	Ref<EditorExportPlatformWeb> platform;
 	platform.instantiate();