From 5593a0b2b27c9e015e711b235539043b6c5742d9 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Thu, 13 Mar 2025 08:04:15 -0500 Subject: [PATCH] Enable Gradle builds on the Android editor via a dedicated build app Co-authored-by: Logan Lang --- editor/editor_node.cpp | 2 +- .../export/android_editor_gradle_runner.cpp | 192 +++++++++++++++ .../export/android_editor_gradle_runner.h | 76 ++++++ platform/android/export/export_plugin.cpp | 75 ++++-- platform/android/export/export_plugin.h | 4 + .../java/editor/src/main/AndroidManifest.xml | 4 + .../org/godotengine/editor/BaseGodotEditor.kt | 13 +- .../GradleBuildEnvironmentClient.kt | 225 ++++++++++++++++++ .../buildprovider/GradleBuildProvider.kt | 95 ++++++++ .../org/godotengine/godot/BuildProvider.java | 81 +++++++ .../main/java/org/godotengine/godot/Godot.kt | 61 +++++ .../org/godotengine/godot/GodotFragment.java | 9 +- .../java/org/godotengine/godot/GodotHost.java | 9 + .../org/godotengine/godot/variant/Callable.kt | 2 +- platform/android/java_godot_wrapper.cpp | 88 +++++++ platform/android/java_godot_wrapper.h | 11 + 16 files changed, 928 insertions(+), 19 deletions(-) create mode 100644 platform/android/export/android_editor_gradle_runner.cpp create mode 100644 platform/android/export/android_editor_gradle_runner.h create mode 100644 platform/android/java/editor/src/main/java/org/godotengine/editor/buildprovider/GradleBuildEnvironmentClient.kt create mode 100644 platform/android/java/editor/src/main/java/org/godotengine/editor/buildprovider/GradleBuildProvider.kt create mode 100644 platform/android/java/lib/src/main/java/org/godotengine/godot/BuildProvider.java diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 96673b3dba7..a9507827a58 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -8528,8 +8528,8 @@ EditorNode::EditorNode() { project_menu->add_separator(); project_menu->add_shortcut(ED_SHORTCUT_AND_COMMAND("editor/export", TTRC("Export..."), Key::NONE, TTRC("Export")), PROJECT_EXPORT); project_menu->add_item(TTRC("Pack Project as ZIP..."), PROJECT_PACK_AS_ZIP); -#ifndef ANDROID_ENABLED project_menu->add_item(TTRC("Install Android Build Template..."), PROJECT_INSTALL_ANDROID_SOURCE); +#ifndef ANDROID_ENABLED project_menu->add_item(TTRC("Open User Data Folder"), PROJECT_OPEN_USER_DATA_FOLDER); #endif diff --git a/platform/android/export/android_editor_gradle_runner.cpp b/platform/android/export/android_editor_gradle_runner.cpp new file mode 100644 index 00000000000..5a8e9224c1a --- /dev/null +++ b/platform/android/export/android_editor_gradle_runner.cpp @@ -0,0 +1,192 @@ +/**************************************************************************/ +/* android_editor_gradle_runner.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifdef ANDROID_ENABLED +#include "android_editor_gradle_runner.h" + +#include "editor/editor_interface.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/rich_text_label.h" + +#include "../java_godot_wrapper.h" +#include "../os_android.h" + +void AndroidEditorGradleRunner::run_gradle(const String &p_project_path, const String &p_build_path, const List &p_gradle_build_args, const List &p_gradle_copy_args) { + project_path = p_project_path; + build_path = p_build_path; + gradle_build_args = p_gradle_build_args; + gradle_copy_args = p_gradle_copy_args; + + if (output_dialog == nullptr) { + output_label = memnew(RichTextLabel); + output_label->set_selection_enabled(true); + output_label->set_context_menu_enabled(true); + output_label->set_scroll_follow(true); + + output_dialog = memnew(ConfirmationDialog); + output_dialog->set_unparent_when_invisible(true); + output_dialog->set_title(TTR("Building Android Project (gradle)")); + output_dialog->add_child(output_label); + + output_dialog->connect("canceled", callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_cancel)); + } + + output_label->clear(); + output_dialog->get_ok_button()->set_disabled(true); + + EditorInterface::get_singleton()->popup_dialog_centered_ratio(output_dialog); + + state = STATE_BUILDING; + _android_gradle_build_connect(); +} + +void AndroidEditorGradleRunner::_android_gradle_build_connect() { + _android_gradle_build_output(0, TTR("> Connecting to Gradle Build Environment...")); + + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + if (!godot_java->build_env_connect(callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_build))) { + _android_gradle_build_failed(TTR("Unable to connect to Gradle Build Environment service")); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_disconnect() { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + godot_java->build_env_disconnect(); +} + +void AndroidEditorGradleRunner::_android_gradle_build_output(int p_type, const String &p_line) { + if (p_type == 0) { + print_line(p_line); + output_label->append_text("[color=green]" + p_line + "[/color]\n"); + } else if (p_type == 1) { + print_line(p_line); + output_label->add_text(p_line + "\n"); + } else { + print_error(p_line); + output_label->append_text("[color=red]" + p_line + "[/color]\n"); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_build() { + _android_gradle_build_output(0, TTR("> Starting Gradle build...")); + + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + job_id = godot_java->build_env_execute( + "gradle", + gradle_build_args, + project_path, + build_path, + callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_output), + callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_build_callback)); + if (job_id < 0) { + _android_gradle_build_failed(TTR("Failed to execute Gradle command")); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_build_callback(int p_exit_code) { + job_id = -1; + if (p_exit_code != 0) { + _android_gradle_build_failed(); + return; + } + + _android_gradle_build_copy(); +} + +void AndroidEditorGradleRunner::_android_gradle_build_copy() { + _android_gradle_build_output(0, TTR("> Copying Gradle artifacts...")); + + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + job_id = godot_java->build_env_execute( + "gradle", + gradle_copy_args, + project_path, + build_path, + callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_output), + callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_copy_callback)); + if (job_id < 0) { + _android_gradle_build_failed(TTR("Failed to execute Gradle command")); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_copy_callback(int p_exit_code) { + job_id = -1; + if (p_exit_code != 0) { + _android_gradle_build_failed(); + } else { + _android_gradle_build_clean_project(true); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_clean_project(bool p_was_successful) { + if (state != STATE_CLEANING) { + state = STATE_CLEANING; + + if (p_was_successful) { + output_dialog->hide(); + } else { + output_dialog->get_ok_button()->set_disabled(false); + } + + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + godot_java->build_env_clean_project( + project_path, + build_path, + callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_clean_project_callback)); + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_clean_project_callback() { + // Ensure we haven't switched back to STATE_BUILDING in the meantime. + if (state == STATE_CLEANING) { + _android_gradle_build_disconnect(); + state = STATE_IDLE; + } +} + +void AndroidEditorGradleRunner::_android_gradle_build_failed(const String &p_msg) { + job_id = -1; + + if (p_msg != "") { + _android_gradle_build_output(1, p_msg); + } + + _android_gradle_build_clean_project(false); +} + +void AndroidEditorGradleRunner::_android_gradle_build_cancel() { + if (job_id > 0) { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + godot_java->build_env_cancel(job_id); + _android_gradle_build_clean_project(false); + } +} + +#endif // ANDROID_ENABLED diff --git a/platform/android/export/android_editor_gradle_runner.h b/platform/android/export/android_editor_gradle_runner.h new file mode 100644 index 00000000000..21334eb4c45 --- /dev/null +++ b/platform/android/export/android_editor_gradle_runner.h @@ -0,0 +1,76 @@ +/**************************************************************************/ +/* android_editor_gradle_runner.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#ifdef ANDROID_ENABLED + +#include "core/object/object.h" + +class ConfirmationDialog; +class RichTextLabel; + +class AndroidEditorGradleRunner : public Object { + GDCLASS(AndroidEditorGradleRunner, Object); + + RichTextLabel *output_label = nullptr; + ConfirmationDialog *output_dialog = nullptr; + + enum State { + STATE_IDLE, + STATE_BUILDING, + STATE_CLEANING, + }; + State state = STATE_IDLE; + + String project_path; + String build_path; + List gradle_build_args; + List gradle_copy_args; + int64_t job_id; + + void _android_gradle_build_connect(); + void _android_gradle_build_disconnect(); + void _android_gradle_build_output(int p_type, const String &p_line); + void _android_gradle_build_build(); + void _android_gradle_build_build_callback(int p_exit_code); + void _android_gradle_build_copy(); + void _android_gradle_build_copy_callback(int p_exit_code); + void _android_gradle_build_clean_project(bool p_was_successful); + void _android_gradle_build_clean_project_callback(); + + void _android_gradle_build_failed(const String &p_msg = String()); + void _android_gradle_build_cancel(); + +public: + void run_gradle(const String &p_project_path, const String &p_build_path, const List &p_gradle_build_args, const List &p_gradle_copy_args); +}; + +#endif // ANDROID_ENABLED diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index e7444d904ac..8c4c94885fd 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -60,7 +60,9 @@ #endif #ifdef ANDROID_ENABLED +#include "../java_godot_wrapper.h" #include "../os_android.h" +#include "android_editor_gradle_runner.h" #endif static const char *ANDROID_PERMS[] = { @@ -2015,6 +2017,11 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport if (!enabled_deprecated_plugins_names.is_empty() && !gradle_build_enabled) { return TTR("\"Use Gradle Build\" must be enabled to use the plugins."); } +#ifdef ANDROID_ENABLED + if (gradle_build_enabled) { + return TTR("Support for \"Use Gradle Build\" on Android is currently experimental."); + } +#endif // ANDROID_ENABLED } else if (p_name == "gradle_build/compress_native_libraries") { bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) { @@ -2100,7 +2107,7 @@ void EditorExportPlatformAndroid::get_export_options(List *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, true, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, false)); 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::BOOL, "gradle_build/compress_native_libraries"), false, false, true)); @@ -2889,10 +2896,6 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Refget("gradle_build/android_source_template"); @@ -2915,7 +2918,6 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Refget("package/name")); @@ -3738,14 +3742,17 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refset_environment("JAVA_HOME", java_sdk_path); print_verbose("Updating ANDROID_HOME environment to " + sdk_path); OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); +#endif String build_command; #ifdef WINDOWS_ENABLED @@ -3832,8 +3839,10 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refglobalize_path("res://addons"); +#ifndef ANDROID_ENABLED cmdline.push_back("-p"); // argument to specify the start directory. cmdline.push_back(build_path); // start directory. +#endif cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. @@ -3872,6 +3881,25 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref temp_file = FileAccess::open(temp_filename, FileAccess::WRITE); + if (temp_file.is_valid()) { + temp_file->store_buffer(keystore_data); + debug_keystore = temp_filename; + } + } + } +#endif cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file. cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias. @@ -3895,21 +3923,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refexecute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output); - if (result != 0) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output); - return ERR_CANT_CREATE; - } else { - print_verbose(build_project_output); - } - List copy_args; String copy_command = "copyAndRenameBinary"; copy_args.push_back(copy_command); +#ifndef ANDROID_ENABLED copy_args.push_back("-p"); // argument to specify the start directory. copy_args.push_back(build_path); // start directory. +#endif copy_args.push_back("-Pexport_edition=" + edition.to_lower()); @@ -3928,6 +3949,23 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refrun_gradle( + project_path, + build_path.substr(project_path.length()), + cmdline, + copy_args); +#else + String build_project_output; + int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output); + if (result != 0) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output); + return ERR_CANT_CREATE; + } else { + print_verbose(build_project_output); + } + print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" "))); String copy_binary_output; int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, ©_binary_output); @@ -3939,6 +3977,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refget_project_metadata("android", "use_scrcpy", false); } @@ -4349,5 +4390,9 @@ EditorExportPlatformAndroid::~EditorExportPlatformAndroid() { if (check_for_changes_thread.is_started()) { check_for_changes_thread.wait_to_finish(); } +#else + if (android_editor_gradle_runner) { + memdelete(android_editor_gradle_runner); + } #endif } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 8a95613d012..c27b7dd0ed3 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -60,6 +60,8 @@ struct LauncherIcon { int dimensions = 0; }; +class AndroidEditorGradleRunner; + class EditorExportPlatformAndroid : public EditorExportPlatform { GDCLASS(EditorExportPlatformAndroid, EditorExportPlatform); @@ -106,6 +108,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static void _check_for_changes_poll_thread(void *ud); void _update_preset_status(); +#else + AndroidEditorGradleRunner *android_editor_gradle_runner = nullptr; #endif String get_project_name(const Ref &p_preset, const String &p_name) const; diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index f47f6cb9638..cbe5a9d2a7f 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> + + + + { + this@GradleBuildEnvironmentClient.receiveCommandResult(msg) + } + MSG_COMMAND_OUTPUT -> { + this@GradleBuildEnvironmentClient.receiveCommandOutput(msg) + } + else -> super.handleMessage(msg) + } + } + } + private val incomingMessenger = Messenger(IncomingHandler()) + + private val connectionCallbacks = mutableListOf<() -> Unit>() + private var connecting = false + private var executionId = 1000 + + private class ExecutionInfo(val outputCallback: (Int, String) -> Unit, val resultCallback: (Int) -> Unit) + private val executionMap = HashMap() + + fun connect(callback: () -> Unit): Boolean { + if (bound) { + callback() + return true; + } + connectionCallbacks.add(callback) + if (connecting) { + return true; + } + connecting = true; + + val intent = Intent("org.godotengine.action.BUILD_PROVIDER").apply { + setPackage("org.godotengine.godot_gradle_build_environment") + } + val info = context.packageManager.resolveService(intent, 0) + if (info == null) { + connecting = false; + Log.e(TAG, "Unable to resolve service") + return false + } + + val result = context.bindService(intent, connection, Context.BIND_AUTO_CREATE) + if (!result) { + Log.e(TAG, "Unable to bind to service") + connecting = false; + } + return result; + } + + fun disconnect() { + if (bound) { + context.unbindService(connection) + bound = false + } + } + + private fun getNextExecutionId(outputCallback: (Int, String) -> Unit, resultCallback: (Int) -> Unit): Int { + val id = executionId++ + executionMap[id] = ExecutionInfo(outputCallback, resultCallback) + return id + } + + fun execute(arguments: Array, projectPath: String, gradleBuildDir: String, outputCallback: (Int, String) -> Unit, resultCallback: (Int) -> Unit): Int { + if (outgoingMessenger == null) { + return -1 + } + + val msg: Message = Message.obtain(null, MSG_EXECUTE_GRADLE, getNextExecutionId(outputCallback, resultCallback),0) + msg.replyTo = incomingMessenger + + val data = Bundle() + data.putStringArrayList("arguments", ArrayList(arguments.toList())) + data.putString("project_path", projectPath) + data.putString("gradle_build_directory", gradleBuildDir) + msg.data = data + + try { + outgoingMessenger?.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to execute Gradle command: gradlew ${arguments.joinToString(" ")}", e) + e.printStackTrace() + executionMap.remove(msg.arg1) + resultCallback(255) + return -1 + } + + return msg.arg1 + } + + private fun receiveCommandResult(msg: Message) { + val executionInfo = executionMap.remove(msg.arg1) + executionInfo?.resultCallback?.invoke(msg.arg2) + } + + private fun receiveCommandOutput(msg: Message) { + val data = msg.data + val line = data.getString("line") + + if (line != null) { + val executionInfo = executionMap.get(msg.arg1) + executionInfo?.outputCallback?.invoke(msg.arg2, line) + } + } + + fun cancel(jobId: Int) { + if (outgoingMessenger == null) { + return + } + + val msg: Message = Message.obtain(null, MSG_CANCEL_COMMAND, jobId, 0) + try { + outgoingMessenger?.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to cancel Gradle command: ${jobId}", e) + e.printStackTrace() + } + } + + fun cleanProject(projectPath: String, gradleBuildDir: String, resultCallback: (Int) -> Unit) { + if (outgoingMessenger == null) { + return + } + + val emptyOutputCallback: (Int, String) -> Unit = { outputType, line -> } + + val msg: Message = Message.obtain(null, MSG_CLEAN_PROJECT, getNextExecutionId(emptyOutputCallback, resultCallback), 0) + msg.replyTo = incomingMessenger + + val data = Bundle() + data.putString("project_path", projectPath) + data.putString("gradle_build_directory", gradleBuildDir) + msg.data = data + + try { + outgoingMessenger?.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to clean Gradle project", e) + executionMap.remove(msg.arg1) + resultCallback(0) + e.printStackTrace() + } + } + +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/buildprovider/GradleBuildProvider.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/buildprovider/GradleBuildProvider.kt new file mode 100644 index 00000000000..cf9d9056854 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/buildprovider/GradleBuildProvider.kt @@ -0,0 +1,95 @@ +/**************************************************************************/ +/* GradleBuildProvider.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.editor.buildprovider + +import android.content.Context +import org.godotengine.godot.BuildProvider +import org.godotengine.godot.GodotHost +import org.godotengine.godot.variant.Callable + +internal class GradleBuildProvider( + val context: Context, + val host: GodotHost, +) : BuildProvider { + + val gradleBuildEnvironmentClient = GradleBuildEnvironmentClient(context) + + val godot get() = host.godot + + override fun buildEnvConnect(callback: Callable): Boolean { + return gradleBuildEnvironmentClient.connect { + godot?.runOnRenderThread { + callback.call() + } + } + } + + override fun buildEnvDisconnect() { + gradleBuildEnvironmentClient.disconnect() + } + + override fun buildEnvExecute( + buildTool: String, + arguments: Array, + projectPath: String, + buildDir: String, + outputCallback: Callable, + resultCallback: Callable + ): Int { + if (buildTool != "gradle") { + return -1; + } + val outputCb: (Int, String) -> Unit = { outputType, line -> + godot?.runOnRenderThread { + outputCallback.call(outputType, line) + } + } + val resultCb: (Int) -> Unit = { exitCode -> + godot?.runOnRenderThread { + resultCallback.call(exitCode) + } + } + return gradleBuildEnvironmentClient.execute(arguments, projectPath, buildDir, outputCb, resultCb) + } + + override fun buildEnvCancel(jobId: Int) { + gradleBuildEnvironmentClient.cancel(jobId) + } + + override fun buildEnvCleanProject(projectPath: String, buildDir: String, callback: Callable) { + val cb: (Int) -> Unit = { exitCode -> + godot?.runOnRenderThread { + callback.call() + } + } + gradleBuildEnvironmentClient.cleanProject(projectPath, buildDir, cb) + } +} diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/BuildProvider.java b/platform/android/java/lib/src/main/java/org/godotengine/godot/BuildProvider.java new file mode 100644 index 00000000000..113caff9faf --- /dev/null +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/BuildProvider.java @@ -0,0 +1,81 @@ +/**************************************************************************/ +/* BuildProvider.java */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot; + +import org.godotengine.godot.variant.Callable; + +import androidx.annotation.NonNull; + +/** + * Provides an environment for executing build commands. + */ +public interface BuildProvider { + /** + * Connects to the build environment. + * + * @param callback The callback to call when connected + * @return Whether or not connecting is possible + */ + boolean buildEnvConnect(@NonNull Callable callback); + + /** + * Disconnects from the build environment. + */ + void buildEnvDisconnect(); + + /** + * Executes a command via the build environment. + * + * @param buildTool The build tool to execute (for example, "gradle") + * @param arguments The argument for the command + * @param projectPath The working directory to use when executing the command + * @param buildDir The build directory within the project + * @param outputCallback The callback to call for each line of output from the command + * @param resultCallback The callback to call when the command is finished running + * @return A positive job id, if successful; otherwise, a negative number + */ + int buildEnvExecute(String buildTool, @NonNull String[] arguments, @NonNull String projectPath, @NonNull String buildDir, @NonNull Callable outputCallback, @NonNull Callable resultCallback); + + /** + * Cancels a command executed via the build environment. + * + * @param jobId The job id returned from buildEnvExecute() + */ + void buildEnvCancel(int jobId); + + /** + * Requests that a project be cleaned up via the build environment. + * + * @param projectPath The working directory to use when executing the command + * @param buildDir The build directory within the project + */ + void buildEnvCleanProject(@NonNull String projectPath, @NonNull String buildDir, @NonNull Callable callback); +} diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt index 0b25611a365..3dadef0641f 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/Godot.kt @@ -75,6 +75,7 @@ import org.godotengine.godot.utils.benchmarkFile import org.godotengine.godot.utils.dumpBenchmark import org.godotengine.godot.utils.endBenchmarkMeasure import org.godotengine.godot.utils.useBenchmark +import org.godotengine.godot.variant.Callable as GodotCallable import org.godotengine.godot.xr.XRMode import java.io.File import java.io.FileInputStream @@ -1304,4 +1305,64 @@ class Godot private constructor(val context: Context) { private fun nativeOnEditorWorkspaceSelected(workspace: String) { primaryHost?.onEditorWorkspaceSelected(workspace) } + + @Keep + private fun nativeBuildEnvConnect(callback: GodotCallable): Boolean { + try { + val buildProvider = primaryHost?.getBuildProvider() + return buildProvider?.buildEnvConnect(callback) ?: false + } catch (e: Exception) { + Log.e(TAG, "Unable to connect to build environment", e) + return false + } + } + + @Keep + private fun nativeBuildEnvDisconnect() { + try { + val buildProvider = primaryHost?.getBuildProvider() + buildProvider?.buildEnvDisconnect() + } catch (e: Exception) { + Log.e(TAG, "Unable to disconnect from build environment", e) + } + } + + @Keep + private fun nativeBuildEnvExecute(buildTool: String, arguments: Array, projectPath: String, buildDir: String, outputCallback: GodotCallable, resultCallback: GodotCallable): Int { + try { + val buildProvider = primaryHost?.getBuildProvider() + return buildProvider?.buildEnvExecute( + buildTool, + arguments, + projectPath, + buildDir, + outputCallback, + resultCallback + ) ?: -1 + } catch (e: Exception) { + Log.e(TAG, "Unable to execute Gradle command in build environment", e); + return -1 + } + } + + @Keep + private fun nativeBuildEnvCancel(jobId: Int) { + try { + val buildProvider = primaryHost?.getBuildProvider() + buildProvider?.buildEnvCancel(jobId) + } catch (e: Exception) { + Log.e(TAG, "Unable to cancel command in build environment", e) + } + } + + @Keep + private fun nativeBuildEnvCleanProject(projectPath: String, buildDir: String, callback: GodotCallable) { + try { + val buildProvider = primaryHost?.getBuildProvider() + buildProvider?.buildEnvCleanProject(projectPath, buildDir, callback) + } catch(e: Exception) { + Log.e(TAG, "Unable to clean project in build environment", e) + } + } + } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotFragment.java index 42f32e93576..1ecbc2acb52 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotFragment.java @@ -40,7 +40,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.os.Build; import android.os.Bundle; import android.os.Messenger; import android.text.TextUtils; @@ -496,4 +495,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH parentHost.onEditorWorkspaceSelected(workspace); } } + + @Override + public BuildProvider getBuildProvider() { + if (parentHost != null) { + return parentHost.getBuildProvider(); + } + return null; + } } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotHost.java index 466da4465a6..e8be6e886a8 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotHost.java @@ -166,4 +166,13 @@ public interface GodotHost { activity.runOnUiThread(action); } } + + /** + * Gets the build provider, if available. + * + * @return the build provider, if available; otherwise, null. + */ + default @Nullable BuildProvider getBuildProvider() { + return null; + } } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/variant/Callable.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/variant/Callable.kt index b658106ab91..845e8abb743 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/variant/Callable.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/variant/Callable.kt @@ -71,7 +71,7 @@ class Callable private constructor(private val nativeCallablePointer: Long) { /** * Calls the method represented by this [Callable]. Arguments can be passed and should match the method's signature. */ - internal fun call(vararg params: Any): Any? { + fun call(vararg params: Any): Any? { if (nativeCallablePointer == 0L) { return null } diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 13d7ed21784..0eb9d12fdfd 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -30,6 +30,8 @@ #include "java_godot_wrapper.h" +#include "jni_utils.h" + // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. // For Godot we call most access methods from our thread and we thus get a valid JNIEnv @@ -88,6 +90,11 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) { _set_window_color = p_env->GetMethodID(godot_class, "setWindowColor", "(Ljava/lang/String;)V"); _on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V"); _get_activity = p_env->GetMethodID(godot_class, "getActivity", "()Landroid/app/Activity;"); + _build_env_connect = p_env->GetMethodID(godot_class, "nativeBuildEnvConnect", "(Lorg/godotengine/godot/variant/Callable;)Z"); + _build_env_disconnect = p_env->GetMethodID(godot_class, "nativeBuildEnvDisconnect", "()V"); + _build_env_execute = p_env->GetMethodID(godot_class, "nativeBuildEnvExecute", "(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;Lorg/godotengine/godot/variant/Callable;)I"); + _build_env_cancel = p_env->GetMethodID(godot_class, "nativeBuildEnvCancel", "(I)V"); + _build_env_clean_project = p_env->GetMethodID(godot_class, "nativeBuildEnvCleanProject", "(Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;)V"); } GodotJavaWrapper::~GodotJavaWrapper() { @@ -607,3 +614,84 @@ void GodotJavaWrapper::on_editor_workspace_selected(const String &p_workspace) { env->CallVoidMethod(godot_instance, _on_editor_workspace_selected, j_workspace); } } + +bool GodotJavaWrapper::build_env_connect(const Callable &p_callback) { + if (_build_env_connect) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + + jobject j_callback = callable_to_jcallable(env, p_callback); + jboolean result = env->CallBooleanMethod(godot_instance, _build_env_connect, j_callback); + env->DeleteLocalRef(j_callback); + + return result; + } + + return false; +} + +void GodotJavaWrapper::build_env_disconnect() { + if (_build_env_disconnect) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->CallVoidMethod(godot_instance, _build_env_disconnect); + } +} + +int GodotJavaWrapper::build_env_execute(const String &p_build_tool, const List &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback) { + if (_build_env_execute) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, -1); + + jstring j_build_tool = env->NewStringUTF(p_build_tool.utf8().get_data()); + jobjectArray j_args = env->NewObjectArray(p_arguments.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < p_arguments.size(); i++) { + jstring j_arg = env->NewStringUTF(p_arguments.get(i).utf8().get_data()); + env->SetObjectArrayElement(j_args, i, j_arg); + env->DeleteLocalRef(j_arg); + } + jstring j_project_path = env->NewStringUTF(p_project_path.utf8().get_data()); + jstring j_gradle_build_directory = env->NewStringUTF(p_gradle_build_directory.utf8().get_data()); + jobject j_output_callback = callable_to_jcallable(env, p_output_callback); + jobject j_result_callback = callable_to_jcallable(env, p_result_callback); + + jint result = env->CallIntMethod(godot_instance, _build_env_execute, j_build_tool, j_args, j_project_path, j_gradle_build_directory, j_output_callback, j_result_callback); + + env->DeleteLocalRef(j_build_tool); + env->DeleteLocalRef(j_args); + env->DeleteLocalRef(j_project_path); + env->DeleteLocalRef(j_gradle_build_directory); + env->DeleteLocalRef(j_output_callback); + env->DeleteLocalRef(j_result_callback); + + return result; + } + + return -1; +} + +void GodotJavaWrapper::build_env_cancel(int p_job_id) { + if (_build_env_cancel) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + env->CallVoidMethod(godot_instance, _build_env_cancel, p_job_id); + } +} + +void GodotJavaWrapper::build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback) { + if (_build_env_clean_project) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + jstring j_project_path = env->NewStringUTF(p_project_path.utf8().get_data()); + jstring j_gradle_build_directory = env->NewStringUTF(p_gradle_build_directory.utf8().get_data()); + jobject j_callback = callable_to_jcallable(env, p_callback); + + env->CallVoidMethod(godot_instance, _build_env_clean_project, j_project_path, j_gradle_build_directory, j_callback); + + env->DeleteLocalRef(j_project_path); + env->DeleteLocalRef(j_gradle_build_directory); + env->DeleteLocalRef(j_callback); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index e0db941f765..15367a1f333 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -84,6 +84,11 @@ private: jmethodID _set_window_color = nullptr; jmethodID _on_editor_workspace_selected = nullptr; jmethodID _get_activity = nullptr; + jmethodID _build_env_connect = nullptr; + jmethodID _build_env_disconnect = nullptr; + jmethodID _build_env_execute = nullptr; + jmethodID _build_env_cancel = nullptr; + jmethodID _build_env_clean_project = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance); @@ -141,4 +146,10 @@ public: void set_window_color(const Color &p_color); void on_editor_workspace_selected(const String &p_workspace); + + bool build_env_connect(const Callable &p_callback); + void build_env_disconnect(); + int build_env_execute(const String &p_build_tool, const List &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback); + void build_env_cancel(int p_job_id); + void build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback); };