1/**************************************************************************/
2/* gradle_export_util.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "gradle_export_util.h"
32
33#include "core/config/project_settings.h"
34
35int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {
36 switch (screen_orientation) {
37 case DisplayServer::SCREEN_PORTRAIT:
38 return 1;
39 case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
40 return 8;
41 case DisplayServer::SCREEN_REVERSE_PORTRAIT:
42 return 9;
43 case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
44 return 11;
45 case DisplayServer::SCREEN_SENSOR_PORTRAIT:
46 return 12;
47 case DisplayServer::SCREEN_SENSOR:
48 return 13;
49 case DisplayServer::SCREEN_LANDSCAPE:
50 default:
51 return 0;
52 }
53}
54
55String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation) {
56 switch (screen_orientation) {
57 case DisplayServer::SCREEN_PORTRAIT:
58 return "portrait";
59 case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
60 return "reverseLandscape";
61 case DisplayServer::SCREEN_REVERSE_PORTRAIT:
62 return "reversePortrait";
63 case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
64 return "userLandscape";
65 case DisplayServer::SCREEN_SENSOR_PORTRAIT:
66 return "userPortrait";
67 case DisplayServer::SCREEN_SENSOR:
68 return "fullUser";
69 case DisplayServer::SCREEN_LANDSCAPE:
70 default:
71 return "landscape";
72 }
73}
74
75int _get_app_category_value(int category_index) {
76 switch (category_index) {
77 case APP_CATEGORY_ACCESSIBILITY:
78 return 8;
79 case APP_CATEGORY_AUDIO:
80 return 1;
81 case APP_CATEGORY_IMAGE:
82 return 3;
83 case APP_CATEGORY_MAPS:
84 return 6;
85 case APP_CATEGORY_NEWS:
86 return 5;
87 case APP_CATEGORY_PRODUCTIVITY:
88 return 7;
89 case APP_CATEGORY_SOCIAL:
90 return 4;
91 case APP_CATEGORY_VIDEO:
92 return 2;
93 case APP_CATEGORY_GAME:
94 default:
95 return 0;
96 }
97}
98
99String _get_app_category_label(int category_index) {
100 switch (category_index) {
101 case APP_CATEGORY_ACCESSIBILITY:
102 return "accessibility";
103 case APP_CATEGORY_AUDIO:
104 return "audio";
105 case APP_CATEGORY_IMAGE:
106 return "image";
107 case APP_CATEGORY_MAPS:
108 return "maps";
109 case APP_CATEGORY_NEWS:
110 return "news";
111 case APP_CATEGORY_PRODUCTIVITY:
112 return "productivity";
113 case APP_CATEGORY_SOCIAL:
114 return "social";
115 case APP_CATEGORY_VIDEO:
116 return "video";
117 case APP_CATEGORY_GAME:
118 default:
119 return "game";
120 }
121}
122
123// Utility method used to create a directory.
124Error create_directory(const String &p_dir) {
125 if (!DirAccess::exists(p_dir)) {
126 Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
127 ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");
128 Error err = filesystem_da->make_dir_recursive(p_dir);
129 ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");
130 }
131 return OK;
132}
133
134// Writes p_data into a file at p_path, creating directories if necessary.
135// Note: this will overwrite the file at p_path if it already exists.
136Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) {
137 String dir = p_path.get_base_dir();
138 Error err = create_directory(dir);
139 if (err != OK) {
140 return err;
141 }
142 Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
143 ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
144 fa->store_buffer(p_data.ptr(), p_data.size());
145 return OK;
146}
147
148// Writes string p_data into a file at p_path, creating directories if necessary.
149// Note: this will overwrite the file at p_path if it already exists.
150Error store_string_at_path(const String &p_path, const String &p_data) {
151 String dir = p_path.get_base_dir();
152 Error err = create_directory(dir);
153 if (err != OK) {
154 if (OS::get_singleton()->is_stdout_verbose()) {
155 print_error("Unable to write data into " + p_path);
156 }
157 return err;
158 }
159 Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
160 ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
161 fa->store_string(p_data);
162 return OK;
163}
164
165// Implementation of EditorExportSaveFunction.
166// This method will only be called as an input to export_project_files.
167// It is used by the export_project_files method to save all the asset files into the gradle project.
168// It's functionality mirrors that of the method save_apk_file.
169// This method will be called ONLY when gradle build is enabled.
170Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
171 CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
172 String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/");
173 print_verbose("Saving project files from " + p_path + " into " + dst_path);
174 Error err = store_file_at_path(dst_path, p_data);
175 return err;
176}
177
178String _android_xml_escape(const String &p_string) {
179 // Android XML requires strings to be both valid XML (`xml_escape()`) but also
180 // to escape characters which are valid XML but have special meaning in Android XML.
181 // https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
182 // Note: Didn't handle U+XXXX unicode chars, could be done if needed.
183 return p_string
184 .replace("@", "\\@")
185 .replace("?", "\\?")
186 .replace("'", "\\'")
187 .replace("\"", "\\\"")
188 .replace("\n", "\\n")
189 .replace("\t", "\\t")
190 .xml_escape(false);
191}
192
193// Creates strings.xml files inside the gradle project for different locales.
194Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &project_name) {
195 print_verbose("Creating strings resources for supported locales for project " + project_name);
196 // Stores the string into the default values directory.
197 String processed_default_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(project_name));
198 store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string);
199
200 // Searches the Gradle project res/ directory to find all supported locales
201 Ref<DirAccess> da = DirAccess::open("res://android/build/res");
202 if (da.is_null()) {
203 if (OS::get_singleton()->is_stdout_verbose()) {
204 print_error("Unable to open Android resources directory.");
205 }
206 return ERR_CANT_OPEN;
207 }
208 da->list_dir_begin();
209 Dictionary appnames = GLOBAL_GET("application/config/name_localized");
210 while (true) {
211 String file = da->get_next();
212 if (file.is_empty()) {
213 break;
214 }
215 if (!file.begins_with("values-")) {
216 // NOTE: This assumes all directories that start with "values-" are for localization.
217 continue;
218 }
219 String locale = file.replace("values-", "").replace("-r", "_");
220 String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml";
221 if (appnames.has(locale)) {
222 String locale_project_name = appnames[locale];
223 String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name));
224 print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
225 store_string_at_path(locale_directory, processed_xml_string);
226 } else {
227 // TODO: Once the legacy build system is deprecated we don't need to have xml files for this else branch
228 store_string_at_path(locale_directory, processed_default_xml_string);
229 }
230 }
231 da->list_dir_end();
232 return OK;
233}
234
235String bool_to_string(bool v) {
236 return v ? "true" : "false";
237}
238
239String _get_gles_tag() {
240 return " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n";
241}
242
243String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) {
244 String manifest_screen_sizes = " <supports-screens \n tools:node=\"replace\"";
245 String sizes[] = { "small", "normal", "large", "xlarge" };
246 size_t num_sizes = sizeof(sizes) / sizeof(sizes[0]);
247 for (size_t i = 0; i < num_sizes; i++) {
248 String feature_name = vformat("screen/support_%s", sizes[i]);
249 String feature_support = bool_to_string(p_preset->get(feature_name));
250 String xml_entry = vformat("\n android:%sScreens=\"%s\"", sizes[i], feature_support);
251 manifest_screen_sizes += xml_entry;
252 }
253 manifest_screen_sizes += " />\n";
254 return manifest_screen_sizes;
255}
256
257String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) {
258 String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation"))));
259 String manifest_activity_text = vformat(
260 " <activity android:name=\"com.godot.game.GodotApp\" "
261 "tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" "
262 "tools:node=\"mergeOnlyAttributes\" "
263 "android:excludeFromRecents=\"%s\" "
264 "android:screenOrientation=\"%s\" "
265 "android:resizeableActivity=\"%s\">\n",
266 bool_to_string(p_preset->get("package/exclude_from_recents")),
267 orientation,
268 bool_to_string(bool(GLOBAL_GET("display/window/size/resizable"))));
269
270 manifest_activity_text += " <intent-filter>\n"
271 " <action android:name=\"android.intent.action.MAIN\" />\n"
272 " <category android:name=\"android.intent.category.DEFAULT\" />\n";
273
274 bool show_in_app_library = p_preset->get("package/show_in_app_library");
275 if (show_in_app_library) {
276 manifest_activity_text += " <category android:name=\"android.intent.category.LAUNCHER\" />\n";
277 }
278
279 bool uses_leanback_category = p_preset->get("package/show_in_android_tv");
280 if (uses_leanback_category) {
281 manifest_activity_text += " <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n";
282 }
283
284 bool uses_home_category = p_preset->get("package/show_as_launcher_app");
285 if (uses_home_category) {
286 manifest_activity_text += " <category android:name=\"android.intent.category.HOME\" />\n";
287 }
288
289 manifest_activity_text += " </intent-filter>\n";
290
291 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
292 for (int i = 0; i < export_plugins.size(); i++) {
293 if (export_plugins[i]->supports_platform(p_export_platform)) {
294 const String contents = export_plugins[i]->get_android_manifest_activity_element_contents(p_export_platform, p_debug);
295 if (!contents.is_empty()) {
296 manifest_activity_text += contents;
297 manifest_activity_text += "\n";
298 }
299 }
300 }
301
302 manifest_activity_text += " </activity>\n";
303 return manifest_activity_text;
304}
305
306String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug) {
307 int app_category_index = (int)(p_preset->get("package/app_category"));
308 bool is_game = app_category_index == APP_CATEGORY_GAME;
309
310 String manifest_application_text = vformat(
311 " <application android:label=\"@string/godot_project_name_string\"\n"
312 " android:allowBackup=\"%s\"\n"
313 " android:icon=\"@mipmap/icon\"\n"
314 " android:appCategory=\"%s\"\n"
315 " android:isGame=\"%s\"\n"
316 " android:hasFragileUserData=\"%s\"\n"
317 " android:requestLegacyExternalStorage=\"%s\"\n"
318 " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n"
319 " tools:ignore=\"GoogleAppIndexingWarning\">\n\n",
320 bool_to_string(p_preset->get("user_data_backup/allow")),
321 _get_app_category_label(app_category_index),
322 bool_to_string(is_game),
323 bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
324 bool_to_string(p_has_read_write_storage_permission));
325
326 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
327 for (int i = 0; i < export_plugins.size(); i++) {
328 if (export_plugins[i]->supports_platform(p_export_platform)) {
329 const String contents = export_plugins[i]->get_android_manifest_application_element_contents(p_export_platform, p_debug);
330 if (!contents.is_empty()) {
331 manifest_application_text += contents;
332 manifest_application_text += "\n";
333 }
334 }
335 }
336
337 manifest_application_text += _get_activity_tag(p_export_platform, p_preset, p_debug);
338 manifest_application_text += " </application>\n";
339 return manifest_application_text;
340}
341