Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/platform/android/export/export_plugin.cpp
10278 views
1
/**************************************************************************/
2
/* export_plugin.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 "export_plugin.h"
32
33
#include "logo_svg.gen.h"
34
#include "run_icon_svg.gen.h"
35
36
#include "core/io/dir_access.h"
37
#include "core/io/file_access.h"
38
#include "core/io/image_loader.h"
39
#include "core/io/json.h"
40
#include "core/io/marshalls.h"
41
#include "core/version.h"
42
#include "drivers/png/png_driver_common.h"
43
#include "editor/editor_log.h"
44
#include "editor/editor_node.h"
45
#include "editor/export/export_template_manager.h"
46
#include "editor/file_system/editor_paths.h"
47
#include "editor/import/resource_importer_texture_settings.h"
48
#include "editor/settings/editor_settings.h"
49
#include "editor/themes/editor_scale.h"
50
#include "main/splash.gen.h"
51
#include "scene/resources/image_texture.h"
52
53
#include "modules/modules_enabled.gen.h" // For mono.
54
#include "modules/svg/image_loader_svg.h"
55
56
#ifdef MODULE_MONO_ENABLED
57
#include "modules/mono/utils/path_utils.h"
58
#endif
59
60
#ifdef ANDROID_ENABLED
61
#include "../os_android.h"
62
#endif
63
64
static const char *ANDROID_PERMS[] = {
65
"ACCESS_CHECKIN_PROPERTIES",
66
"ACCESS_COARSE_LOCATION",
67
"ACCESS_FINE_LOCATION",
68
"ACCESS_LOCATION_EXTRA_COMMANDS",
69
"ACCESS_MEDIA_LOCATION",
70
"ACCESS_MOCK_LOCATION",
71
"ACCESS_NETWORK_STATE",
72
"ACCESS_SURFACE_FLINGER",
73
"ACCESS_WIFI_STATE",
74
"ACCOUNT_MANAGER",
75
"ADD_VOICEMAIL",
76
"AUTHENTICATE_ACCOUNTS",
77
"BATTERY_STATS",
78
"BIND_ACCESSIBILITY_SERVICE",
79
"BIND_APPWIDGET",
80
"BIND_DEVICE_ADMIN",
81
"BIND_INPUT_METHOD",
82
"BIND_NFC_SERVICE",
83
"BIND_NOTIFICATION_LISTENER_SERVICE",
84
"BIND_PRINT_SERVICE",
85
"BIND_REMOTEVIEWS",
86
"BIND_TEXT_SERVICE",
87
"BIND_VPN_SERVICE",
88
"BIND_WALLPAPER",
89
"BLUETOOTH",
90
"BLUETOOTH_ADMIN",
91
"BLUETOOTH_PRIVILEGED",
92
"BRICK",
93
"BROADCAST_PACKAGE_REMOVED",
94
"BROADCAST_SMS",
95
"BROADCAST_STICKY",
96
"BROADCAST_WAP_PUSH",
97
"CALL_PHONE",
98
"CALL_PRIVILEGED",
99
"CAMERA",
100
"CAPTURE_AUDIO_OUTPUT",
101
"CAPTURE_SECURE_VIDEO_OUTPUT",
102
"CAPTURE_VIDEO_OUTPUT",
103
"CHANGE_COMPONENT_ENABLED_STATE",
104
"CHANGE_CONFIGURATION",
105
"CHANGE_NETWORK_STATE",
106
"CHANGE_WIFI_MULTICAST_STATE",
107
"CHANGE_WIFI_STATE",
108
"CLEAR_APP_CACHE",
109
"CLEAR_APP_USER_DATA",
110
"CONTROL_LOCATION_UPDATES",
111
"DELETE_CACHE_FILES",
112
"DELETE_PACKAGES",
113
"DEVICE_POWER",
114
"DIAGNOSTIC",
115
"DISABLE_KEYGUARD",
116
"DUMP",
117
"EXPAND_STATUS_BAR",
118
"FACTORY_TEST",
119
"FLASHLIGHT",
120
"FORCE_BACK",
121
"GET_ACCOUNTS",
122
"GET_PACKAGE_SIZE",
123
"GET_TASKS",
124
"GET_TOP_ACTIVITY_INFO",
125
"GLOBAL_SEARCH",
126
"HARDWARE_TEST",
127
"INJECT_EVENTS",
128
"INSTALL_LOCATION_PROVIDER",
129
"INSTALL_PACKAGES",
130
"INSTALL_SHORTCUT",
131
"INTERNAL_SYSTEM_WINDOW",
132
"INTERNET",
133
"KILL_BACKGROUND_PROCESSES",
134
"LOCATION_HARDWARE",
135
"MANAGE_ACCOUNTS",
136
"MANAGE_APP_TOKENS",
137
"MANAGE_DOCUMENTS",
138
"MANAGE_EXTERNAL_STORAGE",
139
"MASTER_CLEAR",
140
"MEDIA_CONTENT_CONTROL",
141
"MODIFY_AUDIO_SETTINGS",
142
"MODIFY_PHONE_STATE",
143
"MOUNT_FORMAT_FILESYSTEMS",
144
"MOUNT_UNMOUNT_FILESYSTEMS",
145
"NFC",
146
"PERSISTENT_ACTIVITY",
147
"POST_NOTIFICATIONS",
148
"PROCESS_OUTGOING_CALLS",
149
"READ_CALENDAR",
150
"READ_CALL_LOG",
151
"READ_CONTACTS",
152
"READ_EXTERNAL_STORAGE",
153
"READ_FRAME_BUFFER",
154
"READ_HISTORY_BOOKMARKS",
155
"READ_INPUT_STATE",
156
"READ_LOGS",
157
"READ_MEDIA_AUDIO",
158
"READ_MEDIA_IMAGES",
159
"READ_MEDIA_VIDEO",
160
"READ_MEDIA_VISUAL_USER_SELECTED",
161
"READ_PHONE_STATE",
162
"READ_PROFILE",
163
"READ_SMS",
164
"READ_SOCIAL_STREAM",
165
"READ_SYNC_SETTINGS",
166
"READ_SYNC_STATS",
167
"READ_USER_DICTIONARY",
168
"REBOOT",
169
"RECEIVE_BOOT_COMPLETED",
170
"RECEIVE_MMS",
171
"RECEIVE_SMS",
172
"RECEIVE_WAP_PUSH",
173
"RECORD_AUDIO",
174
"REORDER_TASKS",
175
"RESTART_PACKAGES",
176
"SEND_RESPOND_VIA_MESSAGE",
177
"SEND_SMS",
178
"SET_ACTIVITY_WATCHER",
179
"SET_ALARM",
180
"SET_ALWAYS_FINISH",
181
"SET_ANIMATION_SCALE",
182
"SET_DEBUG_APP",
183
"SET_ORIENTATION",
184
"SET_POINTER_SPEED",
185
"SET_PREFERRED_APPLICATIONS",
186
"SET_PROCESS_LIMIT",
187
"SET_TIME",
188
"SET_TIME_ZONE",
189
"SET_WALLPAPER",
190
"SET_WALLPAPER_HINTS",
191
"SIGNAL_PERSISTENT_PROCESSES",
192
"STATUS_BAR",
193
"SUBSCRIBED_FEEDS_READ",
194
"SUBSCRIBED_FEEDS_WRITE",
195
"SYSTEM_ALERT_WINDOW",
196
"TRANSMIT_IR",
197
"UNINSTALL_SHORTCUT",
198
"UPDATE_DEVICE_STATS",
199
"USE_CREDENTIALS",
200
"USE_SIP",
201
"VIBRATE",
202
"WAKE_LOCK",
203
"WRITE_APN_SETTINGS",
204
"WRITE_CALENDAR",
205
"WRITE_CALL_LOG",
206
"WRITE_CONTACTS",
207
"WRITE_EXTERNAL_STORAGE",
208
"WRITE_GSERVICES",
209
"WRITE_HISTORY_BOOKMARKS",
210
"WRITE_PROFILE",
211
"WRITE_SECURE_SETTINGS",
212
"WRITE_SETTINGS",
213
"WRITE_SMS",
214
"WRITE_SOCIAL_STREAM",
215
"WRITE_SYNC_SETTINGS",
216
"WRITE_USER_DICTIONARY",
217
nullptr
218
};
219
220
static const char *MISMATCHED_VERSIONS_MESSAGE = "Android build version mismatch:\n| Template installed: %s\n| Requested version: %s\nPlease reinstall Android build template from 'Project' menu.";
221
222
static const char *GDEXTENSION_LIBS_PATH = "libs/gdextensionlibs.json";
223
224
// This template string must be in sync with the content of 'platform/android/java/lib/res/mipmap-anydpi-v26/icon.xml'.
225
static const String ICON_XML_TEMPLATE =
226
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
227
"<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n"
228
" <background android:drawable=\"@mipmap/icon_background\"/>\n"
229
" <foreground android:drawable=\"@mipmap/icon_foreground\"/>\n"
230
"%s" // Placeholder for the optional monochrome tag.
231
"</adaptive-icon>";
232
233
static const String ICON_XML_PATH = "res/mipmap-anydpi-v26/icon.xml";
234
static const String THEMED_ICON_XML_PATH = "res/mipmap-anydpi-v26/themed_icon.xml";
235
236
static const int ICON_DENSITIES_COUNT = 6;
237
static const char *LAUNCHER_ICON_OPTION = PNAME("launcher_icons/main_192x192");
238
static const char *LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION = PNAME("launcher_icons/adaptive_foreground_432x432");
239
static const char *LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION = PNAME("launcher_icons/adaptive_background_432x432");
240
static const char *LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION = PNAME("launcher_icons/adaptive_monochrome_432x432");
241
242
static const LauncherIcon LAUNCHER_ICONS[ICON_DENSITIES_COUNT] = {
243
{ "res/mipmap-xxxhdpi-v4/icon.png", 192 },
244
{ "res/mipmap-xxhdpi-v4/icon.png", 144 },
245
{ "res/mipmap-xhdpi-v4/icon.png", 96 },
246
{ "res/mipmap-hdpi-v4/icon.png", 72 },
247
{ "res/mipmap-mdpi-v4/icon.png", 48 },
248
{ "res/mipmap/icon.png", 192 }
249
};
250
251
static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[ICON_DENSITIES_COUNT] = {
252
{ "res/mipmap-xxxhdpi-v4/icon_foreground.png", 432 },
253
{ "res/mipmap-xxhdpi-v4/icon_foreground.png", 324 },
254
{ "res/mipmap-xhdpi-v4/icon_foreground.png", 216 },
255
{ "res/mipmap-hdpi-v4/icon_foreground.png", 162 },
256
{ "res/mipmap-mdpi-v4/icon_foreground.png", 108 },
257
{ "res/mipmap/icon_foreground.png", 432 }
258
};
259
260
static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[ICON_DENSITIES_COUNT] = {
261
{ "res/mipmap-xxxhdpi-v4/icon_background.png", 432 },
262
{ "res/mipmap-xxhdpi-v4/icon_background.png", 324 },
263
{ "res/mipmap-xhdpi-v4/icon_background.png", 216 },
264
{ "res/mipmap-hdpi-v4/icon_background.png", 162 },
265
{ "res/mipmap-mdpi-v4/icon_background.png", 108 },
266
{ "res/mipmap/icon_background.png", 432 }
267
};
268
269
static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[ICON_DENSITIES_COUNT] = {
270
{ "res/mipmap-xxxhdpi-v4/icon_monochrome.png", 432 },
271
{ "res/mipmap-xxhdpi-v4/icon_monochrome.png", 324 },
272
{ "res/mipmap-xhdpi-v4/icon_monochrome.png", 216 },
273
{ "res/mipmap-hdpi-v4/icon_monochrome.png", 162 },
274
{ "res/mipmap-mdpi-v4/icon_monochrome.png", 108 },
275
{ "res/mipmap/icon_monochrome.png", 432 }
276
};
277
278
static const int EXPORT_FORMAT_APK = 0;
279
static const int EXPORT_FORMAT_AAB = 1;
280
281
static const char *APK_ASSETS_DIRECTORY = "assets";
282
static const char *AAB_ASSETS_DIRECTORY = "assetPackInstallTime/src/main/assets";
283
284
static const int DEFAULT_MIN_SDK_VERSION = 24; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
285
static const int DEFAULT_TARGET_SDK_VERSION = 35; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
286
287
#ifndef ANDROID_ENABLED
288
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
289
EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);
290
291
while (!ea->quit_request.is_set()) {
292
#ifndef DISABLE_DEPRECATED
293
// Check for android plugins updates
294
{
295
// Nothing to do if we already know the plugins have changed.
296
if (!ea->android_plugins_changed.is_set()) {
297
Vector<PluginConfigAndroid> loaded_plugins = get_plugins();
298
299
MutexLock lock(ea->android_plugins_lock);
300
301
if (ea->android_plugins.size() != loaded_plugins.size()) {
302
ea->android_plugins_changed.set();
303
} else {
304
for (int i = 0; i < ea->android_plugins.size(); i++) {
305
if (ea->android_plugins[i].name != loaded_plugins[i].name) {
306
ea->android_plugins_changed.set();
307
break;
308
}
309
}
310
}
311
312
if (ea->android_plugins_changed.is_set()) {
313
ea->android_plugins = loaded_plugins;
314
}
315
}
316
}
317
#endif // DISABLE_DEPRECATED
318
319
// Check for devices updates
320
String adb = get_adb_path();
321
// adb.exe was locking the editor_doc_cache file on startup. Adding a check for is_editor_ready provides just enough time
322
// to regenerate the doc cache.
323
if (ea->has_runnable_preset.is_set() && FileAccess::exists(adb) && EditorNode::get_singleton()->is_editor_ready()) {
324
String devices;
325
List<String> args;
326
args.push_back("devices");
327
int ec;
328
OS::get_singleton()->execute(adb, args, &devices, &ec);
329
330
Vector<String> ds = devices.split("\n");
331
Vector<String> ldevices;
332
for (int i = 1; i < ds.size(); i++) {
333
String d = ds[i];
334
int dpos = d.find("device");
335
if (dpos == -1) {
336
continue;
337
}
338
d = d.substr(0, dpos).strip_edges();
339
ldevices.push_back(d);
340
}
341
342
MutexLock lock(ea->device_lock);
343
344
bool different = false;
345
346
if (ea->devices.size() != ldevices.size()) {
347
different = true;
348
} else {
349
for (int i = 0; i < ea->devices.size(); i++) {
350
if (ea->devices[i].id != ldevices[i]) {
351
different = true;
352
break;
353
}
354
}
355
}
356
357
if (different) {
358
Vector<Device> ndevices;
359
360
for (int i = 0; i < ldevices.size(); i++) {
361
Device d;
362
d.id = ldevices[i];
363
for (int j = 0; j < ea->devices.size(); j++) {
364
if (ea->devices[j].id == ldevices[i]) {
365
d.description = ea->devices[j].description;
366
d.name = ea->devices[j].name;
367
d.api_level = ea->devices[j].api_level;
368
}
369
}
370
371
if (d.description.is_empty()) {
372
//in the oven, request!
373
args.clear();
374
args.push_back("-s");
375
args.push_back(d.id);
376
args.push_back("shell");
377
args.push_back("getprop");
378
int ec2;
379
String dp;
380
381
OS::get_singleton()->execute(adb, args, &dp, &ec2);
382
383
Vector<String> props = dp.split("\n");
384
String vendor;
385
String device;
386
d.description = "Device ID: " + d.id + "\n";
387
d.api_level = 0;
388
for (int j = 0; j < props.size(); j++) {
389
// got information by `shell cat /system/build.prop` before and its format is "property=value"
390
// it's now changed to use `shell getporp` because of permission issue with Android 8.0 and above
391
// its format is "[property]: [value]" so changed it as like build.prop
392
String p = props[j];
393
p = p.replace("]: ", "=");
394
p = p.remove_chars("[]");
395
396
if (p.begins_with("ro.product.model=")) {
397
device = p.get_slicec('=', 1).strip_edges();
398
} else if (p.begins_with("ro.product.brand=")) {
399
vendor = p.get_slicec('=', 1).strip_edges().capitalize();
400
} else if (p.begins_with("ro.build.display.id=")) {
401
d.description += "Build: " + p.get_slicec('=', 1).strip_edges() + "\n";
402
} else if (p.begins_with("ro.build.version.release=")) {
403
d.description += "Release: " + p.get_slicec('=', 1).strip_edges() + "\n";
404
} else if (p.begins_with("ro.build.version.sdk=")) {
405
d.api_level = p.get_slicec('=', 1).to_int();
406
} else if (p.begins_with("ro.product.cpu.abi=")) {
407
d.architecture = p.get_slicec('=', 1).strip_edges();
408
d.description += "CPU: " + d.architecture + "\n";
409
} else if (p.begins_with("ro.product.manufacturer=")) {
410
d.description += "Manufacturer: " + p.get_slicec('=', 1).strip_edges() + "\n";
411
} else if (p.begins_with("ro.board.platform=")) {
412
d.description += "Chipset: " + p.get_slicec('=', 1).strip_edges() + "\n";
413
} else if (p.begins_with("ro.opengles.version=")) {
414
uint32_t opengl = p.get_slicec('=', 1).to_int();
415
d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n";
416
}
417
}
418
419
d.name = vendor + " " + device;
420
if (device.is_empty()) {
421
continue;
422
}
423
}
424
425
ndevices.push_back(d);
426
}
427
428
ea->devices = ndevices;
429
ea->devices_changed.set();
430
}
431
}
432
433
uint64_t sleep = 200;
434
uint64_t wait = 3000000;
435
uint64_t time = OS::get_singleton()->get_ticks_usec();
436
while (OS::get_singleton()->get_ticks_usec() - time < wait) {
437
OS::get_singleton()->delay_usec(1000 * sleep);
438
if (ea->quit_request.is_set()) {
439
break;
440
}
441
}
442
}
443
444
if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) {
445
String adb = get_adb_path();
446
if (!FileAccess::exists(adb)) {
447
return; //adb not configured
448
}
449
450
List<String> args;
451
args.push_back("kill-server");
452
OS::get_singleton()->execute(adb, args);
453
}
454
}
455
456
void EditorExportPlatformAndroid::_update_preset_status() {
457
const int preset_count = EditorExport::get_singleton()->get_export_preset_count();
458
bool has_runnable = false;
459
460
for (int i = 0; i < preset_count; i++) {
461
const Ref<EditorExportPreset> &preset = EditorExport::get_singleton()->get_export_preset(i);
462
if (preset->get_platform() == this && preset->is_runnable()) {
463
has_runnable = true;
464
break;
465
}
466
}
467
468
if (has_runnable) {
469
has_runnable_preset.set();
470
} else {
471
has_runnable_preset.clear();
472
}
473
devices_changed.set();
474
}
475
#endif
476
477
String EditorExportPlatformAndroid::get_project_name(const Ref<EditorExportPreset> &p_preset, const String &p_name) const {
478
String aname;
479
if (!p_name.is_empty()) {
480
aname = p_name;
481
} else {
482
aname = get_project_setting(p_preset, "application/config/name");
483
}
484
485
if (aname.is_empty()) {
486
aname = GODOT_VERSION_NAME;
487
}
488
489
return aname;
490
}
491
492
String EditorExportPlatformAndroid::get_package_name(const Ref<EditorExportPreset> &p_preset, const String &p_package) const {
493
String pname = p_package;
494
String name = get_valid_basename(p_preset);
495
pname = pname.replace("$genname", name);
496
return pname;
497
}
498
499
// Returns the project name without invalid characters
500
// or the "noname" string if all characters are invalid.
501
String EditorExportPlatformAndroid::get_valid_basename(const Ref<EditorExportPreset> &p_preset) const {
502
String basename = get_project_setting(p_preset, "application/config/name");
503
basename = basename.to_lower();
504
505
String name;
506
bool first = true;
507
for (int i = 0; i < basename.length(); i++) {
508
char32_t c = basename[i];
509
if (is_digit(c) && first) {
510
continue;
511
}
512
if (is_ascii_identifier_char(c)) {
513
name += String::chr(c);
514
first = false;
515
}
516
}
517
518
if (name.is_empty()) {
519
name = "noname";
520
}
521
522
return name;
523
}
524
525
String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const {
526
String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
527
return gradle_build_directory.path_join(p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY);
528
}
529
530
bool EditorExportPlatformAndroid::is_package_name_valid(const Ref<EditorExportPreset> &p_preset, const String &p_package, String *r_error) const {
531
String pname = get_package_name(p_preset, p_package);
532
533
if (pname.length() == 0) {
534
if (r_error) {
535
*r_error = TTR("Package name is missing.");
536
}
537
return false;
538
}
539
540
int segments = 0;
541
bool first = true;
542
for (int i = 0; i < pname.length(); i++) {
543
char32_t c = pname[i];
544
if (first && c == '.') {
545
if (r_error) {
546
*r_error = TTR("Package segments must be of non-zero length.");
547
}
548
return false;
549
}
550
if (c == '.') {
551
segments++;
552
first = true;
553
continue;
554
}
555
if (!is_ascii_identifier_char(c)) {
556
if (r_error) {
557
*r_error = vformat(TTR("The character '%s' is not allowed in Android application package names."), String::chr(c));
558
}
559
return false;
560
}
561
if (first && is_digit(c)) {
562
if (r_error) {
563
*r_error = TTR("A digit cannot be the first character in a package segment.");
564
}
565
return false;
566
}
567
if (first && is_underscore(c)) {
568
if (r_error) {
569
*r_error = vformat(TTR("The character '%s' cannot be the first character in a package segment."), String::chr(c));
570
}
571
return false;
572
}
573
first = false;
574
}
575
576
if (segments == 0) {
577
if (r_error) {
578
*r_error = TTR("The package must have at least one '.' separator.");
579
}
580
return false;
581
}
582
583
if (first) {
584
if (r_error) {
585
*r_error = TTR("Package segments must be of non-zero length.");
586
}
587
return false;
588
}
589
590
return true;
591
}
592
593
bool EditorExportPlatformAndroid::is_project_name_valid(const Ref<EditorExportPreset> &p_preset) const {
594
// Get the original project name and convert to lowercase.
595
String basename = get_project_setting(p_preset, "application/config/name");
596
basename = basename.to_lower();
597
// Check if there are invalid characters.
598
if (basename != get_valid_basename(p_preset)) {
599
return false;
600
}
601
return true;
602
}
603
604
bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) {
605
/*
606
* By not compressing files with little or no benefit in doing so,
607
* a performance gain is expected at runtime. Moreover, if the APK is
608
* zip-aligned, assets stored as they are can be efficiently read by
609
* Android by memory-mapping them.
610
*/
611
612
// -- Unconditional uncompress to mimic AAPT plus some other
613
614
static const char *unconditional_compress_ext[] = {
615
// From https://github.com/android/platform_frameworks_base/blob/master/tools/aapt/Package.cpp
616
// These formats are already compressed, or don't compress well:
617
".jpg", ".jpeg", ".png", ".gif",
618
".wav", ".mp2", ".mp3", ".ogg", ".aac",
619
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
620
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
621
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
622
".amr", ".awb", ".wma", ".wmv",
623
// Godot-specific:
624
".webp", // Same reasoning as .png
625
".cfb", // Don't let small config files slow-down startup
626
".scn", // Binary scenes are usually already compressed
627
".ctex", // Streamable textures are usually already compressed
628
".pck", // Pack.
629
// Trailer for easier processing
630
nullptr
631
};
632
633
for (const char **ext = unconditional_compress_ext; *ext; ++ext) {
634
if (p_path.to_lower().ends_with(String(*ext))) {
635
return false;
636
}
637
}
638
639
// -- Compressed resource?
640
641
if (p_data.size() >= 4 && p_data[0] == 'R' && p_data[1] == 'S' && p_data[2] == 'C' && p_data[3] == 'C') {
642
// Already compressed
643
return false;
644
}
645
646
// --- TODO: Decide on texture resources according to their image compression setting
647
648
return true;
649
}
650
651
zip_fileinfo EditorExportPlatformAndroid::get_zip_fileinfo() {
652
OS::DateTime dt = OS::get_singleton()->get_datetime();
653
654
zip_fileinfo zipfi;
655
zipfi.tmz_date.tm_year = dt.year;
656
zipfi.tmz_date.tm_mon = dt.month - 1; // tm_mon is zero indexed
657
zipfi.tmz_date.tm_mday = dt.day;
658
zipfi.tmz_date.tm_hour = dt.hour;
659
zipfi.tmz_date.tm_min = dt.minute;
660
zipfi.tmz_date.tm_sec = dt.second;
661
zipfi.dosDate = 0;
662
zipfi.external_fa = 0;
663
zipfi.internal_fa = 0;
664
665
return zipfi;
666
}
667
668
Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_abis() {
669
// Should have the same order and size as get_archs.
670
Vector<ABI> abis;
671
abis.push_back(ABI("armeabi-v7a", "arm32"));
672
abis.push_back(ABI("arm64-v8a", "arm64"));
673
abis.push_back(ABI("x86", "x86_32"));
674
abis.push_back(ABI("x86_64", "x86_64"));
675
return abis;
676
}
677
678
#ifndef DISABLE_DEPRECATED
679
/// List the gdap files in the directory specified by the p_path parameter.
680
Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) {
681
Vector<String> dir_files;
682
Ref<DirAccess> da = DirAccess::open(p_path);
683
if (da.is_valid()) {
684
da->list_dir_begin();
685
while (true) {
686
String file = da->get_next();
687
if (file.is_empty()) {
688
break;
689
}
690
691
if (da->current_is_dir() || da->current_is_hidden()) {
692
continue;
693
}
694
695
if (file.ends_with(PluginConfigAndroid::PLUGIN_CONFIG_EXT)) {
696
dir_files.push_back(file);
697
}
698
}
699
da->list_dir_end();
700
}
701
702
return dir_files;
703
}
704
705
Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_plugins() {
706
Vector<PluginConfigAndroid> loaded_plugins;
707
708
String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().path_join("android/plugins");
709
710
// Add the prebuilt plugins
711
loaded_plugins.append_array(PluginConfigAndroid::get_prebuilt_plugins(plugins_dir));
712
713
if (DirAccess::exists(plugins_dir)) {
714
Vector<String> plugins_filenames = list_gdap_files(plugins_dir);
715
716
if (!plugins_filenames.is_empty()) {
717
Ref<ConfigFile> config_file;
718
config_file.instantiate();
719
for (int i = 0; i < plugins_filenames.size(); i++) {
720
PluginConfigAndroid config = PluginConfigAndroid::load_plugin_config(config_file, plugins_dir.path_join(plugins_filenames[i]));
721
if (config.valid_config) {
722
loaded_plugins.push_back(config);
723
} else {
724
print_error("Invalid plugin config file " + plugins_filenames[i]);
725
}
726
}
727
}
728
}
729
730
return loaded_plugins;
731
}
732
733
Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_enabled_plugins(const Ref<EditorExportPreset> &p_presets) {
734
Vector<PluginConfigAndroid> enabled_plugins;
735
Vector<PluginConfigAndroid> all_plugins = get_plugins();
736
for (int i = 0; i < all_plugins.size(); i++) {
737
PluginConfigAndroid plugin = all_plugins[i];
738
bool enabled = p_presets->get("plugins/" + plugin.name);
739
if (enabled) {
740
enabled_plugins.push_back(plugin);
741
}
742
}
743
744
return enabled_plugins;
745
}
746
#endif // DISABLE_DEPRECATED
747
748
Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method) {
749
zip_fileinfo zipfi = get_zip_fileinfo();
750
zipOpenNewFileInZip(ed->apk,
751
p_path.utf8().get_data(),
752
&zipfi,
753
nullptr,
754
0,
755
nullptr,
756
0,
757
nullptr,
758
compression_method,
759
Z_DEFAULT_COMPRESSION);
760
761
zipWriteInFileInZip(ed->apk, p_data.ptr(), p_data.size());
762
zipCloseFileInZip(ed->apk);
763
764
return OK;
765
}
766
767
Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObject &p_so) {
768
if (!p_so.path.get_file().begins_with("lib")) {
769
String err = "Android .so file names must start with \"lib\", but got: " + p_so.path;
770
ERR_PRINT(err);
771
return FAILED;
772
}
773
APKExportData *ed = static_cast<APKExportData *>(p_userdata);
774
Vector<ABI> abis = get_abis();
775
bool exported = false;
776
for (int i = 0; i < p_so.tags.size(); ++i) {
777
// shared objects can be fat (compatible with multiple ABIs)
778
int abi_index = -1;
779
for (int j = 0; j < abis.size(); ++j) {
780
if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {
781
abi_index = j;
782
break;
783
}
784
}
785
if (abi_index != -1) {
786
exported = true;
787
String abi = abis[abi_index].abi;
788
String dst_path = String("lib").path_join(abi).path_join(p_so.path.get_file());
789
Vector<uint8_t> array = FileAccess::get_file_as_bytes(p_so.path);
790
Error store_err = store_in_apk(ed, dst_path, array, Z_NO_COMPRESSION);
791
ERR_FAIL_COND_V_MSG(store_err, store_err, "Cannot store in apk file '" + dst_path + "'.");
792
}
793
}
794
if (!exported) {
795
ERR_PRINT("Cannot determine architecture for library \"" + p_so.path + "\". One of the supported architectures must be used as a tag: " + join_abis(abis, " ", true));
796
return FAILED;
797
}
798
return OK;
799
}
800
801
Error EditorExportPlatformAndroid::save_apk_file(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) {
802
APKExportData *ed = static_cast<APKExportData *>(p_userdata);
803
804
String simplified_path = p_path.simplify_path();
805
if (simplified_path.begins_with("uid://")) {
806
simplified_path = ResourceUID::uid_to_path(simplified_path).simplify_path();
807
print_verbose(vformat(R"(UID referenced exported file name "%s" was replaced with "%s".)", p_path, simplified_path));
808
}
809
810
Vector<uint8_t> enc_data;
811
EditorExportPlatform::SavedData sd;
812
Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, enc_data, sd);
813
if (err != OK) {
814
return err;
815
}
816
817
const String dst_path = String("assets/") + simplified_path.trim_prefix("res://");
818
print_verbose("Saving project files from " + simplified_path + " into " + dst_path);
819
store_in_apk(ed, dst_path, enc_data, _should_compress_asset(simplified_path, enc_data) ? Z_DEFLATED : 0);
820
821
ed->pd.file_ofs.push_back(sd);
822
823
return OK;
824
}
825
826
Error EditorExportPlatformAndroid::ignore_apk_file(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) {
827
return OK;
828
}
829
830
Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const SharedObject &p_so) {
831
ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED,
832
"Android .so file names must start with \"lib\", but got: " + p_so.path);
833
Vector<ABI> abis = get_abis();
834
CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
835
bool exported = false;
836
for (int i = 0; i < p_so.tags.size(); ++i) {
837
int abi_index = -1;
838
for (int j = 0; j < abis.size(); ++j) {
839
if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {
840
abi_index = j;
841
break;
842
}
843
}
844
if (abi_index != -1) {
845
exported = true;
846
String type = export_data->debug ? "debug" : "release";
847
String abi = abis[abi_index].abi;
848
String filename = p_so.path.get_file();
849
String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename);
850
Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path);
851
print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);
852
Error err = store_file_at_path(dst_path, data);
853
ERR_FAIL_COND_V_MSG(err, err, "Failed to copy .so file from " + p_so.path + " to " + dst_path);
854
export_data->libs.push_back(dst_path);
855
}
856
}
857
ERR_FAIL_COND_V_MSG(!exported, FAILED,
858
"Cannot determine architecture for library \"" + p_so.path + "\". One of the supported architectures must be used as a tag:" + join_abis(abis, " ", true));
859
return OK;
860
}
861
862
bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) {
863
return p_permissions.has("android.permission.READ_EXTERNAL_STORAGE") || p_permissions.has("android.permission.WRITE_EXTERNAL_STORAGE");
864
}
865
866
bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) {
867
return p_permissions.has("android.permission.MANAGE_EXTERNAL_STORAGE");
868
}
869
870
bool EditorExportPlatformAndroid::_uses_vulkan(const Ref<EditorExportPreset> &p_preset) const {
871
String rendering_method = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");
872
String rendering_driver = get_project_setting(p_preset, "rendering/rendering_device/driver.android");
873
return (rendering_method == "forward_plus" || rendering_method == "mobile") && rendering_driver == "vulkan";
874
}
875
876
void EditorExportPlatformAndroid::_notification(int p_what) {
877
#ifndef ANDROID_ENABLED
878
switch (p_what) {
879
case NOTIFICATION_POSTINITIALIZE: {
880
if (EditorExport::get_singleton()) {
881
EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status));
882
}
883
} break;
884
885
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
886
if (EditorSettings::get_singleton()->check_changed_settings_in_group("export/android")) {
887
_create_editor_debug_keystore_if_needed();
888
}
889
} break;
890
}
891
#endif
892
}
893
894
void EditorExportPlatformAndroid::_create_editor_debug_keystore_if_needed() {
895
// Check if we have a valid keytool path.
896
String keytool_path = get_keytool_path();
897
if (!FileAccess::exists(keytool_path)) {
898
return;
899
}
900
901
// Check if the current editor debug keystore exists.
902
String editor_debug_keystore = EDITOR_GET("export/android/debug_keystore");
903
if (FileAccess::exists(editor_debug_keystore)) {
904
return;
905
}
906
907
// Generate the debug keystore.
908
String keystore_path = EditorPaths::get_singleton()->get_debug_keystore_path();
909
String keystores_dir = keystore_path.get_base_dir();
910
if (!DirAccess::exists(keystores_dir)) {
911
Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
912
Error err = dir_access->make_dir_recursive(keystores_dir);
913
if (err != OK) {
914
WARN_PRINT(TTR("Error creating keystores directory:") + "\n" + keystores_dir);
915
return;
916
}
917
}
918
919
if (!FileAccess::exists(keystore_path)) {
920
String output;
921
List<String> args;
922
args.push_back("-genkey");
923
args.push_back("-keystore");
924
args.push_back(keystore_path);
925
args.push_back("-storepass");
926
args.push_back("android");
927
args.push_back("-alias");
928
args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
929
args.push_back("-keypass");
930
args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
931
args.push_back("-keyalg");
932
args.push_back("RSA");
933
args.push_back("-keysize");
934
args.push_back("2048");
935
args.push_back("-validity");
936
args.push_back("10000");
937
args.push_back("-dname");
938
args.push_back("cn=Godot, ou=Godot Engine, o=Stichting Godot, c=NL");
939
Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);
940
print_verbose(output);
941
if (error != OK) {
942
WARN_PRINT("Error: Unable to create debug keystore");
943
return;
944
}
945
}
946
947
// Update the editor settings.
948
EditorSettings::get_singleton()->set("export/android/debug_keystore", keystore_path);
949
EditorSettings::get_singleton()->set("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
950
EditorSettings::get_singleton()->set("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
951
print_verbose("Updated editor debug keystore to " + keystore_path);
952
}
953
954
void EditorExportPlatformAndroid::_get_manifest_info(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions, Vector<FeatureInfo> &r_features, Vector<MetadataInfo> &r_metadata) {
955
const char **aperms = ANDROID_PERMS;
956
while (*aperms) {
957
bool enabled = p_preset->get("permissions/" + String(*aperms).to_lower());
958
if (enabled) {
959
r_permissions.push_back("android.permission." + String(*aperms));
960
}
961
aperms++;
962
}
963
PackedStringArray user_perms = p_preset->get("permissions/custom_permissions");
964
for (int i = 0; i < user_perms.size(); i++) {
965
String user_perm = user_perms[i].strip_edges();
966
if (!user_perm.is_empty()) {
967
r_permissions.push_back(user_perm);
968
}
969
}
970
if (p_give_internet) {
971
if (!r_permissions.has("android.permission.INTERNET")) {
972
r_permissions.push_back("android.permission.INTERNET");
973
}
974
}
975
976
if (_uses_vulkan(p_preset)) {
977
// Require vulkan hardware level 1 support
978
FeatureInfo vulkan_level = {
979
"android.hardware.vulkan.level", // name
980
false, // required
981
"1" // version
982
};
983
r_features.append(vulkan_level);
984
985
// Require vulkan version 1.0
986
FeatureInfo vulkan_version = {
987
"android.hardware.vulkan.version", // name
988
true, // required
989
"0x400003" // version - Encoded value for api version 1.0
990
};
991
r_features.append(vulkan_version);
992
}
993
994
MetadataInfo rendering_method_metadata = {
995
"org.godotengine.rendering.method",
996
p_preset->get_project_setting("rendering/renderer/rendering_method.mobile")
997
};
998
r_metadata.append(rendering_method_metadata);
999
1000
MetadataInfo editor_version_metadata = {
1001
"org.godotengine.editor.version",
1002
String(GODOT_VERSION_FULL_CONFIG)
1003
};
1004
r_metadata.append(editor_version_metadata);
1005
}
1006
1007
void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) {
1008
print_verbose("Building temporary manifest...");
1009
String manifest_text =
1010
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
1011
"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
1012
" xmlns:tools=\"http://schemas.android.com/tools\">\n";
1013
1014
manifest_text += _get_screen_sizes_tag(p_preset);
1015
manifest_text += _get_gles_tag();
1016
1017
Vector<String> perms;
1018
Vector<FeatureInfo> features;
1019
Vector<MetadataInfo> manifest_metadata;
1020
_get_manifest_info(p_preset, p_give_internet, perms, features, manifest_metadata);
1021
for (int i = 0; i < perms.size(); i++) {
1022
String permission = perms.get(i);
1023
if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {
1024
manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);
1025
} else {
1026
manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission);
1027
}
1028
}
1029
1030
for (int i = 0; i < features.size(); i++) {
1031
manifest_text += vformat(" <uses-feature tools:node=\"replace\" android:name=\"%s\" android:required=\"%s\" android:version=\"%s\" />\n", features[i].name, features[i].required, features[i].version);
1032
}
1033
1034
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
1035
for (int i = 0; i < export_plugins.size(); i++) {
1036
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
1037
const String contents = export_plugins[i]->get_android_manifest_element_contents(Ref<EditorExportPlatform>(this), p_debug);
1038
if (!contents.is_empty()) {
1039
manifest_text += contents;
1040
manifest_text += "\n";
1041
}
1042
}
1043
}
1044
1045
manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug, manifest_metadata);
1046
manifest_text += "</manifest>\n";
1047
String manifest_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join(vformat("src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")));
1048
1049
print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text);
1050
store_string_at_path(manifest_path, manifest_text);
1051
}
1052
1053
bool EditorExportPlatformAndroid::_is_transparency_allowed(const Ref<EditorExportPreset> &p_preset) const {
1054
return (bool)get_project_setting(p_preset, "display/window/per_pixel_transparency/allowed");
1055
}
1056
1057
void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset> &p_preset) {
1058
const String themes_xml_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join("res/values/themes.xml");
1059
1060
if (!FileAccess::exists(themes_xml_path)) {
1061
print_error("res/values/themes.xml does not exist.");
1062
return;
1063
}
1064
1065
bool transparency_allowed = _is_transparency_allowed(p_preset);
1066
1067
// Default/Reserved theme attributes.
1068
Dictionary main_theme_attributes;
1069
main_theme_attributes["android:windowSwipeToDismiss"] = bool_to_string(p_preset->get("gesture/swipe_to_dismiss"));
1070
main_theme_attributes["android:windowIsTranslucent"] = bool_to_string(transparency_allowed);
1071
if (transparency_allowed) {
1072
main_theme_attributes["android:windowBackground"] = "@android:color/transparent";
1073
} else {
1074
main_theme_attributes["android:windowBackground"] = "#" + p_preset->get("screen/background_color").operator Color().to_html(false);
1075
}
1076
1077
Dictionary splash_theme_attributes;
1078
splash_theme_attributes["android:windowSplashScreenBackground"] = "@mipmap/icon_background";
1079
splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@mipmap/icon_foreground";
1080
splash_theme_attributes["postSplashScreenTheme"] = "@style/GodotAppMainTheme";
1081
splash_theme_attributes["android:windowIsTranslucent"] = bool_to_string(transparency_allowed);
1082
1083
PackedStringArray reserved_splash_keys;
1084
reserved_splash_keys.append("postSplashScreenTheme");
1085
reserved_splash_keys.append("android:windowIsTranslucent");
1086
1087
Dictionary custom_theme_attributes = p_preset->get("gradle_build/custom_theme_attributes");
1088
1089
// Does not override default/reserved theme attributes; skips any duplicates from custom_theme_attributes.
1090
for (const Variant &k : custom_theme_attributes.keys()) {
1091
String key = k;
1092
String value = custom_theme_attributes[k];
1093
if (key.begins_with("[splash]")) {
1094
String splash_key = key.trim_prefix("[splash]");
1095
if (reserved_splash_keys.has(splash_key)) {
1096
WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", splash_key));
1097
} else {
1098
splash_theme_attributes[splash_key] = value;
1099
}
1100
} else {
1101
if (main_theme_attributes.has(key)) {
1102
WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", key));
1103
} else {
1104
main_theme_attributes[key] = value;
1105
}
1106
}
1107
}
1108
1109
Ref<FileAccess> file = FileAccess::open(themes_xml_path, FileAccess::READ);
1110
PackedStringArray lines = file->get_as_text().split("\n");
1111
file->close();
1112
1113
PackedStringArray new_lines;
1114
bool inside_main_theme = false;
1115
bool inside_splash_theme = false;
1116
1117
for (int i = 0; i < lines.size(); i++) {
1118
String line = lines[i];
1119
1120
if (line.contains("<style name=\"GodotAppMainTheme\"")) {
1121
inside_main_theme = true;
1122
new_lines.append(line);
1123
continue;
1124
}
1125
if (line.contains("<style name=\"GodotAppSplashTheme\"")) {
1126
inside_splash_theme = true;
1127
new_lines.append(line);
1128
continue;
1129
}
1130
1131
// Inject GodotAppMainTheme attributes.
1132
if (inside_main_theme && line.contains("</style>")) {
1133
for (const Variant &attribute : main_theme_attributes.keys()) {
1134
String value = main_theme_attributes[attribute];
1135
String item_line = vformat(" <item name=\"%s\">%s</item>", attribute, value);
1136
new_lines.append(item_line);
1137
}
1138
new_lines.append(line); // Add </style> in the end.
1139
inside_main_theme = false;
1140
continue;
1141
}
1142
1143
// Inject GodotAppSplashTheme attributes.
1144
if (inside_splash_theme && line.contains("</style>")) {
1145
for (const Variant &attribute : splash_theme_attributes.keys()) {
1146
String value = splash_theme_attributes[attribute];
1147
String item_line = vformat(" <item name=\"%s\">%s</item>", attribute, value);
1148
new_lines.append(item_line);
1149
}
1150
new_lines.append(line); // Add </style> in the end.
1151
inside_splash_theme = false;
1152
continue;
1153
}
1154
1155
// Add all other lines unchanged.
1156
if (!inside_main_theme && !inside_splash_theme) {
1157
new_lines.append(line);
1158
}
1159
}
1160
1161
// Reconstruct the XML content from the modified lines.
1162
String xml_content = String("\n").join(new_lines);
1163
store_string_at_path(themes_xml_path, xml_content);
1164
print_verbose("Successfully modified " + themes_xml_path + ": " + "\n" + xml_content);
1165
}
1166
1167
void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet) {
1168
// Leaving the unused types commented because looking these constants up
1169
// again later would be annoying
1170
// const int CHUNK_AXML_FILE = 0x00080003;
1171
// const int CHUNK_RESOURCEIDS = 0x00080180;
1172
const int CHUNK_STRINGS = 0x001C0001;
1173
// const int CHUNK_XML_END_NAMESPACE = 0x00100101;
1174
const int CHUNK_XML_END_TAG = 0x00100103;
1175
// const int CHUNK_XML_START_NAMESPACE = 0x00100100;
1176
const int CHUNK_XML_START_TAG = 0x00100102;
1177
// const int CHUNK_XML_TEXT = 0x00100104;
1178
const int UTF8_FLAG = 0x00000100;
1179
1180
Vector<String> string_table;
1181
1182
uint32_t ofs = 8;
1183
1184
uint32_t string_count = 0;
1185
uint32_t string_flags = 0;
1186
uint32_t string_data_offset = 0;
1187
1188
uint32_t string_table_begins = 0;
1189
uint32_t string_table_ends = 0;
1190
Vector<uint8_t> stable_extra;
1191
1192
String version_name = p_preset->get_version("version/name");
1193
int version_code = p_preset->get("version/code");
1194
String package_name = p_preset->get("package/unique_name");
1195
1196
const int screen_orientation =
1197
_get_android_orientation_value(DisplayServer::ScreenOrientation(int(get_project_setting(p_preset, "display/window/handheld/orientation"))));
1198
1199
bool screen_support_small = p_preset->get("screen/support_small");
1200
bool screen_support_normal = p_preset->get("screen/support_normal");
1201
bool screen_support_large = p_preset->get("screen/support_large");
1202
bool screen_support_xlarge = p_preset->get("screen/support_xlarge");
1203
1204
bool backup_allowed = p_preset->get("user_data_backup/allow");
1205
int app_category = p_preset->get("package/app_category");
1206
bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall");
1207
bool exclude_from_recents = p_preset->get("package/exclude_from_recents");
1208
bool is_resizeable = bool(get_project_setting(p_preset, "display/window/size/resizable"));
1209
1210
Vector<String> perms;
1211
Vector<FeatureInfo> features;
1212
Vector<MetadataInfo> manifest_metadata;
1213
_get_manifest_info(p_preset, p_give_internet, perms, features, manifest_metadata);
1214
bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);
1215
1216
while (ofs < (uint32_t)p_manifest.size()) {
1217
uint32_t chunk = decode_uint32(&p_manifest[ofs]);
1218
uint32_t size = decode_uint32(&p_manifest[ofs + 4]);
1219
1220
switch (chunk) {
1221
case CHUNK_STRINGS: {
1222
int iofs = ofs + 8;
1223
1224
string_count = decode_uint32(&p_manifest[iofs]);
1225
string_flags = decode_uint32(&p_manifest[iofs + 8]);
1226
string_data_offset = decode_uint32(&p_manifest[iofs + 12]);
1227
1228
uint32_t st_offset = iofs + 20;
1229
string_table.resize(string_count);
1230
uint32_t string_end = 0;
1231
1232
string_table_begins = st_offset;
1233
1234
for (uint32_t i = 0; i < string_count; i++) {
1235
uint32_t string_at = decode_uint32(&p_manifest[st_offset + i * 4]);
1236
string_at += st_offset + string_count * 4;
1237
1238
ERR_FAIL_COND_MSG(string_flags & UTF8_FLAG, "Unimplemented, can't read UTF-8 string table.");
1239
1240
if (string_flags & UTF8_FLAG) {
1241
} else {
1242
uint32_t len = decode_uint16(&p_manifest[string_at]);
1243
Vector<char32_t> ucstring;
1244
ucstring.resize(len + 1);
1245
for (uint32_t j = 0; j < len; j++) {
1246
uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]);
1247
ucstring.write[j] = c;
1248
}
1249
string_end = MAX(string_at + 2 + 2 * len, string_end);
1250
ucstring.write[len] = 0;
1251
string_table.write[i] = ucstring.ptr();
1252
}
1253
}
1254
1255
for (uint32_t i = string_end; i < (ofs + size); i++) {
1256
stable_extra.push_back(p_manifest[i]);
1257
}
1258
1259
string_table_ends = ofs + size;
1260
1261
} break;
1262
case CHUNK_XML_START_TAG: {
1263
int iofs = ofs + 8;
1264
uint32_t name = decode_uint32(&p_manifest[iofs + 12]);
1265
1266
String tname = string_table[name];
1267
uint32_t attrcount = decode_uint32(&p_manifest[iofs + 20]);
1268
iofs += 28;
1269
1270
for (uint32_t i = 0; i < attrcount; i++) {
1271
uint32_t attr_nspace = decode_uint32(&p_manifest[iofs]);
1272
uint32_t attr_name = decode_uint32(&p_manifest[iofs + 4]);
1273
uint32_t attr_value = decode_uint32(&p_manifest[iofs + 8]);
1274
uint32_t attr_resid = decode_uint32(&p_manifest[iofs + 16]);
1275
1276
const String value = (attr_value != 0xFFFFFFFF) ? string_table[attr_value] : "Res #" + itos(attr_resid);
1277
String attrname = string_table[attr_name];
1278
const String nspace = (attr_nspace != 0xFFFFFFFF) ? string_table[attr_nspace] : "";
1279
1280
//replace project information
1281
if (tname == "manifest" && attrname == "package") {
1282
string_table.write[attr_value] = get_package_name(p_preset, package_name);
1283
}
1284
1285
if (tname == "manifest" && attrname == "versionCode") {
1286
encode_uint32(version_code, &p_manifest.write[iofs + 16]);
1287
}
1288
1289
if (tname == "manifest" && attrname == "versionName") {
1290
if (attr_value == 0xFFFFFFFF) {
1291
WARN_PRINT("Version name in a resource, should be plain text");
1292
} else {
1293
string_table.write[attr_value] = version_name;
1294
}
1295
}
1296
1297
if (tname == "application" && attrname == "requestLegacyExternalStorage") {
1298
encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1299
}
1300
1301
if (tname == "application" && attrname == "allowBackup") {
1302
encode_uint32(backup_allowed, &p_manifest.write[iofs + 16]);
1303
}
1304
1305
if (tname == "application" && attrname == "appCategory") {
1306
encode_uint32(_get_app_category_value(app_category), &p_manifest.write[iofs + 16]);
1307
}
1308
1309
if (tname == "application" && attrname == "isGame") {
1310
encode_uint32(app_category == APP_CATEGORY_GAME, &p_manifest.write[iofs + 16]);
1311
}
1312
1313
if (tname == "application" && attrname == "hasFragileUserData") {
1314
encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]);
1315
}
1316
1317
if (tname == "activity" && attrname == "screenOrientation") {
1318
encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]);
1319
}
1320
1321
if (tname == "activity" && attrname == "excludeFromRecents") {
1322
encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]);
1323
}
1324
1325
if (tname == "activity" && attrname == "resizeableActivity") {
1326
encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]);
1327
}
1328
1329
if (tname == "provider" && attrname == "authorities") {
1330
string_table.write[attr_value] = get_package_name(p_preset, package_name) + String(".fileprovider");
1331
}
1332
1333
if (tname == "supports-screens") {
1334
if (attrname == "smallScreens") {
1335
encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1336
1337
} else if (attrname == "normalScreens") {
1338
encode_uint32(screen_support_normal ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1339
1340
} else if (attrname == "largeScreens") {
1341
encode_uint32(screen_support_large ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1342
1343
} else if (attrname == "xlargeScreens") {
1344
encode_uint32(screen_support_xlarge ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1345
}
1346
}
1347
1348
iofs += 20;
1349
}
1350
1351
} break;
1352
case CHUNK_XML_END_TAG: {
1353
int iofs = ofs + 8;
1354
uint32_t name = decode_uint32(&p_manifest[iofs + 12]);
1355
String tname = string_table[name];
1356
1357
if (tname == "manifest" || tname == "application") {
1358
// save manifest ending so we can restore it
1359
Vector<uint8_t> manifest_end;
1360
uint32_t manifest_cur_size = p_manifest.size();
1361
1362
manifest_end.resize(p_manifest.size() - ofs);
1363
memcpy(manifest_end.ptrw(), &p_manifest[ofs], manifest_end.size());
1364
1365
int32_t attr_name_string = string_table.find("name");
1366
ERR_FAIL_COND_MSG(attr_name_string == -1, "Template does not have 'name' attribute.");
1367
1368
int32_t ns_android_string = string_table.find("http://schemas.android.com/apk/res/android");
1369
if (ns_android_string == -1) {
1370
string_table.push_back("http://schemas.android.com/apk/res/android");
1371
ns_android_string = string_table.size() - 1;
1372
}
1373
1374
if (tname == "manifest") {
1375
// Updating manifest features
1376
int32_t attr_uses_feature_string = string_table.find("uses-feature");
1377
if (attr_uses_feature_string == -1) {
1378
string_table.push_back("uses-feature");
1379
attr_uses_feature_string = string_table.size() - 1;
1380
}
1381
1382
int32_t attr_required_string = string_table.find("required");
1383
if (attr_required_string == -1) {
1384
string_table.push_back("required");
1385
attr_required_string = string_table.size() - 1;
1386
}
1387
1388
for (int i = 0; i < features.size(); i++) {
1389
const String &feature_name = features[i].name;
1390
bool feature_required = features[i].required;
1391
String feature_version = features[i].version;
1392
bool has_version_attribute = !feature_version.is_empty();
1393
1394
print_line("Adding feature " + feature_name);
1395
1396
int32_t feature_string = string_table.find(feature_name);
1397
if (feature_string == -1) {
1398
string_table.push_back(feature_name);
1399
feature_string = string_table.size() - 1;
1400
}
1401
1402
String required_value_string = feature_required ? "true" : "false";
1403
int32_t required_value = string_table.find(required_value_string);
1404
if (required_value == -1) {
1405
string_table.push_back(required_value_string);
1406
required_value = string_table.size() - 1;
1407
}
1408
1409
int32_t attr_version_string = -1;
1410
int32_t version_value = -1;
1411
int tag_size;
1412
int attr_count;
1413
if (has_version_attribute) {
1414
attr_version_string = string_table.find("version");
1415
if (attr_version_string == -1) {
1416
string_table.push_back("version");
1417
attr_version_string = string_table.size() - 1;
1418
}
1419
1420
version_value = string_table.find(feature_version);
1421
if (version_value == -1) {
1422
string_table.push_back(feature_version);
1423
version_value = string_table.size() - 1;
1424
}
1425
1426
tag_size = 96; // node and three attrs + end node
1427
attr_count = 3;
1428
} else {
1429
tag_size = 76; // node and two attrs + end node
1430
attr_count = 2;
1431
}
1432
manifest_cur_size += tag_size + 24;
1433
p_manifest.resize(manifest_cur_size);
1434
1435
// start tag
1436
encode_uint16(0x102, &p_manifest.write[ofs]); // type
1437
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1438
encode_uint32(tag_size, &p_manifest.write[ofs + 4]); // size
1439
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1440
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1441
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1442
encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name
1443
encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start
1444
encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size
1445
encode_uint16(attr_count, &p_manifest.write[ofs + 28]); // num_attrs
1446
encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index
1447
encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index
1448
encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index
1449
1450
// android:name attribute
1451
encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns
1452
encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'
1453
encode_uint32(feature_string, &p_manifest.write[ofs + 44]); // raw_value
1454
encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size
1455
p_manifest.write[ofs + 50] = 0; // typedvalue_always0
1456
p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)
1457
encode_uint32(feature_string, &p_manifest.write[ofs + 52]); // typedvalue reference
1458
1459
// android:required attribute
1460
encode_uint32(ns_android_string, &p_manifest.write[ofs + 56]); // ns
1461
encode_uint32(attr_required_string, &p_manifest.write[ofs + 60]); // 'name'
1462
encode_uint32(required_value, &p_manifest.write[ofs + 64]); // raw_value
1463
encode_uint16(8, &p_manifest.write[ofs + 68]); // typedvalue_size
1464
p_manifest.write[ofs + 70] = 0; // typedvalue_always0
1465
p_manifest.write[ofs + 71] = 0x03; // typedvalue_type (string)
1466
encode_uint32(required_value, &p_manifest.write[ofs + 72]); // typedvalue reference
1467
1468
ofs += 76;
1469
1470
if (has_version_attribute) {
1471
// android:version attribute
1472
encode_uint32(ns_android_string, &p_manifest.write[ofs]); // ns
1473
encode_uint32(attr_version_string, &p_manifest.write[ofs + 4]); // 'name'
1474
encode_uint32(version_value, &p_manifest.write[ofs + 8]); // raw_value
1475
encode_uint16(8, &p_manifest.write[ofs + 12]); // typedvalue_size
1476
p_manifest.write[ofs + 14] = 0; // typedvalue_always0
1477
p_manifest.write[ofs + 15] = 0x03; // typedvalue_type (string)
1478
encode_uint32(version_value, &p_manifest.write[ofs + 16]); // typedvalue reference
1479
1480
ofs += 20;
1481
}
1482
1483
// end tag
1484
encode_uint16(0x103, &p_manifest.write[ofs]); // type
1485
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1486
encode_uint32(24, &p_manifest.write[ofs + 4]); // size
1487
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1488
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1489
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1490
encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name
1491
1492
ofs += 24;
1493
}
1494
1495
// Updating manifest permissions
1496
int32_t attr_uses_permission_string = string_table.find("uses-permission");
1497
if (attr_uses_permission_string == -1) {
1498
string_table.push_back("uses-permission");
1499
attr_uses_permission_string = string_table.size() - 1;
1500
}
1501
1502
for (int i = 0; i < perms.size(); ++i) {
1503
print_line("Adding permission " + perms[i]);
1504
1505
manifest_cur_size += 56 + 24; // node + end node
1506
p_manifest.resize(manifest_cur_size);
1507
1508
// Add permission to the string pool
1509
int32_t perm_string = string_table.find(perms[i]);
1510
if (perm_string == -1) {
1511
string_table.push_back(perms[i]);
1512
perm_string = string_table.size() - 1;
1513
}
1514
1515
// start tag
1516
encode_uint16(0x102, &p_manifest.write[ofs]); // type
1517
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1518
encode_uint32(56, &p_manifest.write[ofs + 4]); // size
1519
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1520
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1521
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1522
encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name
1523
encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start
1524
encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size
1525
encode_uint16(1, &p_manifest.write[ofs + 28]); // num_attrs
1526
encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index
1527
encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index
1528
encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index
1529
1530
// attribute
1531
encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns
1532
encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'
1533
encode_uint32(perm_string, &p_manifest.write[ofs + 44]); // raw_value
1534
encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size
1535
p_manifest.write[ofs + 50] = 0; // typedvalue_always0
1536
p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)
1537
encode_uint32(perm_string, &p_manifest.write[ofs + 52]); // typedvalue reference
1538
1539
ofs += 56;
1540
1541
// end tag
1542
encode_uint16(0x103, &p_manifest.write[ofs]); // type
1543
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1544
encode_uint32(24, &p_manifest.write[ofs + 4]); // size
1545
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1546
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1547
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1548
encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name
1549
1550
ofs += 24;
1551
}
1552
}
1553
1554
if (tname == "application") {
1555
// Updating application meta-data
1556
int32_t attr_meta_data_string = string_table.find("meta-data");
1557
if (attr_meta_data_string == -1) {
1558
string_table.push_back("meta-data");
1559
attr_meta_data_string = string_table.size() - 1;
1560
}
1561
1562
int32_t attr_value_string = string_table.find("value");
1563
if (attr_value_string == -1) {
1564
string_table.push_back("value");
1565
attr_value_string = string_table.size() - 1;
1566
}
1567
1568
for (int i = 0; i < manifest_metadata.size(); i++) {
1569
String meta_data_name = manifest_metadata[i].name;
1570
String meta_data_value = manifest_metadata[i].value;
1571
1572
print_line("Adding application metadata " + meta_data_name);
1573
1574
int32_t meta_data_name_string = string_table.find(meta_data_name);
1575
if (meta_data_name_string == -1) {
1576
string_table.push_back(meta_data_name);
1577
meta_data_name_string = string_table.size() - 1;
1578
}
1579
1580
int32_t meta_data_value_string = string_table.find(meta_data_value);
1581
if (meta_data_value_string == -1) {
1582
string_table.push_back(meta_data_value);
1583
meta_data_value_string = string_table.size() - 1;
1584
}
1585
1586
int tag_size = 76; // node and two attrs + end node
1587
int attr_count = 2;
1588
manifest_cur_size += tag_size + 24;
1589
p_manifest.resize(manifest_cur_size);
1590
1591
// start tag
1592
encode_uint16(0x102, &p_manifest.write[ofs]); // type
1593
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1594
encode_uint32(tag_size, &p_manifest.write[ofs + 4]); // size
1595
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1596
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1597
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1598
encode_uint32(attr_meta_data_string, &p_manifest.write[ofs + 20]); // name
1599
encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start
1600
encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size
1601
encode_uint16(attr_count, &p_manifest.write[ofs + 28]); // num_attrs
1602
encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index
1603
encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index
1604
encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index
1605
1606
// android:name attribute
1607
encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns
1608
encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'
1609
encode_uint32(meta_data_name_string, &p_manifest.write[ofs + 44]); // raw_value
1610
encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size
1611
p_manifest.write[ofs + 50] = 0; // typedvalue_always0
1612
p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)
1613
encode_uint32(meta_data_name_string, &p_manifest.write[ofs + 52]); // typedvalue reference
1614
1615
// android:value attribute
1616
encode_uint32(ns_android_string, &p_manifest.write[ofs + 56]); // ns
1617
encode_uint32(attr_value_string, &p_manifest.write[ofs + 60]); // 'value'
1618
encode_uint32(meta_data_value_string, &p_manifest.write[ofs + 64]); // raw_value
1619
encode_uint16(8, &p_manifest.write[ofs + 68]); // typedvalue_size
1620
p_manifest.write[ofs + 70] = 0; // typedvalue_always0
1621
p_manifest.write[ofs + 71] = 0x03; // typedvalue_type (string)
1622
encode_uint32(meta_data_value_string, &p_manifest.write[ofs + 72]); // typedvalue reference
1623
1624
ofs += 76;
1625
1626
// end tag
1627
encode_uint16(0x103, &p_manifest.write[ofs]); // type
1628
encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1629
encode_uint32(24, &p_manifest.write[ofs + 4]); // size
1630
encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1631
encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1632
encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1633
encode_uint32(attr_meta_data_string, &p_manifest.write[ofs + 20]); // name
1634
1635
ofs += 24;
1636
}
1637
}
1638
1639
// copy footer back in
1640
memcpy(&p_manifest.write[ofs], manifest_end.ptr(), manifest_end.size());
1641
}
1642
} break;
1643
}
1644
1645
ofs += size;
1646
}
1647
1648
// Create new android manifest binary.
1649
1650
Vector<uint8_t> ret;
1651
ret.resize(string_table_begins + string_table.size() * 4);
1652
1653
for (uint32_t i = 0; i < string_table_begins; i++) {
1654
ret.write[i] = p_manifest[i];
1655
}
1656
1657
ofs = 0;
1658
for (int i = 0; i < string_table.size(); i++) {
1659
encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);
1660
ofs += string_table[i].length() * 2 + 2 + 2;
1661
}
1662
1663
ret.resize(ret.size() + ofs);
1664
string_data_offset = ret.size() - ofs;
1665
uint8_t *chars = &ret.write[string_data_offset];
1666
for (int i = 0; i < string_table.size(); i++) {
1667
String s = string_table[i];
1668
encode_uint16(s.length(), chars);
1669
chars += 2;
1670
for (int j = 0; j < s.length(); j++) {
1671
encode_uint16(s[j], chars);
1672
chars += 2;
1673
}
1674
encode_uint16(0, chars);
1675
chars += 2;
1676
}
1677
1678
for (int i = 0; i < stable_extra.size(); i++) {
1679
ret.push_back(stable_extra[i]);
1680
}
1681
1682
//pad
1683
while (ret.size() % 4) {
1684
ret.push_back(0);
1685
}
1686
1687
uint32_t new_stable_end = ret.size();
1688
1689
uint32_t extra = (p_manifest.size() - string_table_ends);
1690
ret.resize(new_stable_end + extra);
1691
for (uint32_t i = 0; i < extra; i++) {
1692
ret.write[new_stable_end + i] = p_manifest[string_table_ends + i];
1693
}
1694
1695
while (ret.size() % 4) {
1696
ret.push_back(0);
1697
}
1698
encode_uint32(ret.size(), &ret.write[4]); //update new file size
1699
1700
encode_uint32(new_stable_end - 8, &ret.write[12]); //update new string table size
1701
encode_uint32(string_table.size(), &ret.write[16]); //update new number of strings
1702
encode_uint32(string_data_offset - 8, &ret.write[28]); //update new string data offset
1703
1704
p_manifest = ret;
1705
}
1706
1707
String EditorExportPlatformAndroid::_get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug) {
1708
String keystore_preference = p_debug ? "keystore/debug" : "keystore/release";
1709
String keystore_env_variable = p_debug ? ENV_ANDROID_KEYSTORE_DEBUG_PATH : ENV_ANDROID_KEYSTORE_RELEASE_PATH;
1710
String keystore_path = p_preset->get_or_env(keystore_preference, keystore_env_variable);
1711
1712
return ProjectSettings::get_singleton()->globalize_path(keystore_path).simplify_path();
1713
}
1714
1715
String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) {
1716
uint32_t offset = 0;
1717
uint32_t len = 0;
1718
1719
if (p_utf8) {
1720
uint8_t byte = p_bytes[offset];
1721
if (byte & 0x80) {
1722
offset += 2;
1723
} else {
1724
offset += 1;
1725
}
1726
byte = p_bytes[offset];
1727
offset++;
1728
if (byte & 0x80) {
1729
len = byte & 0x7F;
1730
len = (len << 8) + p_bytes[offset];
1731
offset++;
1732
} else {
1733
len = byte;
1734
}
1735
} else {
1736
len = decode_uint16(&p_bytes[offset]);
1737
offset += 2;
1738
if (len & 0x8000) {
1739
len &= 0x7FFF;
1740
len = (len << 16) + decode_uint16(&p_bytes[offset]);
1741
offset += 2;
1742
}
1743
}
1744
1745
if (p_utf8) {
1746
Vector<uint8_t> str8;
1747
str8.resize(len + 1);
1748
for (uint32_t i = 0; i < len; i++) {
1749
str8.write[i] = p_bytes[offset + i];
1750
}
1751
str8.write[len] = 0;
1752
return String::utf8((const char *)str8.ptr(), len);
1753
} else {
1754
String str;
1755
for (uint32_t i = 0; i < len; i++) {
1756
char32_t c = decode_uint16(&p_bytes[offset + i * 2]);
1757
if (c == 0) {
1758
break;
1759
}
1760
str += String::chr(c);
1761
}
1762
return str;
1763
}
1764
}
1765
1766
void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest) {
1767
const int UTF8_FLAG = 0x00000100;
1768
1769
uint32_t string_block_len = decode_uint32(&r_manifest[16]);
1770
uint32_t string_count = decode_uint32(&r_manifest[20]);
1771
uint32_t string_flags = decode_uint32(&r_manifest[28]);
1772
const uint32_t string_table_begins = 40;
1773
1774
Vector<String> string_table;
1775
1776
String package_name = p_preset->get("package/name");
1777
Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");
1778
1779
for (uint32_t i = 0; i < string_count; i++) {
1780
uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]);
1781
offset += string_table_begins + string_count * 4;
1782
1783
String str = _parse_string(&r_manifest[offset], string_flags & UTF8_FLAG);
1784
1785
if (str.begins_with("godot-project-name")) {
1786
if (str == "godot-project-name") {
1787
//project name
1788
str = get_project_name(p_preset, package_name);
1789
1790
} else {
1791
String lang = str.substr(str.rfind_char('-') + 1).replace_char('-', '_');
1792
if (appnames.has(lang)) {
1793
str = appnames[lang];
1794
} else {
1795
str = get_project_name(p_preset, package_name);
1796
}
1797
}
1798
}
1799
1800
string_table.push_back(str);
1801
}
1802
1803
//write a new string table, but use 16 bits
1804
Vector<uint8_t> ret;
1805
ret.resize(string_table_begins + string_table.size() * 4);
1806
1807
for (uint32_t i = 0; i < string_table_begins; i++) {
1808
ret.write[i] = r_manifest[i];
1809
}
1810
1811
int ofs = 0;
1812
for (int i = 0; i < string_table.size(); i++) {
1813
encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);
1814
ofs += string_table[i].length() * 2 + 2 + 2;
1815
}
1816
1817
ret.resize(ret.size() + ofs);
1818
uint8_t *chars = &ret.write[ret.size() - ofs];
1819
for (int i = 0; i < string_table.size(); i++) {
1820
String s = string_table[i];
1821
encode_uint16(s.length(), chars);
1822
chars += 2;
1823
for (int j = 0; j < s.length(); j++) {
1824
encode_uint16(s[j], chars);
1825
chars += 2;
1826
}
1827
encode_uint16(0, chars);
1828
chars += 2;
1829
}
1830
1831
//pad
1832
while (ret.size() % 4) {
1833
ret.push_back(0);
1834
}
1835
1836
//change flags to not use utf8
1837
encode_uint32(string_flags & ~0x100, &ret.write[28]);
1838
//change length
1839
encode_uint32(ret.size() - 12, &ret.write[16]);
1840
//append the rest...
1841
int rest_from = 12 + string_block_len;
1842
int rest_to = ret.size();
1843
int rest_len = (r_manifest.size() - rest_from);
1844
ret.resize(ret.size() + (r_manifest.size() - rest_from));
1845
for (int i = 0; i < rest_len; i++) {
1846
ret.write[rest_to + i] = r_manifest[rest_from + i];
1847
}
1848
//finally update the size
1849
encode_uint32(ret.size(), &ret.write[4]);
1850
1851
r_manifest = ret;
1852
//printf("end\n");
1853
}
1854
1855
void EditorExportPlatformAndroid::_load_image_data(const Ref<Image> &p_splash_image, Vector<uint8_t> &p_data) {
1856
Vector<uint8_t> png_buffer;
1857
Error err = PNGDriverCommon::image_to_png(p_splash_image, png_buffer);
1858
if (err == OK) {
1859
p_data.resize(png_buffer.size());
1860
memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size());
1861
} else {
1862
String err_str = String("Failed to convert splash image to png.");
1863
WARN_PRINT(err_str.utf8().get_data());
1864
}
1865
}
1866
1867
void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) {
1868
Ref<Image> working_image = p_source_image;
1869
1870
if (p_source_image->get_width() != dimension || p_source_image->get_height() != dimension) {
1871
working_image = p_source_image->duplicate();
1872
working_image->resize(dimension, dimension, Image::Interpolation::INTERPOLATE_LANCZOS);
1873
}
1874
1875
Vector<uint8_t> png_buffer;
1876
Error err = PNGDriverCommon::image_to_png(working_image, png_buffer);
1877
if (err == OK) {
1878
p_data.resize(png_buffer.size());
1879
memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size());
1880
} else {
1881
String err_str = String("Failed to convert resized icon (") + p_file_name + ") to png.";
1882
WARN_PRINT(err_str.utf8().get_data());
1883
}
1884
}
1885
1886
void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome) {
1887
String project_icon_path = get_project_setting(p_preset, "application/config/icon");
1888
1889
Error err = OK;
1890
1891
// Regular icon: user selection -> project icon -> default.
1892
String path = static_cast<String>(p_preset->get(LAUNCHER_ICON_OPTION)).strip_edges();
1893
print_verbose("Loading regular icon from " + path);
1894
if (!path.is_empty()) {
1895
icon = _load_icon_or_splash_image(path, &err);
1896
}
1897
if (path.is_empty() || err != OK || icon.is_null() || icon->is_empty()) {
1898
print_verbose("- falling back to project icon: " + project_icon_path);
1899
if (!project_icon_path.is_empty()) {
1900
icon = _load_icon_or_splash_image(project_icon_path, &err);
1901
} else {
1902
ERR_PRINT("No project icon specified. Please specify one in the Project Settings under Application -> Config -> Icon");
1903
}
1904
}
1905
1906
// Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default).
1907
path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION)).strip_edges();
1908
print_verbose("Loading adaptive foreground icon from " + path);
1909
if (!path.is_empty()) {
1910
foreground = _load_icon_or_splash_image(path, &err);
1911
}
1912
if (path.is_empty() || err != OK || foreground.is_null() || foreground->is_empty()) {
1913
print_verbose("- falling back to using the regular icon");
1914
foreground = icon;
1915
}
1916
1917
// Adaptive background: user selection -> default.
1918
path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION)).strip_edges();
1919
if (!path.is_empty()) {
1920
print_verbose("Loading adaptive background icon from " + path);
1921
background = _load_icon_or_splash_image(path, &err);
1922
}
1923
1924
// Adaptive monochrome: user selection -> default.
1925
path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION)).strip_edges();
1926
if (!path.is_empty()) {
1927
print_verbose("Loading adaptive monochrome icon from " + path);
1928
monochrome = _load_icon_or_splash_image(path, &err);
1929
}
1930
}
1931
1932
void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
1933
const Ref<Image> &p_main_image,
1934
const Ref<Image> &p_foreground,
1935
const Ref<Image> &p_background,
1936
const Ref<Image> &p_monochrome) {
1937
String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
1938
1939
String monochrome_tag = "";
1940
1941
// Prepare images to be resized for the icons. If some image ends up being uninitialized,
1942
// the default image from the export template will be used.
1943
1944
for (int i = 0; i < ICON_DENSITIES_COUNT; ++i) {
1945
if (p_main_image.is_valid() && !p_main_image->is_empty()) {
1946
print_verbose("Processing launcher icon for dimension " + itos(LAUNCHER_ICONS[i].dimensions) + " into " + LAUNCHER_ICONS[i].export_path);
1947
Vector<uint8_t> data;
1948
_process_launcher_icons(LAUNCHER_ICONS[i].export_path, p_main_image, LAUNCHER_ICONS[i].dimensions, data);
1949
store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ICONS[i].export_path), data);
1950
}
1951
1952
if (p_foreground.is_valid() && !p_foreground->is_empty()) {
1953
print_verbose("Processing launcher adaptive icon p_foreground for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path);
1954
Vector<uint8_t> data;
1955
_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path, p_foreground,
1956
LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data);
1957
store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path), data);
1958
}
1959
1960
if (p_background.is_valid() && !p_background->is_empty()) {
1961
print_verbose("Processing launcher adaptive icon p_background for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path);
1962
Vector<uint8_t> data;
1963
_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path, p_background,
1964
LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data);
1965
store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path), data);
1966
}
1967
1968
if (p_monochrome.is_valid() && !p_monochrome->is_empty()) {
1969
print_verbose("Processing launcher adaptive icon p_monochrome for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path);
1970
Vector<uint8_t> data;
1971
_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path, p_monochrome,
1972
LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data);
1973
store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path), data);
1974
monochrome_tag = " <monochrome android:drawable=\"@mipmap/icon_monochrome\"/>\n";
1975
}
1976
}
1977
1978
// Finalize the icon.xml by formatting the template with the optional monochrome tag.
1979
store_string_at_path(gradle_build_dir.path_join(ICON_XML_PATH), vformat(ICON_XML_TEMPLATE, monochrome_tag));
1980
}
1981
1982
Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_enabled_abis(const Ref<EditorExportPreset> &p_preset) {
1983
Vector<ABI> abis = get_abis();
1984
Vector<ABI> enabled_abis;
1985
for (int i = 0; i < abis.size(); ++i) {
1986
bool is_enabled = p_preset->get("architectures/" + abis[i].abi);
1987
if (is_enabled) {
1988
enabled_abis.push_back(abis[i]);
1989
}
1990
}
1991
return enabled_abis;
1992
}
1993
1994
void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
1995
r_features->push_back("etc2");
1996
r_features->push_back("astc");
1997
1998
if (p_preset->get("shader_baker/enabled")) {
1999
r_features->push_back("shader_baker");
2000
}
2001
2002
Vector<ABI> abis = get_enabled_abis(p_preset);
2003
for (int i = 0; i < abis.size(); ++i) {
2004
r_features->push_back(abis[i].arch);
2005
}
2006
}
2007
2008
String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {
2009
if (p_preset) {
2010
if (p_name == ("apk_expansion/public_key")) {
2011
bool apk_expansion = p_preset->get("apk_expansion/enable");
2012
String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
2013
if (apk_expansion && apk_expansion_pkey.is_empty()) {
2014
return TTR("Invalid public key for APK expansion.");
2015
}
2016
} else if (p_name == "package/unique_name") {
2017
String pn = p_preset->get("package/unique_name");
2018
String pn_err;
2019
2020
if (!is_package_name_valid(Ref<EditorExportPreset>(p_preset), pn, &pn_err)) {
2021
return TTR("Invalid package name:") + " " + pn_err;
2022
}
2023
} else if (p_name == "gesture/swipe_to_dismiss") {
2024
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2025
if (bool(p_preset->get("gesture/swipe_to_dismiss")) && !gradle_build_enabled) {
2026
return TTR("\"Use Gradle Build\" is required to enable \"Swipe to dismiss\".");
2027
}
2028
} else if (p_name == "gradle_build/use_gradle_build") {
2029
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2030
String enabled_deprecated_plugins_names = _get_deprecated_plugins_names(Ref<EditorExportPreset>(p_preset));
2031
if (!enabled_deprecated_plugins_names.is_empty() && !gradle_build_enabled) {
2032
return TTR("\"Use Gradle Build\" must be enabled to use the plugins.");
2033
}
2034
} else if (p_name == "gradle_build/compress_native_libraries") {
2035
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2036
if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) {
2037
return TTR("\"Compress Native Libraries\" is only valid when \"Use Gradle Build\" is enabled.");
2038
}
2039
} else if (p_name == "gradle_build/export_format") {
2040
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2041
if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) {
2042
return TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled.");
2043
}
2044
} else if (p_name == "gradle_build/min_sdk") {
2045
String min_sdk_str = p_preset->get("gradle_build/min_sdk");
2046
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2047
if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
2048
if (!gradle_build_enabled) {
2049
return TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");
2050
}
2051
if (!min_sdk_str.is_valid_int()) {
2052
return vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str);
2053
} else {
2054
int min_sdk_int = min_sdk_str.to_int();
2055
if (min_sdk_int < DEFAULT_MIN_SDK_VERSION) {
2056
return vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), DEFAULT_MIN_SDK_VERSION);
2057
}
2058
}
2059
}
2060
} else if (p_name == "gradle_build/target_sdk") {
2061
String target_sdk_str = p_preset->get("gradle_build/target_sdk");
2062
int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
2063
2064
String min_sdk_str = p_preset->get("gradle_build/min_sdk");
2065
int min_sdk_int = DEFAULT_MIN_SDK_VERSION;
2066
if (min_sdk_str.is_valid_int()) {
2067
min_sdk_int = min_sdk_str.to_int();
2068
}
2069
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2070
if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
2071
if (!gradle_build_enabled) {
2072
return TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");
2073
}
2074
if (!target_sdk_str.is_valid_int()) {
2075
return vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str);
2076
} else {
2077
target_sdk_int = target_sdk_str.to_int();
2078
if (target_sdk_int < min_sdk_int) {
2079
return TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version.");
2080
}
2081
}
2082
}
2083
} else if (p_name == "gradle_build/custom_theme_attributes") {
2084
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2085
if (bool(p_preset->get("gradle_build/custom_theme_attributes")) && !gradle_build_enabled) {
2086
return TTR("\"Use Gradle Build\" is required to add custom theme attributes.");
2087
}
2088
} else if (p_name == "package/show_in_android_tv") {
2089
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2090
if (bool(p_preset->get("package/show_in_android_tv")) && !gradle_build_enabled) {
2091
return TTR("\"Use Gradle Build\" must be enabled to enable \"Show In Android Tv\".");
2092
}
2093
} else if (p_name == "package/show_as_launcher_app") {
2094
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2095
if (bool(p_preset->get("package/show_as_launcher_app")) && !gradle_build_enabled) {
2096
return TTR("\"Use Gradle Build\" must be enabled to enable \"Show As Launcher App\".");
2097
}
2098
} else if (p_name == "package/show_in_app_library") {
2099
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2100
if (!bool(p_preset->get("package/show_in_app_library")) && !gradle_build_enabled) {
2101
return TTR("\"Use Gradle Build\" must be enabled to disable \"Show In App Library\".");
2102
}
2103
} else if (p_name == "shader_baker/enabled" && bool(p_preset->get("shader_baker/enabled"))) {
2104
String export_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile");
2105
if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") {
2106
return TTR("\"Shader Baker\" is not supported when using the Compatibility renderer.");
2107
} else if (OS::get_singleton()->get_current_rendering_method() != export_renderer) {
2108
return vformat(TTR("The editor is currently using a different renderer than what the target platform will use. \"Shader Baker\" won't be able to include core shaders. Switch to the \"%s\" renderer temporarily to fix this."), export_renderer);
2109
}
2110
}
2111
}
2112
return String();
2113
}
2114
2115
void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) const {
2116
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
2117
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
2118
2119
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, true));
2120
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false));
2121
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
2122
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/compress_native_libraries"), false, false, true));
2123
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));
2124
// Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).
2125
// This implies doing validation that the string is a proper int.
2126
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), "", false, true));
2127
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true));
2128
2129
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "gradle_build/custom_theme_attributes", PROPERTY_HINT_DICTIONARY_TYPE, "String;String"), Dictionary()));
2130
2131
#ifndef DISABLE_DEPRECATED
2132
Vector<PluginConfigAndroid> plugins_configs = get_plugins();
2133
for (int i = 0; i < plugins_configs.size(); i++) {
2134
print_verbose("Found Android plugin " + plugins_configs[i].name);
2135
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false));
2136
}
2137
android_plugins_changed.clear();
2138
#endif // DISABLE_DEPRECATED
2139
2140
// Android supports multiple architectures in an app bundle, so
2141
// we expose each option as a checkbox in the export dialog.
2142
const Vector<ABI> abis = get_abis();
2143
for (int i = 0; i < abis.size(); ++i) {
2144
const String abi = abis[i].abi;
2145
// All Android devices supporting Vulkan run 64-bit Android,
2146
// so there is usually no point in exporting for 32-bit Android.
2147
const bool is_default = abi == "arm64-v8a";
2148
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default));
2149
}
2150
2151
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2152
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2153
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2154
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2155
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2156
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
2157
2158
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));
2159
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
2160
2161
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "com.example.$genname", false, true));
2162
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), ""));
2163
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true));
2164
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video,Undefined"), APP_CATEGORY_GAME));
2165
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/retain_data_on_uninstall"), false));
2166
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/exclude_from_recents"), false));
2167
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_android_tv"), false));
2168
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_app_library"), true));
2169
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_as_launcher_app"), false));
2170
2171
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ICON_OPTION, PROPERTY_HINT_FILE, "*.png"), ""));
2172
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION, PROPERTY_HINT_FILE, "*.png"), ""));
2173
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION, PROPERTY_HINT_FILE, "*.png"), ""));
2174
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION, PROPERTY_HINT_FILE, "*.png"), ""));
2175
2176
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false));
2177
2178
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "shader_baker/enabled"), false));
2179
2180
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true));
2181
2182
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gesture/swipe_to_dismiss"), false));
2183
2184
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true));
2185
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/edge_to_edge"), false));
2186
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true));
2187
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true));
2188
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true));
2189
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));
2190
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "screen/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
2191
2192
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));
2193
2194
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), ""));
2195
2196
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true));
2197
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), ""));
2198
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "", false, true));
2199
2200
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray()));
2201
2202
const char **perms = ANDROID_PERMS;
2203
while (*perms) {
2204
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false));
2205
perms++;
2206
}
2207
}
2208
2209
bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
2210
if (p_preset == nullptr) {
2211
return true;
2212
}
2213
2214
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
2215
if (p_option == "graphics/opengl_debug" ||
2216
p_option == "gradle_build/custom_theme_attributes" ||
2217
p_option == "command_line/extra_args" ||
2218
p_option == "permissions/custom_permissions" ||
2219
p_option == "keystore/debug" ||
2220
p_option == "keystore/debug_user" ||
2221
p_option == "keystore/debug_password" ||
2222
p_option == "package/retain_data_on_uninstall" ||
2223
p_option == "package/exclude_from_recents" ||
2224
p_option == "package/show_in_app_library" ||
2225
p_option == "package/show_as_launcher_app" ||
2226
p_option == "gesture/swipe_to_dismiss" ||
2227
p_option == "apk_expansion/enable" ||
2228
p_option == "apk_expansion/SALT" ||
2229
p_option == "apk_expansion/public_key") {
2230
return advanced_options_enabled;
2231
}
2232
if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {
2233
return advanced_options_enabled && bool(p_preset->get("gradle_build/use_gradle_build"));
2234
}
2235
if (p_option == "custom_template/debug" || p_option == "custom_template/release") {
2236
// The APK templates are ignored if Gradle build is enabled.
2237
return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build"));
2238
}
2239
2240
// Hide .NET embedding option (always enabled).
2241
if (p_option == "dotnet/embed_build_outputs") {
2242
return false;
2243
}
2244
2245
if (p_option == "dotnet/android_use_linux_bionic") {
2246
return advanced_options_enabled;
2247
}
2248
return true;
2249
}
2250
2251
String EditorExportPlatformAndroid::get_name() const {
2252
return "Android";
2253
}
2254
2255
String EditorExportPlatformAndroid::get_os_name() const {
2256
return "Android";
2257
}
2258
2259
Ref<Texture2D> EditorExportPlatformAndroid::get_logo() const {
2260
return logo;
2261
}
2262
2263
bool EditorExportPlatformAndroid::should_update_export_options() {
2264
#ifndef DISABLE_DEPRECATED
2265
if (android_plugins_changed.is_set()) {
2266
// don't clear unless we're reporting true, to avoid race
2267
android_plugins_changed.clear();
2268
return true;
2269
}
2270
#endif // DISABLE_DEPRECATED
2271
return false;
2272
}
2273
2274
bool EditorExportPlatformAndroid::poll_export() {
2275
bool dc = devices_changed.is_set();
2276
if (dc) {
2277
// don't clear unless we're reporting true, to avoid race
2278
devices_changed.clear();
2279
}
2280
return dc;
2281
}
2282
2283
int EditorExportPlatformAndroid::get_options_count() const {
2284
MutexLock lock(device_lock);
2285
return devices.size();
2286
}
2287
2288
String EditorExportPlatformAndroid::get_options_tooltip() const {
2289
return TTR("Select device from the list");
2290
}
2291
2292
String EditorExportPlatformAndroid::get_option_label(int p_index) const {
2293
ERR_FAIL_INDEX_V(p_index, devices.size(), "");
2294
MutexLock lock(device_lock);
2295
return devices[p_index].name;
2296
}
2297
2298
String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const {
2299
ERR_FAIL_INDEX_V(p_index, devices.size(), "");
2300
MutexLock lock(device_lock);
2301
String s = devices[p_index].description;
2302
if (devices.size() == 1) {
2303
// Tooltip will be:
2304
// Name
2305
// Description
2306
s = devices[p_index].name + "\n\n" + s;
2307
}
2308
return s;
2309
}
2310
2311
String EditorExportPlatformAndroid::get_device_architecture(int p_index) const {
2312
ERR_FAIL_INDEX_V(p_index, devices.size(), "");
2313
MutexLock lock(device_lock);
2314
return devices[p_index].architecture;
2315
}
2316
2317
Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {
2318
ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
2319
2320
String can_export_error;
2321
bool can_export_missing_templates;
2322
if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {
2323
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);
2324
return ERR_UNCONFIGURED;
2325
}
2326
2327
MutexLock lock(device_lock);
2328
2329
EditorProgress ep("run", vformat(TTR("Running on %s"), devices[p_device].name), 3);
2330
2331
String adb = get_adb_path();
2332
2333
// Export_temp APK.
2334
if (ep.step(TTR("Exporting APK..."), 0)) {
2335
return ERR_SKIP;
2336
}
2337
2338
const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug");
2339
const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT);
2340
const bool use_reverse = devices[p_device].api_level >= 21 && !use_wifi_for_remote_debug;
2341
2342
if (use_reverse) {
2343
p_debug_flags.set_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST);
2344
}
2345
2346
String tmp_export_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
2347
2348
#define CLEANUP_AND_RETURN(m_err) \
2349
{ \
2350
DirAccess::remove_file_or_error(tmp_export_path); \
2351
if (FileAccess::exists(tmp_export_path + ".idsig")) { \
2352
DirAccess::remove_file_or_error(tmp_export_path + ".idsig"); \
2353
} \
2354
return m_err; \
2355
} \
2356
((void)0)
2357
2358
// Export to temporary APK before sending to device.
2359
Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags);
2360
2361
if (err != OK) {
2362
CLEANUP_AND_RETURN(err);
2363
}
2364
2365
List<String> args;
2366
int rv;
2367
String output;
2368
2369
bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install");
2370
String version_name = p_preset->get_version("version/name");
2371
String package_name = p_preset->get("package/unique_name");
2372
2373
if (remove_prev) {
2374
if (ep.step(TTR("Uninstalling..."), 1)) {
2375
CLEANUP_AND_RETURN(ERR_SKIP);
2376
}
2377
2378
print_line("Uninstalling previous version: " + devices[p_device].name);
2379
2380
args.push_back("-s");
2381
args.push_back(devices[p_device].id);
2382
args.push_back("uninstall");
2383
if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device].api_level >= 17) {
2384
args.push_back("--user");
2385
args.push_back("0");
2386
}
2387
args.push_back(get_package_name(p_preset, package_name));
2388
2389
output.clear();
2390
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2391
print_verbose(output);
2392
}
2393
2394
print_line("Installing to device (please wait...): " + devices[p_device].name);
2395
if (ep.step(TTR("Installing to device, please wait..."), 2)) {
2396
CLEANUP_AND_RETURN(ERR_SKIP);
2397
}
2398
2399
args.clear();
2400
args.push_back("-s");
2401
args.push_back(devices[p_device].id);
2402
args.push_back("install");
2403
if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device].api_level >= 17) {
2404
args.push_back("--user");
2405
args.push_back("0");
2406
}
2407
args.push_back("-r");
2408
args.push_back(tmp_export_path);
2409
2410
output.clear();
2411
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2412
print_verbose(output);
2413
if (err || rv != 0) {
2414
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output));
2415
CLEANUP_AND_RETURN(ERR_CANT_CREATE);
2416
}
2417
2418
if (use_remote) {
2419
if (use_reverse) {
2420
static const char *const msg = "--- Device API >= 21; debugging over USB ---";
2421
EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);
2422
print_line(String(msg).to_upper());
2423
2424
args.clear();
2425
args.push_back("-s");
2426
args.push_back(devices[p_device].id);
2427
args.push_back("reverse");
2428
args.push_back("--remove-all");
2429
output.clear();
2430
OS::get_singleton()->execute(adb, args, &output, &rv, true);
2431
print_verbose(output);
2432
2433
if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) {
2434
int dbg_port = EDITOR_GET("network/debug/remote_port");
2435
args.clear();
2436
args.push_back("-s");
2437
args.push_back(devices[p_device].id);
2438
args.push_back("reverse");
2439
args.push_back("tcp:" + itos(dbg_port));
2440
args.push_back("tcp:" + itos(dbg_port));
2441
2442
output.clear();
2443
OS::get_singleton()->execute(adb, args, &output, &rv, true);
2444
print_verbose(output);
2445
print_line("Reverse result: " + itos(rv));
2446
}
2447
2448
if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
2449
int fs_port = EDITOR_GET("filesystem/file_server/port");
2450
2451
args.clear();
2452
args.push_back("-s");
2453
args.push_back(devices[p_device].id);
2454
args.push_back("reverse");
2455
args.push_back("tcp:" + itos(fs_port));
2456
args.push_back("tcp:" + itos(fs_port));
2457
2458
output.clear();
2459
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2460
print_verbose(output);
2461
print_line("Reverse result2: " + itos(rv));
2462
}
2463
} else {
2464
static const char *const api_version_msg = "--- Device API < 21; debugging over Wi-Fi ---";
2465
static const char *const manual_override_msg = "--- Wi-Fi remote debug enabled in project settings; debugging over Wi-Fi ---";
2466
2467
const char *const msg = use_wifi_for_remote_debug ? manual_override_msg : api_version_msg;
2468
EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);
2469
print_line(String(msg).to_upper());
2470
}
2471
}
2472
2473
if (ep.step(TTR("Running on device..."), 3)) {
2474
CLEANUP_AND_RETURN(ERR_SKIP);
2475
}
2476
args.clear();
2477
args.push_back("-s");
2478
args.push_back(devices[p_device].id);
2479
args.push_back("shell");
2480
args.push_back("am");
2481
args.push_back("start");
2482
if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device].api_level >= 17) {
2483
args.push_back("--user");
2484
args.push_back("0");
2485
}
2486
args.push_back("-a");
2487
args.push_back("android.intent.action.MAIN");
2488
2489
// Going with implicit launch first based on the LAUNCHER category and the app's package.
2490
args.push_back("-c");
2491
args.push_back("android.intent.category.LAUNCHER");
2492
args.push_back(get_package_name(p_preset, package_name));
2493
2494
output.clear();
2495
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2496
print_verbose(output);
2497
if (err || rv != 0 || output.contains("Error: Activity not started")) {
2498
// The implicit launch failed, let's try an explicit launch by specifying the component name before giving up.
2499
const String component_name = get_package_name(p_preset, package_name) + "/com.godot.game.GodotApp";
2500
print_line("Implicit launch failed.. Trying explicit launch using", component_name);
2501
args.erase(get_package_name(p_preset, package_name));
2502
args.push_back("-n");
2503
args.push_back(component_name);
2504
2505
output.clear();
2506
err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2507
print_verbose(output);
2508
2509
if (err || rv != 0 || output.begins_with("Error: Activity not started")) {
2510
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device."));
2511
CLEANUP_AND_RETURN(ERR_CANT_CREATE);
2512
}
2513
}
2514
2515
CLEANUP_AND_RETURN(OK);
2516
#undef CLEANUP_AND_RETURN
2517
}
2518
2519
Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {
2520
return run_icon;
2521
}
2522
2523
String EditorExportPlatformAndroid::get_java_path() {
2524
String exe_ext;
2525
if (OS::get_singleton()->get_name() == "Windows") {
2526
exe_ext = ".exe";
2527
}
2528
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
2529
return java_sdk_path.path_join("bin/java" + exe_ext);
2530
}
2531
2532
String EditorExportPlatformAndroid::get_keytool_path() {
2533
String exe_ext;
2534
if (OS::get_singleton()->get_name() == "Windows") {
2535
exe_ext = ".exe";
2536
}
2537
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
2538
return java_sdk_path.path_join("bin/keytool" + exe_ext);
2539
}
2540
2541
String EditorExportPlatformAndroid::get_adb_path() {
2542
String exe_ext;
2543
if (OS::get_singleton()->get_name() == "Windows") {
2544
exe_ext = ".exe";
2545
}
2546
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2547
return sdk_path.path_join("platform-tools/adb" + exe_ext);
2548
}
2549
2550
String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_check_executes) {
2551
if (p_target_sdk == -1) {
2552
p_target_sdk = DEFAULT_TARGET_SDK_VERSION;
2553
}
2554
String exe_ext;
2555
if (OS::get_singleton()->get_name() == "Windows") {
2556
exe_ext = ".bat";
2557
}
2558
String apksigner_command_name = "apksigner" + exe_ext;
2559
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2560
String apksigner_path;
2561
2562
Error errn;
2563
String build_tools_dir = sdk_path.path_join("build-tools");
2564
Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn);
2565
if (errn != OK) {
2566
print_error("Unable to open Android 'build-tools' directory.");
2567
return apksigner_path;
2568
}
2569
2570
// There are additional versions directories we need to go through.
2571
Vector<String> dir_list = da->get_directories();
2572
2573
// We need to use the version of build_tools that matches the Target SDK
2574
// If somehow we can't find that, we see if a version between 28 and the default target SDK exists.
2575
// We need to avoid versions <= 27 because they fail on Java versions >9
2576
// If we can't find that, we just use the first valid version.
2577
Vector<String> ideal_versions;
2578
Vector<String> other_versions;
2579
Vector<String> versions;
2580
bool found_target_sdk = false;
2581
// We only allow for versions <= 27 if specifically set
2582
int min_version = p_target_sdk <= 27 ? p_target_sdk : 28;
2583
for (String sub_dir : dir_list) {
2584
if (!sub_dir.begins_with(".")) {
2585
Vector<String> ver_numbers = sub_dir.split(".");
2586
// Dir not a version number, will use as last resort
2587
if (!ver_numbers.size() || !ver_numbers[0].is_valid_int()) {
2588
other_versions.push_back(sub_dir);
2589
continue;
2590
}
2591
int ver_number = ver_numbers[0].to_int();
2592
if (ver_number == p_target_sdk) {
2593
found_target_sdk = true;
2594
//ensure this is in front of the ones we check
2595
versions.push_back(sub_dir);
2596
} else {
2597
if (ver_number >= min_version && ver_number <= DEFAULT_TARGET_SDK_VERSION) {
2598
ideal_versions.push_back(sub_dir);
2599
} else {
2600
other_versions.push_back(sub_dir);
2601
}
2602
}
2603
}
2604
}
2605
// we will check ideal versions first, then other versions.
2606
versions.append_array(ideal_versions);
2607
versions.append_array(other_versions);
2608
2609
if (!versions.size()) {
2610
print_error("Unable to find the 'apksigner' tool.");
2611
return apksigner_path;
2612
}
2613
2614
int i;
2615
bool failed = false;
2616
String version_to_use;
2617
2618
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
2619
if (!java_sdk_path.is_empty()) {
2620
OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
2621
2622
#ifdef UNIX_ENABLED
2623
String env_path = OS::get_singleton()->get_environment("PATH");
2624
if (!env_path.contains(java_sdk_path)) {
2625
OS::get_singleton()->set_environment("PATH", java_sdk_path + "/bin:" + env_path);
2626
}
2627
#endif
2628
}
2629
2630
List<String> args;
2631
args.push_back("--version");
2632
String output;
2633
int retval;
2634
Error err;
2635
for (i = 0; i < versions.size(); i++) {
2636
// Check if the tool is here.
2637
apksigner_path = build_tools_dir.path_join(versions[i]).path_join(apksigner_command_name);
2638
if (FileAccess::exists(apksigner_path)) {
2639
version_to_use = versions[i];
2640
// If we aren't exporting, just break here.
2641
if (!p_check_executes) {
2642
break;
2643
}
2644
// we only check to see if it executes on export because it is slow to load
2645
err = OS::get_singleton()->execute(apksigner_path, args, &output, &retval, false);
2646
if (err || retval) {
2647
failed = true;
2648
} else {
2649
break;
2650
}
2651
}
2652
}
2653
if (i == versions.size()) {
2654
if (failed) {
2655
print_error("All located 'apksigner' tools in " + build_tools_dir + " failed to execute");
2656
return "<FAILED>";
2657
} else {
2658
print_error("Unable to find the 'apksigner' tool.");
2659
return "";
2660
}
2661
}
2662
if (!found_target_sdk) {
2663
print_line("Could not find version of build tools that matches Target SDK, using " + version_to_use);
2664
} else if (failed && found_target_sdk) {
2665
print_line("Version of build tools that matches Target SDK failed to execute, using " + version_to_use);
2666
}
2667
2668
return apksigner_path;
2669
}
2670
2671
static bool has_valid_keystore_credentials(String &r_error_str, const String &p_keystore, const String &p_username, const String &p_password, const String &p_type) {
2672
String output;
2673
List<String> args;
2674
args.push_back("-list");
2675
args.push_back("-keystore");
2676
args.push_back(p_keystore);
2677
args.push_back("-storepass");
2678
args.push_back(p_password);
2679
args.push_back("-alias");
2680
args.push_back(p_username);
2681
String keytool_path = EditorExportPlatformAndroid::get_keytool_path();
2682
Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);
2683
String keytool_error = "keytool error:";
2684
bool valid = output.substr(0, keytool_error.length()) != keytool_error;
2685
2686
if (error != OK) {
2687
r_error_str = TTR("Error: There was a problem validating the keystore username and password");
2688
return false;
2689
}
2690
if (!valid) {
2691
r_error_str = TTR(p_type + " Username and/or Password is invalid for the given " + p_type + " Keystore");
2692
return false;
2693
}
2694
r_error_str = "";
2695
return true;
2696
}
2697
2698
bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) {
2699
String dk = _get_keystore_path(p_preset, true);
2700
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2701
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2702
String rk = _get_keystore_path(p_preset, false);
2703
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
2704
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
2705
2706
bool valid = true;
2707
if (!dk.is_empty() && !dk_user.is_empty() && !dk_password.is_empty()) {
2708
String err = "";
2709
valid = has_valid_keystore_credentials(err, dk, dk_user, dk_password, "Debug");
2710
r_error += err;
2711
}
2712
if (!rk.is_empty() && !rk_user.is_empty() && !rk_password.is_empty()) {
2713
String err = "";
2714
valid = has_valid_keystore_credentials(err, rk, rk_user, rk_password, "Release");
2715
r_error += err;
2716
}
2717
return valid;
2718
}
2719
2720
#ifdef MODULE_MONO_ENABLED
2721
static uint64_t _last_validate_tfm_time = 0;
2722
static String _last_validate_tfm = "";
2723
2724
bool _validate_dotnet_tfm(const String &required_tfm, String &r_error) {
2725
String assembly_name = Path::get_csharp_project_name();
2726
String project_path = ProjectSettings::get_singleton()->globalize_path("res://" + assembly_name + ".csproj");
2727
2728
if (!FileAccess::exists(project_path)) {
2729
return true;
2730
}
2731
2732
uint64_t modified_time = FileAccess::get_modified_time(project_path);
2733
String tfm;
2734
2735
if (modified_time == _last_validate_tfm_time) {
2736
tfm = _last_validate_tfm;
2737
} else {
2738
String pipe;
2739
List<String> args;
2740
args.push_back("build");
2741
args.push_back(project_path);
2742
args.push_back("/p:GodotTargetPlatform=android");
2743
args.push_back("--getProperty:TargetFramework");
2744
2745
int exitcode;
2746
Error err = OS::get_singleton()->execute("dotnet", args, &pipe, &exitcode, true);
2747
if (err != OK || exitcode != 0) {
2748
if (err != OK) {
2749
WARN_PRINT("Failed to execute dotnet command. Error " + String(error_names[err]));
2750
} else if (exitcode != 0) {
2751
print_line(pipe);
2752
WARN_PRINT("dotnet command exited with code " + itos(exitcode) + ". See output above for more details.");
2753
}
2754
r_error += vformat(TTR("Unable to determine the C# project's TFM, it may be incompatible. The export template only supports '%s'. Make sure the project targets '%s' or consider using gradle builds instead."), required_tfm, required_tfm) + "\n";
2755
return true;
2756
} else {
2757
tfm = pipe.strip_edges();
2758
_last_validate_tfm_time = modified_time;
2759
_last_validate_tfm = tfm;
2760
}
2761
}
2762
2763
if (tfm != required_tfm) {
2764
r_error += vformat(TTR("C# project targets '%s' but the export template only supports '%s'. Consider using gradle builds instead."), tfm, required_tfm) + "\n";
2765
return false;
2766
}
2767
2768
return true;
2769
}
2770
#endif
2771
2772
bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
2773
String err;
2774
bool valid = false;
2775
const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2776
2777
#ifdef MODULE_MONO_ENABLED
2778
// Android export is still a work in progress, keep a message as a warning.
2779
err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n";
2780
2781
if (!gradle_build_enabled) {
2782
// For template exports we only support .NET 9 because the template
2783
// includes .jar dependencies that may only be compatible with .NET 9.
2784
if (!_validate_dotnet_tfm("net9.0", err)) {
2785
r_error = err;
2786
return false;
2787
}
2788
}
2789
#endif
2790
2791
// Look for export templates (first official, and if defined custom templates).
2792
2793
if (!gradle_build_enabled) {
2794
String template_err;
2795
bool dvalid = false;
2796
bool rvalid = false;
2797
bool has_export_templates = false;
2798
2799
if (p_preset->get("custom_template/debug") != "") {
2800
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
2801
if (!dvalid) {
2802
template_err += TTR("Custom debug template not found.") + "\n";
2803
}
2804
has_export_templates |= dvalid;
2805
} else {
2806
has_export_templates |= exists_export_template("android_debug.apk", &template_err);
2807
}
2808
2809
if (p_preset->get("custom_template/release") != "") {
2810
rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
2811
if (!rvalid) {
2812
template_err += TTR("Custom release template not found.") + "\n";
2813
}
2814
has_export_templates |= rvalid;
2815
} else {
2816
has_export_templates |= exists_export_template("android_release.apk", &template_err);
2817
}
2818
2819
r_missing_templates = !has_export_templates;
2820
valid = dvalid || rvalid || has_export_templates;
2821
if (!valid) {
2822
err += template_err;
2823
}
2824
} else {
2825
#ifdef ANDROID_ENABLED
2826
err += TTR("Gradle build is not supported for the Android editor.") + "\n";
2827
valid = false;
2828
#else
2829
// Validate the custom gradle android source template.
2830
bool android_source_template_valid = false;
2831
const String android_source_template = p_preset->get("gradle_build/android_source_template");
2832
if (!android_source_template.is_empty()) {
2833
android_source_template_valid = FileAccess::exists(android_source_template);
2834
if (!android_source_template_valid) {
2835
err += TTR("Custom Android source template not found.") + "\n";
2836
}
2837
}
2838
2839
// Validate the installed build template.
2840
bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle"));
2841
if (!installed_android_build_template) {
2842
if (!android_source_template_valid) {
2843
r_missing_templates = !exists_export_template("android_source.zip", &err);
2844
}
2845
err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";
2846
} else {
2847
r_missing_templates = false;
2848
}
2849
2850
valid = installed_android_build_template && !r_missing_templates;
2851
#endif
2852
}
2853
2854
// Validate the rest of the export configuration.
2855
2856
String dk = _get_keystore_path(p_preset, true);
2857
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2858
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2859
2860
if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) {
2861
valid = false;
2862
err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n";
2863
}
2864
2865
// Use OR to make the export UI able to show this error.
2866
if ((p_debug || !dk.is_empty()) && !FileAccess::exists(dk)) {
2867
dk = EDITOR_GET("export/android/debug_keystore");
2868
if (!FileAccess::exists(dk)) {
2869
valid = false;
2870
err += TTR("Debug keystore not configured in the Editor Settings nor in the preset.") + "\n";
2871
}
2872
}
2873
2874
String rk = _get_keystore_path(p_preset, false);
2875
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
2876
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
2877
2878
if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) {
2879
valid = false;
2880
err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n";
2881
}
2882
2883
if (!p_debug && !rk.is_empty() && !FileAccess::exists(rk)) {
2884
valid = false;
2885
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
2886
}
2887
2888
#ifndef ANDROID_ENABLED
2889
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
2890
if (java_sdk_path.is_empty()) {
2891
err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
2892
valid = false;
2893
} else {
2894
// Validate the given path by checking that `java` is present under the `bin` directory.
2895
Error errn;
2896
// Check for the bin directory.
2897
Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
2898
if (errn != OK) {
2899
err += TTR("Invalid Java SDK path in Editor Settings.") + " ";
2900
err += TTR("Missing 'bin' directory!");
2901
err += "\n";
2902
valid = false;
2903
} else {
2904
// Check for the `java` command.
2905
String java_path = get_java_path();
2906
if (!FileAccess::exists(java_path)) {
2907
err += TTR("Unable to find 'java' command using the Java SDK path.") + " ";
2908
err += TTR("Please check the Java SDK directory specified in Editor Settings.");
2909
err += "\n";
2910
valid = false;
2911
}
2912
}
2913
}
2914
2915
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2916
if (sdk_path.is_empty()) {
2917
err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
2918
valid = false;
2919
} else {
2920
Error errn;
2921
// Check for the platform-tools directory.
2922
Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);
2923
if (errn != OK) {
2924
err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
2925
err += TTR("Missing 'platform-tools' directory!");
2926
err += "\n";
2927
valid = false;
2928
}
2929
2930
// Validate that adb is available.
2931
String adb_path = get_adb_path();
2932
if (!FileAccess::exists(adb_path)) {
2933
err += TTR("Unable to find Android SDK platform-tools' adb command.") + " ";
2934
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2935
err += "\n";
2936
valid = false;
2937
}
2938
2939
// Check for the build-tools directory.
2940
Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);
2941
if (errn != OK) {
2942
err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
2943
err += TTR("Missing 'build-tools' directory!");
2944
err += "\n";
2945
valid = false;
2946
}
2947
2948
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
2949
if (!target_sdk_version.is_valid_int()) {
2950
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
2951
}
2952
// Validate that apksigner is available.
2953
String apksigner_path = get_apksigner_path(target_sdk_version.to_int());
2954
if (!FileAccess::exists(apksigner_path)) {
2955
err += TTR("Unable to find Android SDK build-tools' apksigner command.") + " ";
2956
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2957
err += "\n";
2958
valid = false;
2959
}
2960
}
2961
#endif
2962
2963
if (!err.is_empty()) {
2964
r_error = err;
2965
}
2966
2967
return valid;
2968
}
2969
2970
bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
2971
String err;
2972
bool valid = true;
2973
2974
List<ExportOption> options;
2975
get_export_options(&options);
2976
for (const EditorExportPlatform::ExportOption &E : options) {
2977
if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {
2978
String warn = get_export_option_warning(p_preset.ptr(), E.option.name);
2979
if (!warn.is_empty()) {
2980
err += warn + "\n";
2981
if (E.required) {
2982
valid = false;
2983
}
2984
}
2985
}
2986
}
2987
2988
if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
2989
valid = false;
2990
}
2991
2992
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2993
if (gradle_build_enabled) {
2994
String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version");
2995
Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ);
2996
if (f.is_valid()) {
2997
String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
2998
String installed_version = f->get_line().strip_edges();
2999
if (current_version != installed_version) {
3000
err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version);
3001
err += "\n";
3002
}
3003
}
3004
} else {
3005
if (_is_transparency_allowed(p_preset)) {
3006
// Warning only, so don't override `valid`.
3007
err += vformat(TTR("\"Use Gradle Build\" is required for transparent background on Android"));
3008
err += "\n";
3009
}
3010
}
3011
3012
String target_sdk_str = p_preset->get("gradle_build/target_sdk");
3013
int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
3014
if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
3015
if (target_sdk_str.is_valid_int()) {
3016
target_sdk_int = target_sdk_str.to_int();
3017
if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) {
3018
// Warning only, so don't override `valid`.
3019
err += vformat(TTR("\"Target SDK\" %d is higher than the default version %d. This may work, but wasn't tested and may be unstable."), target_sdk_int, DEFAULT_TARGET_SDK_VERSION);
3020
err += "\n";
3021
}
3022
}
3023
}
3024
3025
String current_renderer = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");
3026
if (current_renderer == "forward_plus") {
3027
// Warning only, so don't override `valid`.
3028
err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer);
3029
err += "\n";
3030
}
3031
3032
String package_name = p_preset->get("package/unique_name");
3033
if (package_name.contains("$genname") && !is_project_name_valid(p_preset)) {
3034
// Warning only, so don't override `valid`.
3035
err += vformat(TTR("The project name does not meet the requirement for the package name format and will be updated to \"%s\". Please explicitly specify the package name if needed."), get_valid_basename(p_preset));
3036
err += "\n";
3037
}
3038
3039
r_error = err;
3040
return valid;
3041
}
3042
3043
List<String> EditorExportPlatformAndroid::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
3044
List<String> list;
3045
list.push_back("apk");
3046
list.push_back("aab");
3047
return list;
3048
}
3049
3050
String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
3051
int version_code = p_preset->get("version/code");
3052
String package_name = p_preset->get("package/unique_name");
3053
String apk_file_name = "main." + itos(version_code) + "." + get_package_name(p_preset, package_name) + ".obb";
3054
String fullpath = p_path.get_base_dir().path_join(apk_file_name);
3055
return fullpath;
3056
}
3057
3058
Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
3059
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
3060
Error err = save_pack(p_preset, p_debug, fullpath);
3061
return err;
3062
}
3063
3064
void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags) {
3065
String cmdline = p_preset->get("command_line/extra_args");
3066
Vector<String> command_line_strings = cmdline.strip_edges().split(" ");
3067
for (int i = 0; i < command_line_strings.size(); i++) {
3068
if (command_line_strings[i].strip_edges().length() == 0) {
3069
command_line_strings.remove_at(i);
3070
i--;
3071
}
3072
}
3073
3074
command_line_strings.append_array(gen_export_flags(p_flags));
3075
3076
bool apk_expansion = p_preset->get("apk_expansion/enable");
3077
if (apk_expansion) {
3078
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
3079
String apk_expansion_public_key = p_preset->get("apk_expansion/public_key");
3080
3081
command_line_strings.push_back("--use_apk_expansion");
3082
command_line_strings.push_back("--apk_expansion_md5");
3083
command_line_strings.push_back(FileAccess::get_md5(fullpath));
3084
command_line_strings.push_back("--apk_expansion_key");
3085
command_line_strings.push_back(apk_expansion_public_key.strip_edges());
3086
}
3087
3088
int xr_mode_index = p_preset->get("xr_features/xr_mode");
3089
if (xr_mode_index == XR_MODE_OPENXR) {
3090
command_line_strings.push_back("--xr_mode_openxr");
3091
} else { // XRMode.REGULAR is the default.
3092
command_line_strings.push_back("--xr_mode_regular");
3093
3094
// Also override the 'xr/openxr/enabled' project setting.
3095
// This is useful for multi-platforms projects supporting both XR and non-XR devices. The project would need
3096
// to enable openxr for development, and would create multiple XR and non-XR export presets.
3097
// These command line args ensure that the non-XR export presets will have openxr disabled.
3098
command_line_strings.push_back("--xr-mode");
3099
command_line_strings.push_back("off");
3100
}
3101
3102
bool immersive = p_preset->get("screen/immersive_mode");
3103
if (immersive) {
3104
command_line_strings.push_back("--fullscreen");
3105
}
3106
3107
bool edge_to_edge = p_preset->get("screen/edge_to_edge");
3108
if (edge_to_edge) {
3109
command_line_strings.push_back("--edge_to_edge");
3110
}
3111
3112
String background_color = "#" + p_preset->get("screen/background_color").operator Color().to_html(false);
3113
3114
// For Gradle build, _fix_themes_xml() sets background to transparent if _is_transparency_allowed().
3115
// Overriding to transparent here too as it's used as fallback for system bar appearance.
3116
if (_is_transparency_allowed(p_preset) && p_preset->get("gradle_build/use_gradle_build")) {
3117
background_color = "#00000000";
3118
}
3119
command_line_strings.push_back("--background_color");
3120
command_line_strings.push_back(background_color);
3121
3122
bool debug_opengl = p_preset->get("graphics/opengl_debug");
3123
if (debug_opengl) {
3124
command_line_strings.push_back("--debug_opengl");
3125
}
3126
3127
if (command_line_strings.size()) {
3128
r_command_line_flags.resize(4);
3129
encode_uint32(command_line_strings.size(), &r_command_line_flags.write[0]);
3130
for (int i = 0; i < command_line_strings.size(); i++) {
3131
print_line(itos(i) + " param: " + command_line_strings[i]);
3132
CharString command_line_argument = command_line_strings[i].utf8();
3133
int base = r_command_line_flags.size();
3134
int length = command_line_argument.length();
3135
if (length == 0) {
3136
continue;
3137
}
3138
r_command_line_flags.resize(base + 4 + length);
3139
encode_uint32(length, &r_command_line_flags.write[base]);
3140
memcpy(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length);
3141
}
3142
}
3143
}
3144
3145
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
3146
int export_format = int(p_preset->get("gradle_build/export_format"));
3147
if (export_format == EXPORT_FORMAT_AAB) {
3148
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
3149
return FAILED;
3150
}
3151
3152
String keystore;
3153
String password;
3154
String user;
3155
if (p_debug) {
3156
keystore = _get_keystore_path(p_preset, true);
3157
password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
3158
user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
3159
3160
if (keystore.is_empty()) {
3161
keystore = EDITOR_GET("export/android/debug_keystore");
3162
password = EDITOR_GET("export/android/debug_keystore_pass");
3163
user = EDITOR_GET("export/android/debug_keystore_user");
3164
}
3165
3166
if (ep.step(TTR("Signing debug APK..."), 104)) {
3167
return ERR_SKIP;
3168
}
3169
} else {
3170
keystore = _get_keystore_path(p_preset, false);
3171
password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
3172
user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
3173
3174
if (ep.step(TTR("Signing release APK..."), 104)) {
3175
return ERR_SKIP;
3176
}
3177
}
3178
3179
if (!FileAccess::exists(keystore)) {
3180
if (p_debug) {
3181
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));
3182
} else {
3183
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));
3184
}
3185
return ERR_FILE_CANT_OPEN;
3186
}
3187
3188
String apk_path = export_path;
3189
if (apk_path.is_relative_path()) {
3190
apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);
3191
}
3192
apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();
3193
3194
Error err;
3195
#ifdef ANDROID_ENABLED
3196
err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);
3197
if (err != OK) {
3198
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));
3199
return err;
3200
}
3201
#else
3202
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
3203
if (!target_sdk_version.is_valid_int()) {
3204
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
3205
}
3206
3207
String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
3208
print_verbose("Starting signing of the APK binary using " + apksigner);
3209
if (apksigner == "<FAILED>") {
3210
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."));
3211
return OK;
3212
}
3213
if (!FileAccess::exists(apksigner)) {
3214
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."));
3215
return OK;
3216
}
3217
3218
String output;
3219
List<String> args;
3220
args.push_back("sign");
3221
args.push_back("--verbose");
3222
args.push_back("--ks");
3223
args.push_back(keystore);
3224
args.push_back("--ks-pass");
3225
args.push_back("pass:" + password);
3226
args.push_back("--ks-key-alias");
3227
args.push_back(user);
3228
args.push_back(apk_path);
3229
if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
3230
// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
3231
print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
3232
} else {
3233
List<String> redacted_args = List<String>(args);
3234
redacted_args.find(keystore)->set("<REDACTED>");
3235
redacted_args.find("pass:" + password)->set("pass:<REDACTED>");
3236
redacted_args.find(user)->set("<REDACTED>");
3237
print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
3238
}
3239
int retval;
3240
err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
3241
if (err != OK) {
3242
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
3243
return err;
3244
}
3245
// By design, apksigner does not output credentials in its output unless --verbose is used
3246
print_line(output);
3247
if (retval) {
3248
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval));
3249
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
3250
return ERR_CANT_CREATE;
3251
}
3252
#endif
3253
3254
if (ep.step(TTR("Verifying APK..."), 105)) {
3255
return ERR_SKIP;
3256
}
3257
3258
#ifdef ANDROID_ENABLED
3259
err = OS_Android::get_singleton()->verify_apk(apk_path);
3260
if (err != OK) {
3261
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));
3262
return err;
3263
}
3264
#else
3265
args.clear();
3266
args.push_back("verify");
3267
args.push_back("--verbose");
3268
args.push_back(apk_path);
3269
if (p_debug) {
3270
print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
3271
}
3272
3273
output.clear();
3274
err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
3275
if (err != OK) {
3276
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
3277
return err;
3278
}
3279
print_verbose(output);
3280
if (retval) {
3281
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));
3282
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
3283
return ERR_CANT_CREATE;
3284
}
3285
#endif
3286
3287
print_verbose("Successfully completed signing build.");
3288
3289
#ifdef ANDROID_ENABLED
3290
bool prompt_apk_install = EDITOR_GET("export/android/install_exported_apk");
3291
if (prompt_apk_install) {
3292
OS_Android::get_singleton()->shell_open(apk_path);
3293
}
3294
#endif
3295
3296
return OK;
3297
}
3298
3299
void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) {
3300
Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);
3301
String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
3302
3303
// Clear the APK assets directory
3304
String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY);
3305
if (da_res->dir_exists(apk_assets_directory)) {
3306
print_verbose("Clearing APK assets directory...");
3307
Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory);
3308
ERR_FAIL_COND(da_assets.is_null());
3309
3310
da_assets->erase_contents_recursive();
3311
da_res->remove(apk_assets_directory);
3312
}
3313
3314
// Clear the AAB assets directory
3315
String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY);
3316
if (da_res->dir_exists(aab_assets_directory)) {
3317
print_verbose("Clearing AAB assets directory...");
3318
Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory);
3319
ERR_FAIL_COND(da_assets.is_null());
3320
3321
da_assets->erase_contents_recursive();
3322
da_res->remove(aab_assets_directory);
3323
}
3324
}
3325
3326
void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) {
3327
print_verbose("Removing previously installed libraries...");
3328
Error error;
3329
String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error);
3330
if (error || libs_json.is_empty()) {
3331
print_verbose("No previously installed libraries found");
3332
return;
3333
}
3334
3335
JSON json;
3336
error = json.parse(libs_json);
3337
ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message());
3338
3339
Vector<String> libs = json.get_data();
3340
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
3341
for (int i = 0; i < libs.size(); i++) {
3342
print_verbose("Removing previously installed library " + libs[i]);
3343
da->remove(libs[i]);
3344
}
3345
da->remove(p_gdextension_libs_path);
3346
}
3347
3348
String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {
3349
String ret;
3350
for (List<String>::ConstIterator itr = p_parts.begin(); itr != p_parts.end(); ++itr) {
3351
if (itr != p_parts.begin()) {
3352
ret += p_separator;
3353
}
3354
ret += *itr;
3355
}
3356
return ret;
3357
}
3358
3359
String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformAndroid::ABI> &p_parts, const String &p_separator, bool p_use_arch) {
3360
String ret;
3361
for (int i = 0; i < p_parts.size(); ++i) {
3362
if (i > 0) {
3363
ret += p_separator;
3364
}
3365
ret += (p_use_arch) ? p_parts[i].arch : p_parts[i].abi;
3366
}
3367
return ret;
3368
}
3369
3370
String EditorExportPlatformAndroid::_get_deprecated_plugins_names(const Ref<EditorExportPreset> &p_preset) const {
3371
Vector<String> names;
3372
3373
#ifndef DISABLE_DEPRECATED
3374
PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);
3375
#endif // DISABLE_DEPRECATED
3376
3377
String plugins_names = String("|").join(names);
3378
return plugins_names;
3379
}
3380
3381
String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const {
3382
Vector<String> names;
3383
3384
#ifndef DISABLE_DEPRECATED
3385
PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);
3386
#endif // DISABLE_DEPRECATED
3387
3388
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3389
for (int i = 0; i < export_plugins.size(); i++) {
3390
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3391
names.push_back(export_plugins[i]->get_name());
3392
}
3393
}
3394
3395
String plugins_names = String("|").join(names);
3396
return plugins_names;
3397
}
3398
3399
String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const {
3400
String absolute_path;
3401
if (!p_android_library_path.is_empty()) {
3402
if (p_android_library_path.is_absolute_path()) {
3403
absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path);
3404
} else {
3405
const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path);
3406
absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path);
3407
}
3408
}
3409
return absolute_path;
3410
}
3411
3412
bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {
3413
bool first_build = last_gradle_build_time == 0;
3414
bool have_plugins_changed = false;
3415
String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
3416
bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir;
3417
3418
String plugin_names = _get_plugins_names(p_preset);
3419
3420
if (!first_build) {
3421
have_plugins_changed = plugin_names != last_plugin_names;
3422
#ifndef DISABLE_DEPRECATED
3423
if (!have_plugins_changed) {
3424
Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
3425
for (int i = 0; i < enabled_plugins.size(); i++) {
3426
if (enabled_plugins.get(i).last_updated > last_gradle_build_time) {
3427
have_plugins_changed = true;
3428
break;
3429
}
3430
}
3431
}
3432
#endif // DISABLE_DEPRECATED
3433
}
3434
3435
last_gradle_build_time = OS::get_singleton()->get_unix_time();
3436
last_gradle_build_dir = gradle_build_dir;
3437
last_plugin_names = plugin_names;
3438
3439
return have_plugins_changed || has_build_dir_changed || first_build;
3440
}
3441
3442
Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
3443
int export_format = int(p_preset->get("gradle_build/export_format"));
3444
bool should_sign = p_preset->get("package/signed");
3445
return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);
3446
}
3447
3448
Error EditorExportPlatformAndroid::_generate_sparse_pck_metadata(const Ref<EditorExportPreset> &p_preset, PackData &p_pack_data, Vector<uint8_t> &r_data) {
3449
Error err;
3450
Ref<FileAccess> ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export_index", "tmp", false, &err);
3451
if (err != OK) {
3452
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not create temporary file!"));
3453
return err;
3454
}
3455
int64_t pck_start_pos = ftmp->get_position();
3456
uint64_t file_base_ofs = 0;
3457
uint64_t dir_base_ofs = 0;
3458
EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs);
3459
3460
// Write directory.
3461
uint64_t dir_offset = ftmp->get_position();
3462
ftmp->seek(dir_base_ofs);
3463
ftmp->store_64(dir_offset - pck_start_pos);
3464
ftmp->seek(dir_offset);
3465
3466
Vector<uint8_t> key;
3467
if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) {
3468
String script_key = _get_script_encryption_key(p_preset);
3469
key.resize(32);
3470
if (script_key.length() == 64) {
3471
for (int i = 0; i < 32; i++) {
3472
int v = 0;
3473
if (i * 2 < script_key.length()) {
3474
char32_t ct = script_key[i * 2];
3475
if (is_digit(ct)) {
3476
ct = ct - '0';
3477
} else if (ct >= 'a' && ct <= 'f') {
3478
ct = 10 + ct - 'a';
3479
}
3480
v |= ct << 4;
3481
}
3482
3483
if (i * 2 + 1 < script_key.length()) {
3484
char32_t ct = script_key[i * 2 + 1];
3485
if (is_digit(ct)) {
3486
ct = ct - '0';
3487
} else if (ct >= 'a' && ct <= 'f') {
3488
ct = 10 + ct - 'a';
3489
}
3490
v |= ct;
3491
}
3492
key.write[i] = v;
3493
}
3494
}
3495
}
3496
3497
if (!EditorExportPlatform::_encrypt_and_store_directory(ftmp, p_pack_data, key, p_preset->get_seed(), 0)) {
3498
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file."));
3499
return ERR_CANT_CREATE;
3500
}
3501
3502
r_data.resize(ftmp->get_length());
3503
ftmp->seek(0);
3504
ftmp->get_buffer(r_data.ptrw(), r_data.size());
3505
ftmp.unref();
3506
3507
return OK;
3508
}
3509
3510
Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags) {
3511
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
3512
3513
const String base_dir = p_path.get_base_dir();
3514
if (!DirAccess::exists(base_dir)) {
3515
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));
3516
return ERR_FILE_BAD_PATH;
3517
}
3518
3519
String src_apk;
3520
Error err;
3521
3522
EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
3523
3524
bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
3525
String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";
3526
bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG);
3527
bool apk_expansion = p_preset->get("apk_expansion/enable");
3528
Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
3529
3530
print_verbose("Exporting for Android...");
3531
print_verbose("- debug build: " + bool_to_string(p_debug));
3532
print_verbose("- export path: " + p_path);
3533
print_verbose("- export format: " + itos(export_format));
3534
print_verbose("- sign build: " + bool_to_string(should_sign));
3535
print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build));
3536
print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion));
3537
print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false));
3538
print_verbose("- export filter: " + itos(p_preset->get_export_filter()));
3539
print_verbose("- include filter: " + p_preset->get_include_filter());
3540
print_verbose("- exclude filter: " + p_preset->get_exclude_filter());
3541
3542
Ref<Image> main_image;
3543
Ref<Image> foreground;
3544
Ref<Image> background;
3545
Ref<Image> monochrome;
3546
3547
load_icon_refs(p_preset, main_image, foreground, background, monochrome);
3548
3549
Vector<uint8_t> command_line_flags;
3550
// Write command line flags into the command_line_flags variable.
3551
get_command_line_flags(p_preset, p_path, p_flags, command_line_flags);
3552
3553
if (export_format == EXPORT_FORMAT_AAB) {
3554
if (!p_path.ends_with(".aab")) {
3555
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
3556
return ERR_UNCONFIGURED;
3557
}
3558
if (apk_expansion) {
3559
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));
3560
return ERR_UNCONFIGURED;
3561
}
3562
}
3563
if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {
3564
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));
3565
return ERR_UNCONFIGURED;
3566
}
3567
if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) {
3568
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));
3569
return ERR_UNCONFIGURED;
3570
}
3571
String err_string;
3572
if (!has_valid_username_and_password(p_preset, err_string)) {
3573
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string));
3574
return ERR_UNCONFIGURED;
3575
}
3576
3577
if (use_gradle_build) {
3578
print_verbose("Starting gradle build...");
3579
//test that installed build version is alright
3580
{
3581
print_verbose("Checking build version...");
3582
String gradle_base_directory = gradle_build_directory.get_base_dir();
3583
Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ);
3584
if (f.is_null()) {
3585
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a gradle built template, but no version info for it exists. Please reinstall from the 'Project' menu."));
3586
return ERR_UNCONFIGURED;
3587
}
3588
String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
3589
String installed_version = f->get_line().strip_edges();
3590
print_verbose("- build version: " + installed_version);
3591
if (installed_version != current_version) {
3592
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version));
3593
return ERR_UNCONFIGURED;
3594
}
3595
}
3596
const String assets_directory = get_assets_directory(p_preset, export_format);
3597
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
3598
if (java_sdk_path.is_empty()) {
3599
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
3600
return ERR_UNCONFIGURED;
3601
}
3602
print_verbose("Java sdk path: " + java_sdk_path);
3603
3604
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
3605
if (sdk_path.is_empty()) {
3606
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));
3607
return ERR_UNCONFIGURED;
3608
}
3609
print_verbose("Android sdk path: " + sdk_path);
3610
3611
// TODO: should we use "package/name" or "application/config/name"?
3612
String project_name = get_project_name(p_preset, p_preset->get("package/name"));
3613
err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory, get_project_setting(p_preset, "application/config/name_localized")); //project name localization.
3614
if (err != OK) {
3615
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));
3616
}
3617
// Copies the project icon files into the appropriate Gradle project directory.
3618
_copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome);
3619
// Write an AndroidManifest.xml file into the Gradle project directory.
3620
_write_tmp_manifest(p_preset, p_give_internet, p_debug);
3621
// Modify res/values/themes.xml file.
3622
_fix_themes_xml(p_preset);
3623
3624
//stores all the project files inside the Gradle project directory. Also includes all ABIs
3625
_clear_assets_directory(p_preset);
3626
String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);
3627
_remove_copied_libs(gdextension_libs_path);
3628
if (!apk_expansion) {
3629
print_verbose("Exporting project files...");
3630
CustomExportData user_data;
3631
user_data.assets_directory = assets_directory;
3632
user_data.libs_directory = gradle_build_directory.path_join("libs");
3633
user_data.debug = p_debug;
3634
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
3635
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);
3636
} else {
3637
user_data.pd.path = "assets.sparsepck";
3638
user_data.pd.use_sparse_pck = true;
3639
err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);
3640
3641
Vector<uint8_t> enc_data;
3642
err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data);
3643
if (err != OK) {
3644
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
3645
return err;
3646
}
3647
3648
err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data);
3649
if (err != OK) {
3650
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!"));
3651
return err;
3652
}
3653
}
3654
if (err != OK) {
3655
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
3656
return err;
3657
}
3658
if (user_data.libs.size() > 0) {
3659
Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
3660
fa->store_string(JSON::stringify(user_data.libs, "\t"));
3661
}
3662
} else {
3663
print_verbose("Saving apk expansion file...");
3664
err = save_apk_expansion_file(p_preset, p_debug, p_path);
3665
if (err != OK) {
3666
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
3667
return err;
3668
}
3669
}
3670
print_verbose("Storing command line flags...");
3671
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
3672
3673
print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
3674
OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
3675
3676
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
3677
OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
3678
String build_command;
3679
3680
#ifdef WINDOWS_ENABLED
3681
build_command = "gradlew.bat";
3682
#else
3683
build_command = "gradlew";
3684
#endif
3685
3686
String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory);
3687
build_command = build_path.path_join(build_command);
3688
3689
String package_name = get_package_name(p_preset, p_preset->get("package/unique_name"));
3690
String version_code = itos(p_preset->get("version/code"));
3691
String version_name = p_preset->get_version("version/name");
3692
String min_sdk_version = p_preset->get("gradle_build/min_sdk");
3693
if (!min_sdk_version.is_valid_int()) {
3694
min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION);
3695
}
3696
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
3697
if (!target_sdk_version.is_valid_int()) {
3698
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
3699
}
3700
String enabled_abi_string = join_abis(enabled_abis, "|", false);
3701
String sign_flag = bool_to_string(should_sign);
3702
String zipalign_flag = "true";
3703
String compress_native_libraries_flag = bool_to_string(p_preset->get("gradle_build/compress_native_libraries"));
3704
3705
Vector<String> android_libraries;
3706
Vector<String> android_dependencies;
3707
Vector<String> android_dependencies_maven_repos;
3708
3709
#ifndef DISABLE_DEPRECATED
3710
Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
3711
PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries);
3712
PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies);
3713
PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);
3714
#endif // DISABLE_DEPRECATED
3715
3716
bool has_dotnet_project = false;
3717
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3718
for (int i = 0; i < export_plugins.size(); i++) {
3719
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3720
PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug);
3721
for (int k = 0; k < export_plugin_android_libraries.size(); k++) {
3722
const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]);
3723
if (!resolved_android_library_path.is_empty()) {
3724
android_libraries.push_back(resolved_android_library_path);
3725
}
3726
}
3727
3728
PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug);
3729
android_dependencies.append_array(export_plugin_android_dependencies);
3730
3731
PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);
3732
android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);
3733
}
3734
3735
PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug);
3736
if (features.has("dotnet")) {
3737
has_dotnet_project = true;
3738
}
3739
}
3740
3741
bool clean_build_required = _is_clean_build_required(p_preset);
3742
String combined_android_libraries = String("|").join(android_libraries);
3743
String combined_android_dependencies = String("|").join(android_dependencies);
3744
String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
3745
3746
List<String> cmdline;
3747
cmdline.push_back("validateJavaVersion");
3748
if (clean_build_required) {
3749
cmdline.push_back("clean");
3750
}
3751
3752
String edition = has_dotnet_project ? "Mono" : "Standard";
3753
String build_type = p_debug ? "Debug" : "Release";
3754
if (export_format == EXPORT_FORMAT_AAB) {
3755
String bundle_build_command = vformat("bundle%s%s", edition, build_type);
3756
cmdline.push_back(bundle_build_command);
3757
} else if (export_format == EXPORT_FORMAT_APK) {
3758
String apk_build_command = vformat("assemble%s%s", edition, build_type);
3759
cmdline.push_back(apk_build_command);
3760
}
3761
3762
String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons");
3763
3764
cmdline.push_back("-p"); // argument to specify the start directory.
3765
cmdline.push_back(build_path); // start directory.
3766
cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies
3767
cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
3768
cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.
3769
cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name.
3770
cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk.
3771
cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk.
3772
cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs.
3773
cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins.
3774
cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins.
3775
cmdline.push_back("-Pplugins_maven_repos=" + combined_android_dependencies_maven_repos); // argument to specify the list of maven repos for android dependencies provided by plugins.
3776
cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned.
3777
cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed.
3778
cmdline.push_back("-Pcompress_native_libraries=" + compress_native_libraries_flag); // argument to specify whether the build should compress native libraries.
3779
3780
// NOTE: The release keystore is not included in the verbose logging
3781
// to avoid accidentally leaking sensitive information when sharing verbose logs for troubleshooting.
3782
// Any non-sensitive additions to the command line arguments must be done above this section.
3783
// Sensitive additions must be done below the logging statement.
3784
print_verbose("Build Android project using gradle command: " + String("\n") + build_command + " " + join_list(cmdline, String(" ")));
3785
3786
if (should_sign) {
3787
if (p_debug) {
3788
String debug_keystore = _get_keystore_path(p_preset, true);
3789
String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
3790
String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
3791
3792
if (debug_keystore.is_empty()) {
3793
debug_keystore = EDITOR_GET("export/android/debug_keystore");
3794
debug_password = EDITOR_GET("export/android/debug_keystore_pass");
3795
debug_user = EDITOR_GET("export/android/debug_keystore_user");
3796
}
3797
if (debug_keystore.is_relative_path()) {
3798
debug_keystore = OS::get_singleton()->get_resource_dir().path_join(debug_keystore).simplify_path();
3799
}
3800
if (!FileAccess::exists(debug_keystore)) {
3801
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));
3802
return ERR_FILE_CANT_OPEN;
3803
}
3804
3805
cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file.
3806
cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias.
3807
cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.
3808
} else {
3809
// Pass the release keystore info as well
3810
String release_keystore = _get_keystore_path(p_preset, false);
3811
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
3812
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
3813
if (release_keystore.is_relative_path()) {
3814
release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path();
3815
}
3816
if (!FileAccess::exists(release_keystore)) {
3817
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));
3818
return ERR_FILE_CANT_OPEN;
3819
}
3820
3821
cmdline.push_back("-Prelease_keystore_file=" + release_keystore); // argument to specify the release keystore file.
3822
cmdline.push_back("-Prelease_keystore_alias=" + release_username); // argument to specify the release keystore alias.
3823
cmdline.push_back("-Prelease_keystore_password=" + release_password); // argument to specify the release keystore password.
3824
}
3825
}
3826
3827
String build_project_output;
3828
int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);
3829
if (result != 0) {
3830
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);
3831
return ERR_CANT_CREATE;
3832
} else {
3833
print_verbose(build_project_output);
3834
}
3835
3836
List<String> copy_args;
3837
String copy_command = "copyAndRenameBinary";
3838
copy_args.push_back(copy_command);
3839
3840
copy_args.push_back("-p"); // argument to specify the start directory.
3841
copy_args.push_back(build_path); // start directory.
3842
3843
copy_args.push_back("-Pexport_edition=" + edition.to_lower());
3844
3845
copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());
3846
3847
String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";
3848
copy_args.push_back("-Pexport_format=" + export_format_arg);
3849
3850
String export_filename = p_path.get_file();
3851
String export_path = p_path.get_base_dir();
3852
if (export_path.is_relative_path()) {
3853
export_path = OS::get_singleton()->get_resource_dir().path_join(export_path);
3854
}
3855
export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path();
3856
3857
copy_args.push_back("-Pexport_path=file:" + export_path);
3858
copy_args.push_back("-Pexport_filename=" + export_filename);
3859
3860
print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
3861
String copy_binary_output;
3862
int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, &copy_binary_output);
3863
if (copy_result != 0) {
3864
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output);
3865
return ERR_CANT_CREATE;
3866
} else {
3867
print_verbose(copy_binary_output);
3868
}
3869
3870
print_verbose("Successfully completed Android gradle build.");
3871
return OK;
3872
}
3873
// This is the start of the Legacy build system
3874
print_verbose("Starting legacy build system...");
3875
if (p_debug) {
3876
src_apk = p_preset->get("custom_template/debug");
3877
} else {
3878
src_apk = p_preset->get("custom_template/release");
3879
}
3880
src_apk = src_apk.strip_edges();
3881
if (src_apk.is_empty()) {
3882
if (p_debug) {
3883
src_apk = find_export_template("android_debug.apk");
3884
} else {
3885
src_apk = find_export_template("android_release.apk");
3886
}
3887
if (src_apk.is_empty()) {
3888
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(p_debug ? TTR("Debug export template not found: \"%s\".") : TTR("Release export template not found: \"%s\"."), src_apk));
3889
return ERR_FILE_NOT_FOUND;
3890
}
3891
}
3892
3893
Ref<FileAccess> io_fa;
3894
zlib_filefunc_def io = zipio_create_io(&io_fa);
3895
3896
if (ep.step(TTR("Creating APK..."), 0)) {
3897
return ERR_SKIP;
3898
}
3899
3900
unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io);
3901
if (!pkg) {
3902
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk));
3903
return ERR_FILE_NOT_FOUND;
3904
}
3905
3906
int ret = unzGoToFirstFile(pkg);
3907
3908
Ref<FileAccess> io2_fa;
3909
zlib_filefunc_def io2 = zipio_create_io(&io2_fa);
3910
3911
String tmp_unaligned_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
3912
3913
#define CLEANUP_AND_RETURN(m_err) \
3914
{ \
3915
DirAccess::remove_file_or_error(tmp_unaligned_path); \
3916
return m_err; \
3917
} \
3918
((void)0)
3919
3920
zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
3921
3922
String cmdline = p_preset->get("command_line/extra_args");
3923
3924
String version_name = p_preset->get_version("version/name");
3925
String package_name = p_preset->get("package/unique_name");
3926
3927
String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
3928
3929
Vector<ABI> invalid_abis(enabled_abis);
3930
3931
//To temporarily store icon xml data.
3932
Vector<uint8_t> themed_icon_xml_data;
3933
int icon_xml_compression_method = -1;
3934
3935
while (ret == UNZ_OK) {
3936
//get filename
3937
unz_file_info info;
3938
char fname[16384];
3939
ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
3940
if (ret != UNZ_OK) {
3941
break;
3942
}
3943
3944
bool skip = false;
3945
3946
String file = String::utf8(fname);
3947
3948
Vector<uint8_t> data;
3949
data.resize(info.uncompressed_size);
3950
3951
//read
3952
unzOpenCurrentFile(pkg);
3953
unzReadCurrentFile(pkg, data.ptrw(), data.size());
3954
unzCloseCurrentFile(pkg);
3955
3956
//write
3957
if (file == "AndroidManifest.xml") {
3958
_fix_manifest(p_preset, data, p_give_internet);
3959
3960
// Allow editor export plugins to update the prebuilt manifest as needed.
3961
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3962
for (int i = 0; i < export_plugins.size(); i++) {
3963
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3964
PackedByteArray export_plugin_data = export_plugins[i]->update_android_prebuilt_manifest(Ref<EditorExportPlatform>(this), data);
3965
if (!export_plugin_data.is_empty()) {
3966
data = export_plugin_data;
3967
}
3968
}
3969
}
3970
}
3971
if (file == "resources.arsc") {
3972
_fix_resources(p_preset, data);
3973
}
3974
3975
if (file == THEMED_ICON_XML_PATH) {
3976
// Store themed_icon.xml data.
3977
themed_icon_xml_data = data;
3978
skip = true;
3979
}
3980
3981
if (file == ICON_XML_PATH) {
3982
if (monochrome.is_valid() && !monochrome->is_empty()) {
3983
// Defer processing of icon.xml until after themed_icon.xml is read.
3984
icon_xml_compression_method = info.compression_method;
3985
skip = true;
3986
}
3987
}
3988
3989
if (file.ends_with(".png") && file.contains("mipmap")) {
3990
for (int i = 0; i < ICON_DENSITIES_COUNT; ++i) {
3991
if (main_image.is_valid() && !main_image->is_empty()) {
3992
if (file == LAUNCHER_ICONS[i].export_path) {
3993
_process_launcher_icons(file, main_image, LAUNCHER_ICONS[i].dimensions, data);
3994
}
3995
}
3996
if (foreground.is_valid() && !foreground->is_empty()) {
3997
if (file == LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path) {
3998
_process_launcher_icons(file, foreground, LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data);
3999
}
4000
}
4001
if (background.is_valid() && !background->is_empty()) {
4002
if (file == LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path) {
4003
_process_launcher_icons(file, background, LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data);
4004
}
4005
}
4006
if (monochrome.is_valid() && !monochrome->is_empty()) {
4007
if (file == LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path) {
4008
_process_launcher_icons(file, monochrome, LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data);
4009
}
4010
}
4011
}
4012
}
4013
4014
if (file.ends_with(".so")) {
4015
bool enabled = false;
4016
for (int i = 0; i < enabled_abis.size(); ++i) {
4017
if (file.begins_with("lib/" + enabled_abis[i].abi + "/")) {
4018
invalid_abis.erase(enabled_abis[i]);
4019
enabled = true;
4020
break;
4021
}
4022
}
4023
if (!enabled) {
4024
skip = true;
4025
}
4026
}
4027
4028
if (file.begins_with("META-INF") && should_sign) {
4029
skip = true;
4030
}
4031
4032
if (!skip) {
4033
print_line("ADDING: " + file);
4034
4035
// Respect decision on compression made by AAPT for the export template
4036
const bool uncompressed = info.compression_method == 0;
4037
4038
zip_fileinfo zipfi = get_zip_fileinfo();
4039
4040
zipOpenNewFileInZip(unaligned_apk,
4041
file.utf8().get_data(),
4042
&zipfi,
4043
nullptr,
4044
0,
4045
nullptr,
4046
0,
4047
nullptr,
4048
uncompressed ? 0 : Z_DEFLATED,
4049
Z_DEFAULT_COMPRESSION);
4050
4051
zipWriteInFileInZip(unaligned_apk, data.ptr(), data.size());
4052
zipCloseFileInZip(unaligned_apk);
4053
}
4054
4055
ret = unzGoToNextFile(pkg);
4056
}
4057
4058
// Process deferred icon.xml and replace it's data with themed_icon.xml.
4059
if (monochrome.is_valid() && !monochrome->is_empty()) {
4060
print_line("ADDING: " + ICON_XML_PATH + " (replacing with themed_icon.xml data)");
4061
4062
const bool uncompressed = icon_xml_compression_method == 0;
4063
zip_fileinfo zipfi = get_zip_fileinfo();
4064
4065
zipOpenNewFileInZip(unaligned_apk,
4066
ICON_XML_PATH.utf8().get_data(),
4067
&zipfi,
4068
nullptr,
4069
0,
4070
nullptr,
4071
0,
4072
nullptr,
4073
uncompressed ? 0 : Z_DEFLATED,
4074
Z_DEFAULT_COMPRESSION);
4075
4076
zipWriteInFileInZip(unaligned_apk, themed_icon_xml_data.ptr(), themed_icon_xml_data.size());
4077
zipCloseFileInZip(unaligned_apk);
4078
}
4079
4080
if (!invalid_abis.is_empty()) {
4081
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Missing libraries in the export template for the selected architectures: %s. Please build a template with all required libraries, or uncheck the missing architectures in the export preset."), join_abis(invalid_abis, ", ", false)));
4082
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
4083
}
4084
4085
if (ep.step(TTR("Adding files..."), 1)) {
4086
CLEANUP_AND_RETURN(ERR_SKIP);
4087
}
4088
err = OK;
4089
4090
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
4091
APKExportData ed;
4092
ed.ep = &ep;
4093
ed.apk = unaligned_apk;
4094
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &ed, save_apk_so);
4095
} else {
4096
if (apk_expansion) {
4097
err = save_apk_expansion_file(p_preset, p_debug, p_path);
4098
if (err != OK) {
4099
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
4100
return err;
4101
}
4102
} else {
4103
APKExportData ed;
4104
ed.ep = &ep;
4105
ed.apk = unaligned_apk;
4106
ed.pd.path = "assets.sparsepck";
4107
ed.pd.use_sparse_pck = true;
4108
err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);
4109
4110
Vector<uint8_t> enc_data;
4111
err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data);
4112
if (err != OK) {
4113
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
4114
return err;
4115
}
4116
4117
store_in_apk(&ed, "assets/assets.sparsepck", enc_data, 0);
4118
}
4119
}
4120
4121
if (err != OK) {
4122
unzClose(pkg);
4123
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files.")));
4124
CLEANUP_AND_RETURN(ERR_SKIP);
4125
}
4126
4127
zip_fileinfo zipfi = get_zip_fileinfo();
4128
zipOpenNewFileInZip(unaligned_apk,
4129
"assets/_cl_",
4130
&zipfi,
4131
nullptr,
4132
0,
4133
nullptr,
4134
0,
4135
nullptr,
4136
0, // No compress (little size gain and potentially slower startup)
4137
Z_DEFAULT_COMPRESSION);
4138
zipWriteInFileInZip(unaligned_apk, command_line_flags.ptr(), command_line_flags.size());
4139
zipCloseFileInZip(unaligned_apk);
4140
zipClose(unaligned_apk, nullptr);
4141
unzClose(pkg);
4142
4143
// Let's zip-align (must be done before signing)
4144
4145
static const int PAGE_SIZE_KB = 16 * 1024;
4146
static const int ZIP_ALIGNMENT = 4;
4147
4148
// If we're not signing the apk, then the next step should be the last.
4149
const int next_step = should_sign ? 103 : 105;
4150
if (ep.step(TTR("Aligning APK..."), next_step)) {
4151
CLEANUP_AND_RETURN(ERR_SKIP);
4152
}
4153
4154
unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io);
4155
if (!tmp_unaligned) {
4156
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK.")));
4157
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
4158
}
4159
4160
ret = unzGoToFirstFile(tmp_unaligned);
4161
4162
io2 = zipio_create_io(&io2_fa);
4163
zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
4164
4165
// Take files from the unaligned APK and write them out to the aligned one
4166
// in raw mode, i.e. not uncompressing and recompressing, aligning them as needed,
4167
// following what is done in https://github.com/android/platform_build/blob/master/tools/zipalign/ZipAlign.cpp
4168
int bias = 0;
4169
while (ret == UNZ_OK) {
4170
unz_file_info info;
4171
memset(&info, 0, sizeof(info));
4172
4173
char fname[16384];
4174
char extra[16384];
4175
ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0);
4176
if (ret != UNZ_OK) {
4177
break;
4178
}
4179
4180
String file = String::utf8(fname);
4181
4182
Vector<uint8_t> data;
4183
data.resize(info.compressed_size);
4184
4185
// read
4186
int method, level;
4187
unzOpenCurrentFile2(tmp_unaligned, &method, &level, 1); // raw read
4188
long file_offset = unzGetCurrentFileZStreamPos64(tmp_unaligned);
4189
unzReadCurrentFile(tmp_unaligned, data.ptrw(), data.size());
4190
unzCloseCurrentFile(tmp_unaligned);
4191
4192
// align
4193
int padding = 0;
4194
if (!info.compression_method) {
4195
// Uncompressed file => Align
4196
long new_offset = file_offset + bias;
4197
const char *ext = strrchr(fname, '.');
4198
if (ext && strcmp(ext, ".so") == 0) {
4199
padding = (PAGE_SIZE_KB - (new_offset % PAGE_SIZE_KB)) % PAGE_SIZE_KB;
4200
} else {
4201
padding = (ZIP_ALIGNMENT - (new_offset % ZIP_ALIGNMENT)) % ZIP_ALIGNMENT;
4202
}
4203
}
4204
4205
memset(extra + info.size_file_extra, 0, padding);
4206
4207
zip_fileinfo fileinfo = get_zip_fileinfo();
4208
zipOpenNewFileInZip2(final_apk,
4209
file.utf8().get_data(),
4210
&fileinfo,
4211
extra,
4212
info.size_file_extra + padding,
4213
nullptr,
4214
0,
4215
nullptr,
4216
method,
4217
level,
4218
1); // raw write
4219
zipWriteInFileInZip(final_apk, data.ptr(), data.size());
4220
zipCloseFileInZipRaw(final_apk, info.uncompressed_size, info.crc);
4221
4222
bias += padding;
4223
4224
ret = unzGoToNextFile(tmp_unaligned);
4225
}
4226
4227
zipClose(final_apk, nullptr);
4228
unzClose(tmp_unaligned);
4229
4230
if (should_sign) {
4231
// Signing must be done last as any additional modifications to the
4232
// file will invalidate the signature.
4233
err = sign_apk(p_preset, p_debug, p_path, ep);
4234
if (err != OK) {
4235
// Message is supplied by the subroutine method.
4236
CLEANUP_AND_RETURN(err);
4237
}
4238
}
4239
4240
CLEANUP_AND_RETURN(OK);
4241
}
4242
4243
void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) const {
4244
r_features->push_back("mobile");
4245
r_features->push_back("android");
4246
}
4247
4248
void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {
4249
}
4250
4251
EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
4252
if (EditorNode::get_singleton()) {
4253
Ref<Image> img = memnew(Image);
4254
const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
4255
4256
ImageLoaderSVG::create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false);
4257
logo = ImageTexture::create_from_image(img);
4258
4259
ImageLoaderSVG::create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false);
4260
run_icon = ImageTexture::create_from_image(img);
4261
4262
devices_changed.set();
4263
#ifndef DISABLE_DEPRECATED
4264
android_plugins_changed.set();
4265
#endif // DISABLE_DEPRECATED
4266
#ifndef ANDROID_ENABLED
4267
_create_editor_debug_keystore_if_needed();
4268
_update_preset_status();
4269
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
4270
#endif
4271
}
4272
}
4273
4274
EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
4275
#ifndef ANDROID_ENABLED
4276
quit_request.set();
4277
if (check_for_changes_thread.is_started()) {
4278
check_for_changes_thread.wait_to_finish();
4279
}
4280
#endif
4281
}
4282
4283