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.
This commit is contained in:
Fredia Huya-Kouadio 2024-06-16 12:14:34 -07:00
parent 794ea99240
commit a5897d579b
26 changed files with 241 additions and 76 deletions

View File

@ -162,6 +162,7 @@ repos:
modules/gdscript/tests/scripts/parser/features/mixed_indentation_on_blank_lines\.gd$| 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_comment\.notest\.gd$|
modules/gdscript/tests/scripts/parser/warnings/empty_file_newline\.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/.* platform/android/java/lib/src/com/google/.*
) )

View File

@ -70,7 +70,8 @@ Copyright: 2020, Manuel Prandini
2007-2014, Juan Linietsky, Ariel Manzur 2007-2014, Juan Linietsky, Ariel Manzur
License: Expat 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/res/layout/status_bar_ongoing_event_progress_bar.xml
./platform/android/java/lib/src/com/google/android/* ./platform/android/java/lib/src/com/google/android/*
./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java ./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java

View File

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

View File

@ -71,7 +71,11 @@ String EditorPaths::get_export_templates_dir() const {
} }
String EditorPaths::get_debug_keystore_path() 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"); return get_data_dir().path_join("keystores/debug.keystore");
#endif
} }
String EditorPaths::get_project_settings_dir() const { String EditorPaths::get_project_settings_dir() const {

View File

@ -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 EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
bool valid = true; bool valid = true;
#ifndef ANDROID_ENABLED
String templates_error; String templates_error;
valid = valid && has_valid_export_configuration(p_preset, templates_error, r_missing_templates, p_debug); 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()) { if (!export_plugins_warning.is_empty()) {
r_error += export_plugins_warning; r_error += export_plugins_warning;
} }
#endif
String project_configuration_error; String project_configuration_error;
valid = valid && has_valid_project_configuration(p_preset, project_configuration_error); valid = valid && has_valid_project_configuration(p_preset, project_configuration_error);

View File

@ -111,7 +111,9 @@ void ExportTemplateManager::_update_template_status() {
TreeItem *ti = installed_table->create_item(installed_root); TreeItem *ti = installed_table->create_item(installed_root);
ti->set_text(0, version_string); 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.")); 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.")); 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_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
current_installed_hb->add_child(current_installed_path); 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_text(TTR("Open Folder"));
current_open_button->set_tooltip_text(TTR("Open the folder containing installed templates for the current version.")); 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_installed_hb->add_child(current_open_button);
current_open_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_folder).bind(VERSION_FULL_CONFIG)); 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 = memnew(Button);
current_uninstall_button->set_text(TTR("Uninstall")); current_uninstall_button->set_text(TTR("Uninstall"));

View File

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

View File

@ -1147,10 +1147,8 @@ void ProjectExportDialog::_export_project_to_path(const String &p_path) {
} }
void ProjectExportDialog::_export_all_dialog() { void ProjectExportDialog::_export_all_dialog() {
#ifndef ANDROID_ENABLED
export_all_dialog->show(); export_all_dialog->show();
export_all_dialog->popup_centered(Size2(300, 80)); export_all_dialog->popup_centered(Size2(300, 80));
#endif
} }
void ProjectExportDialog::_export_all_dialog_action(const String &p_str) { void ProjectExportDialog::_export_all_dialog_action(const String &p_str) {
@ -1491,13 +1489,9 @@ ProjectExportDialog::ProjectExportDialog() {
set_ok_button_text(TTR("Export PCK/ZIP...")); 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_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); 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 = 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.")); 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)); export_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_project));
// Disable initially before we select a valid preset // Disable initially before we select a valid preset
export_button->set_disabled(true); 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("Debug"), true, "debug");
export_all_dialog->add_button(TTR("Release"), true, "release"); export_all_dialog->add_button(TTR("Release"), true, "release");
export_all_dialog->connect("custom_action", callable_mp(this, &ProjectExportDialog::_export_all_dialog_action)); 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"); 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->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_all_dialog));
export_all_button->set_disabled(true); export_all_button->set_disabled(true);

View File

@ -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 // Check if the directory exists already
if (dir_exists(p_dir)) { if (dir_exists(p_dir)) {
return ERR_ALREADY_EXISTS; return ERR_ALREADY_EXISTS;
@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
} }
} }
Error DirAccessJAndroid::make_dir(String p_dir) { Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
return make_dir_recursive(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) { Error DirAccessJAndroid::rename(String p_from, String p_to) {

View File

@ -84,7 +84,7 @@ public:
virtual bool is_link(String p_file) override { return false; } virtual bool is_link(String p_file) override { return false; }
virtual String read_link(String p_file) override { return p_file; } 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; virtual uint64_t get_space_left() override;

View File

@ -42,16 +42,17 @@ void register_android_exporter_types() {
} }
void register_android_exporter() { 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()); 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")); 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_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD); 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)); 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/force_system_user", false);
EDITOR_DEF("export/android/shutdown_adb_on_exit", true); EDITOR_DEF("export/android/shutdown_adb_on_exit", true);

View File

@ -57,6 +57,10 @@
#include "modules/svg/image_loader_svg.h" #include "modules/svg/image_loader_svg.h"
#endif #endif
#ifdef ANDROID_ENABLED
#include "../os_android.h"
#endif
#include <string.h> #include <string.h>
static const char *android_perms[] = { static const char *android_perms[] = {
@ -2417,6 +2421,10 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += template_err; err += template_err;
} }
} else { } 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. // Validate the custom gradle android source template.
bool android_source_template_valid = false; bool android_source_template_valid = false;
const String android_source_template = p_preset->get("gradle_build/android_source_template"); 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; valid = installed_android_build_template && !r_missing_templates;
#endif
} }
// Validate the rest of the export configuration. // 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"; 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"); String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
if (java_sdk_path.is_empty()) { if (java_sdk_path.is_empty()) {
err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n"; 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; valid = false;
} }
} }
#endif
if (!err.is_empty()) { if (!err.is_empty()) {
r_error = err; 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) { 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")); int export_format = int(p_preset->get("gradle_build/export_format"));
String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; if (export_format == EXPORT_FORMAT_AAB) {
String release_keystore = _get_keystore_path(p_preset, false); add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); return FAILED;
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;
} }
String keystore; String keystore;
@ -2750,15 +2747,15 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
user = EDITOR_GET("export/android/debug_keystore_user"); 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; return ERR_SKIP;
} }
} else { } else {
keystore = release_keystore; keystore = _get_keystore_path(p_preset, false);
password = release_password; password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
user = release_username; 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; return ERR_SKIP;
} }
} }
@ -2768,6 +2765,36 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
return ERR_FILE_CANT_OPEN; 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; String output;
List<String> args; List<String> args;
args.push_back("sign"); 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("pass:" + password);
args.push_back("--ks-key-alias"); args.push_back("--ks-key-alias");
args.push_back(user); args.push_back(user);
args.push_back(export_path); args.push_back(apk_path);
if (OS::get_singleton()->is_stdout_verbose() && p_debug) { 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. // 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(" "))); 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(" "))); print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
} }
int retval; 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) { if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
return err; 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)); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE; 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; 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.clear();
args.push_back("verify"); args.push_back("verify");
args.push_back("--verbose"); args.push_back("--verbose");
args.push_back(export_path); args.push_back(apk_path);
if (p_debug) { if (p_debug) {
print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" "))); 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); print_verbose(output);
if (retval) { 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)); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE; return ERR_CANT_CREATE;
} }
#endif
print_verbose("Successfully completed signing build."); print_verbose("Successfully completed signing build.");
return OK; return OK;
@ -3319,7 +3355,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
src_apk = find_export_template("android_release.apk"); src_apk = find_export_template("android_release.apk");
} }
if (src_apk.is_empty()) { 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; return ERR_FILE_NOT_FOUND;
} }
} }

View File

@ -86,7 +86,7 @@ public:
virtual uint64_t _get_modified_time(const String &p_file) override { return 0; } 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 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 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; } virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }

View File

@ -101,7 +101,7 @@ public:
virtual uint64_t _get_modified_time(const String &p_file) override; 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 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 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; } virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }

View File

@ -3,14 +3,6 @@
This file list third-party libraries used in the Android source folder, This file list third-party libraries used in the Android source folder,
with their provenance and, when relevant, modifications made to those files. 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 ## com.google.android.vending.expansion.downloader
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library - 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: 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. 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 ## com.google.android.vending.licensing
@ -32,8 +24,18 @@ See the `patches/com.google.android.vending.expansion.downloader.patch` file.
Overwrite all files under: Overwrite all files under:
- `aidl/com/android/vending/licensing` - `lib/aidl/com/android/vending/licensing`
- `src/com/google/android/vending/licensing` - `lib/src/com/google/android/vending/licensing`
Some files have been modified to silence linter errors or fix downstream issues. 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`

View File

@ -50,6 +50,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.google.android.vending.expansion.downloader.* import com.google.android.vending.expansion.downloader.*
import org.godotengine.godot.error.Error
import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.input.GodotInputHandler
import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler
@ -96,7 +97,6 @@ class Godot(private val context: Context) {
fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR 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 mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 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) { private fun nativeDumpBenchmark(benchmarkFile: String) {
dumpBenchmark(fileAccessHandler, benchmarkFile) 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()
}
} }

View File

@ -30,6 +30,7 @@
package org.godotengine.godot; package org.godotengine.godot;
import org.godotengine.godot.error.Error;
import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.utils.BenchmarkUtils; import org.godotengine.godot.utils.BenchmarkUtils;
@ -484,4 +485,20 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
} }
return Collections.emptySet(); 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;
}
} }

View File

@ -30,10 +30,13 @@
package org.godotengine.godot; package org.godotengine.godot;
import org.godotengine.godot.error.Error;
import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPlugin;
import android.app.Activity; import android.app.Activity;
import androidx.annotation.NonNull;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -108,4 +111,29 @@ public interface GodotHost {
default Set<GodotPlugin> getHostPlugins(Godot engine) { default Set<GodotPlugin> getHostPlugins(Godot engine) {
return Collections.emptySet(); 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;
}
} }

View File

@ -73,6 +73,14 @@ internal enum class StorageScope {
private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath private val downloadsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).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. * Determines which [StorageScope] the given path falls under.
*/ */

