Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/platform/android/export/gradle_export_util.cpp
10278 views
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
35
int _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
55
String _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
75
int _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_UNDEFINED:
92
return -1;
93
case APP_CATEGORY_VIDEO:
94
return 2;
95
case APP_CATEGORY_GAME:
96
default:
97
return 0;
98
}
99
}
100
101
String _get_app_category_label(int category_index) {
102
switch (category_index) {
103
case APP_CATEGORY_ACCESSIBILITY:
104
return "accessibility";
105
case APP_CATEGORY_AUDIO:
106
return "audio";
107
case APP_CATEGORY_IMAGE:
108
return "image";
109
case APP_CATEGORY_MAPS:
110
return "maps";
111
case APP_CATEGORY_NEWS:
112
return "news";
113
case APP_CATEGORY_PRODUCTIVITY:
114
return "productivity";
115
case APP_CATEGORY_SOCIAL:
116
return "social";
117
case APP_CATEGORY_VIDEO:
118
return "video";
119
case APP_CATEGORY_GAME:
120
default:
121
return "game";
122
}
123
}
124
125
// Utility method used to create a directory.
126
Error create_directory(const String &p_dir) {
127
if (!DirAccess::exists(p_dir)) {
128
Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
129
ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");
130
Error err = filesystem_da->make_dir_recursive(p_dir);
131
ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");
132
}
133
return OK;
134
}
135
136
// Writes p_data into a file at p_path, creating directories if necessary.
137
// Note: this will overwrite the file at p_path if it already exists.
138
Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) {
139
String dir = p_path.get_base_dir();
140
Error err = create_directory(dir);
141
if (err != OK) {
142
return err;
143
}
144
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
145
ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
146
fa->store_buffer(p_data.ptr(), p_data.size());
147
return OK;
148
}
149
150
// Writes string p_data into a file at p_path, creating directories if necessary.
151
// Note: this will overwrite the file at p_path if it already exists.
152
Error store_string_at_path(const String &p_path, const String &p_data) {
153
String dir = p_path.get_base_dir();
154
Error err = create_directory(dir);
155
if (err != OK) {
156
if (OS::get_singleton()->is_stdout_verbose()) {
157
print_error("Unable to write data into " + p_path);
158
}
159
return err;
160
}
161
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
162
ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
163
fa->store_string(p_data);
164
return OK;
165
}
166
167
// Implementation of EditorExportSaveFunction.
168
// This method will only be called as an input to export_project_files.
169
// It is used by the export_project_files method to save all the asset files into the gradle project.
170
// It's functionality mirrors that of the method save_apk_file.
171
// This method will be called ONLY when gradle build is enabled.
172
Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed) {
173
CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
174
175
String simplified_path = p_path.simplify_path();
176
if (simplified_path.begins_with("uid://")) {
177
simplified_path = ResourceUID::uid_to_path(simplified_path).simplify_path();
178
print_verbose(vformat(R"(UID referenced exported file name "%s" was replaced with "%s".)", p_path, simplified_path));
179
}
180
181
Vector<uint8_t> enc_data;
182
EditorExportPlatform::SavedData sd;
183
Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, enc_data, sd);
184
if (err != OK) {
185
return err;
186
}
187
188
const String dst_path = export_data->assets_directory + String("/") + simplified_path.trim_prefix("res://");
189
print_verbose("Saving project files from " + simplified_path + " into " + dst_path);
190
err = store_file_at_path(dst_path, enc_data);
191
192
export_data->pd.file_ofs.push_back(sd);
193
return err;
194
}
195
196
String _android_xml_escape(const String &p_string) {
197
// Android XML requires strings to be both valid XML (`xml_escape()`) but also
198
// to escape characters which are valid XML but have special meaning in Android XML.
199
// https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
200
// Note: Didn't handle U+XXXX unicode chars, could be done if needed.
201
return p_string
202
.replace("@", "\\@")
203
.replace("?", "\\?")
204
.replace("'", "\\'")
205
.replace("\"", "\\\"")
206
.replace("\n", "\\n")
207
.replace("\t", "\\t")
208
.xml_escape(false);
209
}
210
211
// Creates strings.xml files inside the gradle project for different locales.
212
Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &p_project_name, const String &p_gradle_build_dir, const Dictionary &p_appnames) {
213
print_verbose("Creating strings resources for supported locales for project " + p_project_name);
214
// Stores the string into the default values directory.
215
String processed_default_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(p_project_name));
216
store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string);
217
218
// Searches the Gradle project res/ directory to find all supported locales
219
Ref<DirAccess> da = DirAccess::open(p_gradle_build_dir.path_join("res"));
220
if (da.is_null()) {
221
if (OS::get_singleton()->is_stdout_verbose()) {
222
print_error("Unable to open Android resources directory.");
223
}
224
return ERR_CANT_OPEN;
225
}
226
da->list_dir_begin();
227
while (true) {
228
String file = da->get_next();
229
if (file.is_empty()) {
230
break;
231
}
232
if (!file.begins_with("values-")) {
233
// NOTE: This assumes all directories that start with "values-" are for localization.
234
continue;
235
}
236
String locale = file.replace("values-", "").replace("-r", "_");
237
String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");
238
if (p_appnames.has(locale)) {
239
String locale_project_name = p_appnames[locale];
240
String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name));
241
print_verbose("Storing project name for locale " + locale + " under " + locale_directory);
242
store_string_at_path(locale_directory, processed_xml_string);
243
} else {
244
// TODO: Once the legacy build system is deprecated we don't need to have xml files for this else branch
245
store_string_at_path(locale_directory, processed_default_xml_string);
246
}
247
}
248
da->list_dir_end();
249
return OK;
250
}
251
252
String bool_to_string(bool v) {
253
return v ? "true" : "false";
254
}
255
256
String _get_gles_tag() {
257
return " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n";
258
}
259
260
String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) {
261
String manifest_screen_sizes = " <supports-screens \n tools:node=\"replace\"";
262
String sizes[] = { "small", "normal", "large", "xlarge" };
263
constexpr size_t num_sizes = std::size(sizes);
264
for (size_t i = 0; i < num_sizes; i++) {
265
String feature_name = vformat("screen/support_%s", sizes[i]);
266
String feature_support = bool_to_string(p_preset->get(feature_name));
267
String xml_entry = vformat("\n android:%sScreens=\"%s\"", sizes[i], feature_support);
268
manifest_screen_sizes += xml_entry;
269
}
270
manifest_screen_sizes += " />\n";
271
return manifest_screen_sizes;
272
}
273
274
String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) {
275
String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(p_export_platform->get_project_setting(p_preset, "display/window/handheld/orientation"))));
276
String manifest_activity_text = vformat(
277
" <activity android:name=\".GodotApp\" "
278
"tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" "
279
"tools:node=\"mergeOnlyAttributes\" "
280
"android:excludeFromRecents=\"%s\" "
281
"android:screenOrientation=\"%s\" "
282
"android:resizeableActivity=\"%s\">\n",
283
bool_to_string(p_preset->get("package/exclude_from_recents")),
284
orientation,
285
bool_to_string(bool(p_export_platform->get_project_setting(p_preset, "display/window/size/resizable"))));
286
287
manifest_activity_text += " <intent-filter>\n"
288
" <action android:name=\"android.intent.action.MAIN\" />\n"
289
" <category android:name=\"android.intent.category.DEFAULT\" />\n";
290
291
bool show_in_app_library = p_preset->get("package/show_in_app_library");
292
if (show_in_app_library) {
293
manifest_activity_text += " <category android:name=\"android.intent.category.LAUNCHER\" />\n";
294
}
295
296
bool uses_leanback_category = p_preset->get("package/show_in_android_tv");
297
if (uses_leanback_category) {
298
manifest_activity_text += " <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n";
299
}
300
301
bool uses_home_category = p_preset->get("package/show_as_launcher_app");
302
if (uses_home_category) {
303
manifest_activity_text += " <category android:name=\"android.intent.category.HOME\" />\n";
304
}
305
306
manifest_activity_text += " </intent-filter>\n";
307
308
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
309
for (int i = 0; i < export_plugins.size(); i++) {
310
if (export_plugins[i]->supports_platform(p_export_platform)) {
311
const String contents = export_plugins[i]->get_android_manifest_activity_element_contents(p_export_platform, p_debug);
312
if (!contents.is_empty()) {
313
manifest_activity_text += contents;
314
manifest_activity_text += "\n";
315
}
316
}
317
}
318
319
manifest_activity_text += " </activity>\n";
320
return manifest_activity_text;
321
}
322
323
String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug, const Vector<MetadataInfo> &p_metadata) {
324
int app_category_index = (int)(p_preset->get("package/app_category"));
325
bool is_game = app_category_index == APP_CATEGORY_GAME;
326
327
String manifest_application_text = vformat(
328
" <application android:label=\"@string/godot_project_name_string\"\n"
329
" android:allowBackup=\"%s\"\n"
330
" android:icon=\"@mipmap/icon\"\n"
331
" android:isGame=\"%s\"\n"
332
" android:hasFragileUserData=\"%s\"\n"
333
" android:requestLegacyExternalStorage=\"%s\"\n",
334
bool_to_string(p_preset->get("user_data_backup/allow")),
335
bool_to_string(is_game),
336
bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
337
bool_to_string(p_has_read_write_storage_permission));
338
if (app_category_index != APP_CATEGORY_UNDEFINED) {
339
manifest_application_text += vformat(" android:appCategory=\"%s\"\n", _get_app_category_label(app_category_index));
340
manifest_application_text += " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
341
} else {
342
manifest_application_text += " tools:remove=\"android:appCategory\"\n";
343
manifest_application_text += " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";
344
}
345
manifest_application_text += " tools:ignore=\"GoogleAppIndexingWarning\">\n\n";
346
347
for (int i = 0; i < p_metadata.size(); i++) {
348
manifest_application_text += vformat(" <meta-data tools:node=\"replace\" android:name=\"%s\" android:value=\"%s\" />\n", p_metadata[i].name, p_metadata[i].value);
349
}
350
351
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
352
for (int i = 0; i < export_plugins.size(); i++) {
353
if (export_plugins[i]->supports_platform(p_export_platform)) {
354
const String contents = export_plugins[i]->get_android_manifest_application_element_contents(p_export_platform, p_debug);
355
if (!contents.is_empty()) {
356
manifest_application_text += contents;
357
manifest_application_text += "\n";
358
}
359
}
360
}
361
362
manifest_application_text += _get_activity_tag(p_export_platform, p_preset, p_debug);
363
manifest_application_text += " </application>\n";
364
return manifest_application_text;
365
}
366
367
Error _store_temp_file(const String &p_simplified_path, const Vector<uint8_t> &p_data, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed, Vector<uint8_t> &r_enc_data, EditorExportPlatform::SavedData &r_sd) {
368
Error err = OK;
369
Ref<FileAccess> ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export", "tmp", false, &err);
370
if (err != OK) {
371
return err;
372
}
373
r_sd.path_utf8 = p_simplified_path.trim_prefix("res://").utf8();
374
r_sd.ofs = 0;
375
r_sd.size = p_data.size();
376
err = EditorExportPlatform::_encrypt_and_store_data(ftmp, p_simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, r_sd.encrypted);
377
if (err != OK) {
378
return err;
379
}
380
381
r_enc_data.resize(ftmp->get_length());
382
ftmp->seek(0);
383
ftmp->get_buffer(r_enc_data.ptrw(), r_enc_data.size());
384
ftmp.unref();
385
386
// Store MD5 of original file.
387
{
388
unsigned char hash[16];
389
CryptoCore::md5(p_data.ptr(), p_data.size(), hash);
390
r_sd.md5.resize(16);
391
for (int i = 0; i < 16; i++) {
392
r_sd.md5.write[i] = hash[i];
393
}
394
}
395
return OK;
396
}
397
398