View File

@ -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 val files = SparseArray<DataAccess>()
private var lastFileId = STARTING_FILE_ID private var lastFileId = STARTING_FILE_ID

View File

@ -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"); _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;"); _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"); _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() { GodotJavaWrapper::~GodotJavaWrapper() {
@ -424,3 +426,42 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const {
return false; 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;
}
}

View File

@ -75,6 +75,8 @@ private:
jmethodID _end_benchmark_measure = nullptr; jmethodID _end_benchmark_measure = nullptr;
jmethodID _dump_benchmark = nullptr; jmethodID _dump_benchmark = nullptr;
jmethodID _has_feature = nullptr; jmethodID _has_feature = nullptr;
jmethodID _sign_apk = nullptr;
jmethodID _verify_apk = nullptr;
public: public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@ -116,6 +118,10 @@ public:
// Return true if the given feature is supported. // Return true if the given feature is supported.
bool has_feature(const String &p_feature) const; 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 #endif // JAVA_GODOT_WRAPPER_H

View File

@ -775,6 +775,16 @@ void OS_Android::benchmark_dump() {
#endif #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) { 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") { if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") {
return false; return false;

View File

@ -91,6 +91,11 @@ public:
static const int DEFAULT_WINDOW_WIDTH = 800; static const int DEFAULT_WINDOW_WIDTH = 800;
static const int DEFAULT_WINDOW_HEIGHT = 600; 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_core() override;
virtual void initialize() override; virtual void initialize() override;

View File

@ -40,7 +40,6 @@ void register_web_exporter_types() {
} }
void register_web_exporter() { void register_web_exporter() {
#ifndef ANDROID_ENABLED
EDITOR_DEF("export/web/http_host", "localhost"); EDITOR_DEF("export/web/http_host", "localhost");
EDITOR_DEF("export/web/http_port", 8060); EDITOR_DEF("export/web/http_port", 8060);
EDITOR_DEF("export/web/use_tls", false); 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::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_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")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem"));
#endif
Ref<EditorExportPlatformWeb> platform; Ref<EditorExportPlatformWeb> platform;
platform.instantiate(); platform.instantiate();