Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/platform/android/export/export_plugin.cpp
11353 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
if (p_debug) {
2857
String dk = _get_keystore_path(p_preset, true);
2858
String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2859
String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2860
2861
if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) {
2862
valid = false;
2863
err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n";
2864
}
2865
2866
// Use OR to make the export UI able to show this error.
2867
if (!dk.is_empty() && !FileAccess::exists(dk)) {
2868
dk = EDITOR_GET("export/android/debug_keystore");
2869
if (!FileAccess::exists(dk)) {
2870
valid = false;
2871
err += TTR("Debug keystore not configured in the Editor Settings nor in the preset.") + "\n";
2872
}
2873
}
2874
} else {
2875
String rk = _get_keystore_path(p_preset, false);
2876
String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
2877
String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
2878
2879
if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) {
2880
valid = false;
2881
err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n";
2882
}
2883
2884
if (!rk.is_empty() && !FileAccess::exists(rk)) {
2885
valid = false;
2886
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
2887
}
2888
}
2889
2890
#ifndef ANDROID_ENABLED
2891
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
2892
if (java_sdk_path.is_empty()) {
2893
err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
2894
valid = false;
2895
} else {
2896
// Validate the given path by checking that `java` is present under the `bin` directory.
2897
Error errn;
2898
// Check for the bin directory.
2899
Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
2900
if (errn != OK) {
2901
err += TTR("Invalid Java SDK path in Editor Settings.") + " ";
2902
err += TTR("Missing 'bin' directory!");
2903
err += "\n";
2904
valid = false;
2905
} else {
2906
// Check for the `java` command.
2907
String java_path = get_java_path();
2908
if (!FileAccess::exists(java_path)) {
2909
err += TTR("Unable to find 'java' command using the Java SDK path.") + " ";
2910
err += TTR("Please check the Java SDK directory specified in Editor Settings.");
2911
err += "\n";
2912
valid = false;
2913
}
2914
}
2915
}
2916
2917
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2918
if (sdk_path.is_empty()) {
2919
err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
2920
valid = false;
2921
} else {
2922
Error errn;
2923
// Check for the platform-tools directory.
2924
Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);
2925
if (errn != OK) {
2926
err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
2927
err += TTR("Missing 'platform-tools' directory!");
2928
err += "\n";
2929
valid = false;
2930
}
2931
2932
// Validate that adb is available.
2933
String adb_path = get_adb_path();
2934
if (!FileAccess::exists(adb_path)) {
2935
err += TTR("Unable to find Android SDK platform-tools' adb command.") + " ";
2936
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2937
err += "\n";
2938
valid = false;
2939
}
2940
2941
// Check for the build-tools directory.
2942
Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);
2943
if (errn != OK) {
2944
err += TTR("Invalid Android SDK path in Editor Settings.") + " ";
2945
err += TTR("Missing 'build-tools' directory!");
2946
err += "\n";
2947
valid = false;
2948
}
2949
2950
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
2951
if (!target_sdk_version.is_valid_int()) {
2952
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
2953
}
2954
// Validate that apksigner is available.
2955
String apksigner_path = get_apksigner_path(target_sdk_version.to_int());
2956
if (!FileAccess::exists(apksigner_path)) {
2957
err += TTR("Unable to find Android SDK build-tools' apksigner command.") + " ";
2958
err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2959
err += "\n";
2960
valid = false;
2961
}
2962
}
2963
#endif
2964
2965
if (!err.is_empty()) {
2966
r_error = err;
2967
}
2968
2969
return valid;
2970
}
2971
2972
bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
2973
String err;
2974
bool valid = true;
2975
2976
List<ExportOption> options;
2977
get_export_options(&options);
2978
for (const EditorExportPlatform::ExportOption &E : options) {
2979
if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {
2980
String warn = get_export_option_warning(p_preset.ptr(), E.option.name);
2981
if (!warn.is_empty()) {
2982
err += warn + "\n";
2983
if (E.required) {
2984
valid = false;
2985
}
2986
}
2987
}
2988
}
2989
2990
if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
2991
valid = false;
2992
}
2993
2994
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2995
if (gradle_build_enabled) {
2996
String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version");
2997
Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ);
2998
if (f.is_valid()) {
2999
String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
3000
String installed_version = f->get_line().strip_edges();
3001
if (current_version != installed_version) {
3002
err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version);
3003
err += "\n";
3004
}
3005
}
3006
} else {
3007
if (_is_transparency_allowed(p_preset)) {
3008
// Warning only, so don't override `valid`.
3009
err += vformat(TTR("\"Use Gradle Build\" is required for transparent background on Android"));
3010
err += "\n";
3011
}
3012
}
3013
3014
String target_sdk_str = p_preset->get("gradle_build/target_sdk");
3015
int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
3016
if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
3017
if (target_sdk_str.is_valid_int()) {
3018
target_sdk_int = target_sdk_str.to_int();
3019
if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) {
3020
// Warning only, so don't override `valid`.
3021
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);
3022
err += "\n";
3023
}
3024
}
3025
}
3026
3027
String current_renderer = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");
3028
if (current_renderer == "forward_plus") {
3029
// Warning only, so don't override `valid`.
3030
err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer);
3031
err += "\n";
3032
}
3033
3034
String package_name = p_preset->get("package/unique_name");
3035
if (package_name.contains("$genname") && !is_project_name_valid(p_preset)) {
3036
// Warning only, so don't override `valid`.
3037
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));
3038
err += "\n";
3039
}
3040
3041
r_error = err;
3042
return valid;
3043
}
3044
3045
List<String> EditorExportPlatformAndroid::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
3046
List<String> list;
3047
list.push_back("apk");
3048
list.push_back("aab");
3049
return list;
3050
}
3051
3052
String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
3053
int version_code = p_preset->get("version/code");
3054
String package_name = p_preset->get("package/unique_name");
3055
String apk_file_name = "main." + itos(version_code) + "." + get_package_name(p_preset, package_name) + ".obb";
3056
String fullpath = p_path.get_base_dir().path_join(apk_file_name);
3057
return fullpath;
3058
}
3059
3060
Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
3061
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
3062
Error err = save_pack(p_preset, p_debug, fullpath);
3063
return err;
3064
}
3065
3066
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) {
3067
String cmdline = p_preset->get("command_line/extra_args");
3068
Vector<String> command_line_strings = cmdline.strip_edges().split(" ");
3069
for (int i = 0; i < command_line_strings.size(); i++) {
3070
if (command_line_strings[i].strip_edges().length() == 0) {
3071
command_line_strings.remove_at(i);
3072
i--;
3073
}
3074
}
3075
3076
command_line_strings.append_array(gen_export_flags(p_flags));
3077
3078
bool apk_expansion = p_preset->get("apk_expansion/enable");
3079
if (apk_expansion) {
3080
String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
3081
String apk_expansion_public_key = p_preset->get("apk_expansion/public_key");
3082
3083
command_line_strings.push_back("--use_apk_expansion");
3084
command_line_strings.push_back("--apk_expansion_md5");
3085
command_line_strings.push_back(FileAccess::get_md5(fullpath));
3086
command_line_strings.push_back("--apk_expansion_key");
3087
command_line_strings.push_back(apk_expansion_public_key.strip_edges());
3088
}
3089
3090
int xr_mode_index = p_preset->get("xr_features/xr_mode");
3091
if (xr_mode_index == XR_MODE_OPENXR) {
3092
command_line_strings.push_back("--xr_mode_openxr");
3093
} else { // XRMode.REGULAR is the default.
3094
command_line_strings.push_back("--xr_mode_regular");
3095
3096
// Also override the 'xr/openxr/enabled' project setting.
3097
// This is useful for multi-platforms projects supporting both XR and non-XR devices. The project would need
3098
// to enable openxr for development, and would create multiple XR and non-XR export presets.
3099
// These command line args ensure that the non-XR export presets will have openxr disabled.
3100
command_line_strings.push_back("--xr-mode");
3101
command_line_strings.push_back("off");
3102
}
3103
3104
bool immersive = p_preset->get("screen/immersive_mode");
3105
if (immersive) {
3106
command_line_strings.push_back("--fullscreen");
3107
}
3108
3109
bool edge_to_edge = p_preset->get("screen/edge_to_edge");
3110
if (edge_to_edge) {
3111
command_line_strings.push_back("--edge_to_edge");
3112
}
3113
3114
String background_color = "#" + p_preset->get("screen/background_color").operator Color().to_html(false);
3115
3116
// For Gradle build, _fix_themes_xml() sets background to transparent if _is_transparency_allowed().
3117
// Overriding to transparent here too as it's used as fallback for system bar appearance.
3118
if (_is_transparency_allowed(p_preset) && p_preset->get("gradle_build/use_gradle_build")) {
3119
background_color = "#00000000";
3120
}
3121
command_line_strings.push_back("--background_color");
3122
command_line_strings.push_back(background_color);
3123
3124
bool debug_opengl = p_preset->get("graphics/opengl_debug");
3125
if (debug_opengl) {
3126
command_line_strings.push_back("--debug_opengl");
3127
}
3128
3129
if (command_line_strings.size()) {
3130
r_command_line_flags.resize(4);
3131
encode_uint32(command_line_strings.size(), &r_command_line_flags.write[0]);
3132
for (int i = 0; i < command_line_strings.size(); i++) {
3133
print_line(itos(i) + " param: " + command_line_strings[i]);
3134
CharString command_line_argument = command_line_strings[i].utf8();
3135
int base = r_command_line_flags.size();
3136
int length = command_line_argument.length();
3137
if (length == 0) {
3138
continue;
3139
}
3140
r_command_line_flags.resize(base + 4 + length);
3141
encode_uint32(length, &r_command_line_flags.write[base]);
3142
memcpy(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length);
3143
}
3144
}
3145
}
3146
3147
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
3148
int export_format = int(p_preset->get("gradle_build/export_format"));
3149
if (export_format == EXPORT_FORMAT_AAB) {
3150
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
3151
return FAILED;
3152
}
3153
3154
String keystore;
3155
String password;
3156
String user;
3157
if (p_debug) {
3158
keystore = _get_keystore_path(p_preset, true);
3159
password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
3160
user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
3161
3162
if (keystore.is_empty()) {
3163
keystore = EDITOR_GET("export/android/debug_keystore");
3164
password = EDITOR_GET("export/android/debug_keystore_pass");
3165
user = EDITOR_GET("export/android/debug_keystore_user");
3166
}
3167
3168
if (ep.step(TTR("Signing debug APK..."), 104)) {
3169
return ERR_SKIP;
3170
}
3171
} else {
3172
keystore = _get_keystore_path(p_preset, false);
3173
password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
3174
user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
3175
3176
if (ep.step(TTR("Signing release APK..."), 104)) {
3177
return ERR_SKIP;
3178
}
3179
}
3180
3181
if (!FileAccess::exists(keystore)) {
3182
if (p_debug) {
3183
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));
3184
} else {
3185
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));
3186
}
3187
return ERR_FILE_CANT_OPEN;
3188
}
3189
3190
String apk_path = export_path;
3191
if (apk_path.is_relative_path()) {
3192
apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);
3193
}
3194
apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();
3195
3196
Error err;
3197
#ifdef ANDROID_ENABLED
3198
err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);
3199
if (err != OK) {
3200
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));
3201
return err;
3202
}
3203
#else
3204
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
3205
if (!target_sdk_version.is_valid_int()) {
3206
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
3207
}
3208
3209
String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
3210
print_verbose("Starting signing of the APK binary using " + apksigner);
3211
if (apksigner == "<FAILED>") {
3212
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."));
3213
return OK;
3214
}
3215
if (!FileAccess::exists(apksigner)) {
3216
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."));
3217
return OK;
3218
}
3219
3220
String output;
3221
List<String> args;
3222
args.push_back("sign");
3223
args.push_back("--verbose");
3224
args.push_back("--ks");
3225
args.push_back(keystore);
3226
args.push_back("--ks-pass");
3227
args.push_back("pass:" + password);
3228
args.push_back("--ks-key-alias");
3229
args.push_back(user);
3230
args.push_back(apk_path);
3231
if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
3232
// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
3233
print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
3234
} else {
3235
List<String> redacted_args = List<String>(args);
3236
redacted_args.find(keystore)->set("<REDACTED>");
3237
redacted_args.find("pass:" + password)->set("pass:<REDACTED>");
3238
redacted_args.find(user)->set("<REDACTED>");
3239
print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
3240
}
3241
int retval;
3242
err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
3243
if (err != OK) {
3244
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
3245
return err;
3246
}
3247
// By design, apksigner does not output credentials in its output unless --verbose is used
3248
print_line(output);
3249
if (retval) {
3250
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval));
3251
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
3252
return ERR_CANT_CREATE;
3253
}
3254
#endif
3255
3256
if (ep.step(TTR("Verifying APK..."), 105)) {
3257
return ERR_SKIP;
3258
}
3259
3260
#ifdef ANDROID_ENABLED
3261
err = OS_Android::get_singleton()->verify_apk(apk_path);
3262
if (err != OK) {
3263
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));
3264
return err;
3265
}
3266
#else
3267
args.clear();
3268
args.push_back("verify");
3269
args.push_back("--verbose");
3270
args.push_back(apk_path);
3271
if (p_debug) {
3272
print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
3273
}
3274
3275
output.clear();
3276
err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
3277
if (err != OK) {
3278
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
3279
return err;
3280
}
3281
print_verbose(output);
3282
if (retval) {
3283
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));
3284
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
3285
return ERR_CANT_CREATE;
3286
}
3287
#endif
3288
3289
print_verbose("Successfully completed signing build.");
3290
3291
#ifdef ANDROID_ENABLED
3292
bool prompt_apk_install = EDITOR_GET("export/android/install_exported_apk");
3293
if (prompt_apk_install) {
3294
OS_Android::get_singleton()->shell_open(apk_path);
3295
}
3296
#endif
3297
3298
return OK;
3299
}
3300
3301
void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) {
3302
Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);
3303
String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);
3304
3305
// Clear the APK assets directory
3306
String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY);
3307
if (da_res->dir_exists(apk_assets_directory)) {
3308
print_verbose("Clearing APK assets directory...");
3309
Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory);
3310
ERR_FAIL_COND(da_assets.is_null());
3311
3312
da_assets->erase_contents_recursive();
3313
da_res->remove(apk_assets_directory);
3314
}
3315
3316
// Clear the AAB assets directory
3317
String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY);
3318
if (da_res->dir_exists(aab_assets_directory)) {
3319
print_verbose("Clearing AAB assets directory...");
3320
Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory);
3321
ERR_FAIL_COND(da_assets.is_null());
3322
3323
da_assets->erase_contents_recursive();
3324
da_res->remove(aab_assets_directory);
3325
}
3326
}
3327
3328
void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) {
3329
print_verbose("Removing previously installed libraries...");
3330
Error error;
3331
String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error);
3332
if (error || libs_json.is_empty()) {
3333
print_verbose("No previously installed libraries found");
3334
return;
3335
}
3336
3337
JSON json;
3338
error = json.parse(libs_json);
3339
ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message());
3340
3341
Vector<String> libs = json.get_data();
3342
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
3343
for (int i = 0; i < libs.size(); i++) {
3344
print_verbose("Removing previously installed library " + libs[i]);
3345
da->remove(libs[i]);
3346
}
3347
da->remove(p_gdextension_libs_path);
3348
}
3349
3350
String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {
3351
String ret;
3352
for (List<String>::ConstIterator itr = p_parts.begin(); itr != p_parts.end(); ++itr) {
3353
if (itr != p_parts.begin()) {
3354
ret += p_separator;
3355
}
3356
ret += *itr;
3357
}
3358
return ret;
3359
}
3360
3361
String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformAndroid::ABI> &p_parts, const String &p_separator, bool p_use_arch) {
3362
String ret;
3363
for (int i = 0; i < p_parts.size(); ++i) {
3364
if (i > 0) {
3365
ret += p_separator;
3366
}
3367
ret += (p_use_arch) ? p_parts[i].arch : p_parts[i].abi;
3368
}
3369
return ret;
3370
}
3371
3372
String EditorExportPlatformAndroid::_get_deprecated_plugins_names(const Ref<EditorExportPreset> &p_preset) const {
3373
Vector<String> names;
3374
3375
#ifndef DISABLE_DEPRECATED
3376
PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);
3377
#endif // DISABLE_DEPRECATED
3378
3379
String plugins_names = String("|").join(names);
3380
return plugins_names;
3381
}
3382
3383
String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const {
3384
Vector<String> names;
3385
3386
#ifndef DISABLE_DEPRECATED
3387
PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);
3388
#endif // DISABLE_DEPRECATED
3389
3390
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3391
for (int i = 0; i < export_plugins.size(); i++) {
3392
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3393
names.push_back(export_plugins[i]->get_name());
3394
}
3395
}
3396
3397
String plugins_names = String("|").join(names);
3398
return plugins_names;
3399
}
3400
3401
String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const {
3402
String absolute_path;
3403
if (!p_android_library_path.is_empty()) {
3404
if (p_android_library_path.is_absolute_path()) {
3405
absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path);
3406
} else {
3407
const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path);
3408
absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path);
3409
}
3410
}
3411
return absolute_path;
3412
}
3413
3414
bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {
3415
bool first_build = last_gradle_build_time == 0;
3416
bool have_plugins_changed = false;
3417
String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);
3418
bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir;
3419
3420
String plugin_names = _get_plugins_names(p_preset);
3421
3422
if (!first_build) {
3423
have_plugins_changed = plugin_names != last_plugin_names;
3424
#ifndef DISABLE_DEPRECATED
3425
if (!have_plugins_changed) {
3426
Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
3427
for (int i = 0; i < enabled_plugins.size(); i++) {
3428
if (enabled_plugins.get(i).last_updated > last_gradle_build_time) {
3429
have_plugins_changed = true;
3430
break;
3431
}
3432
}
3433
}
3434
#endif // DISABLE_DEPRECATED
3435
}
3436
3437
last_gradle_build_time = OS::get_singleton()->get_unix_time();
3438
last_gradle_build_dir = gradle_build_dir;
3439
last_plugin_names = plugin_names;
3440
3441
return have_plugins_changed || has_build_dir_changed || first_build;
3442
}
3443
3444
Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
3445
int export_format = int(p_preset->get("gradle_build/export_format"));
3446
bool should_sign = p_preset->get("package/signed");
3447
return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);
3448
}
3449
3450
Error EditorExportPlatformAndroid::_generate_sparse_pck_metadata(const Ref<EditorExportPreset> &p_preset, PackData &p_pack_data, Vector<uint8_t> &r_data) {
3451
Error err;
3452
Ref<FileAccess> ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export_index", "tmp", false, &err);
3453
if (err != OK) {
3454
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not create temporary file!"));
3455
return err;
3456
}
3457
int64_t pck_start_pos = ftmp->get_position();
3458
uint64_t file_base_ofs = 0;
3459
uint64_t dir_base_ofs = 0;
3460
EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs);
3461
3462
// Write directory.
3463
uint64_t dir_offset = ftmp->get_position();
3464
ftmp->seek(dir_base_ofs);
3465
ftmp->store_64(dir_offset - pck_start_pos);
3466
ftmp->seek(dir_offset);
3467
3468
Vector<uint8_t> key;
3469
if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) {
3470
String script_key = _get_script_encryption_key(p_preset);
3471
key.resize(32);
3472
if (script_key.length() == 64) {
3473
for (int i = 0; i < 32; i++) {
3474
int v = 0;
3475
if (i * 2 < script_key.length()) {
3476
char32_t ct = script_key[i * 2];
3477
if (is_digit(ct)) {
3478
ct = ct - '0';
3479
} else if (ct >= 'a' && ct <= 'f') {
3480
ct = 10 + ct - 'a';
3481
}
3482
v |= ct << 4;
3483
}
3484
3485
if (i * 2 + 1 < script_key.length()) {
3486
char32_t ct = script_key[i * 2 + 1];
3487
if (is_digit(ct)) {
3488
ct = ct - '0';
3489
} else if (ct >= 'a' && ct <= 'f') {
3490
ct = 10 + ct - 'a';
3491
}
3492
v |= ct;
3493
}
3494
key.write[i] = v;
3495
}
3496
}
3497
}
3498
3499
if (!EditorExportPlatform::_encrypt_and_store_directory(ftmp, p_pack_data, key, p_preset->get_seed(), 0)) {
3500
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file."));
3501
return ERR_CANT_CREATE;
3502
}
3503
3504
r_data.resize(ftmp->get_length());
3505
ftmp->seek(0);
3506
ftmp->get_buffer(r_data.ptrw(), r_data.size());
3507
ftmp.unref();
3508
3509
return OK;
3510
}
3511
3512
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) {
3513
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
3514
3515
const String base_dir = p_path.get_base_dir();
3516
if (!DirAccess::exists(base_dir)) {
3517
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));
3518
return ERR_FILE_BAD_PATH;
3519
}
3520
3521
String src_apk;
3522
Error err;
3523
3524
EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
3525
3526
bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
3527
String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";
3528
bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG);
3529
bool apk_expansion = p_preset->get("apk_expansion/enable");
3530
Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
3531
3532
print_verbose("Exporting for Android...");
3533
print_verbose("- debug build: " + bool_to_string(p_debug));
3534
print_verbose("- export path: " + p_path);
3535
print_verbose("- export format: " + itos(export_format));
3536
print_verbose("- sign build: " + bool_to_string(should_sign));
3537
print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build));
3538
print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion));
3539
print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false));
3540
print_verbose("- export filter: " + itos(p_preset->get_export_filter()));
3541
print_verbose("- include filter: " + p_preset->get_include_filter());
3542
print_verbose("- exclude filter: " + p_preset->get_exclude_filter());
3543
3544
Ref<Image> main_image;
3545
Ref<Image> foreground;
3546
Ref<Image> background;
3547
Ref<Image> monochrome;
3548
3549
load_icon_refs(p_preset, main_image, foreground, background, monochrome);
3550
3551
Vector<uint8_t> command_line_flags;
3552
// Write command line flags into the command_line_flags variable.
3553
get_command_line_flags(p_preset, p_path, p_flags, command_line_flags);
3554
3555
if (export_format == EXPORT_FORMAT_AAB) {
3556
if (!p_path.ends_with(".aab")) {
3557
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
3558
return ERR_UNCONFIGURED;
3559
}
3560
if (apk_expansion) {
3561
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));
3562
return ERR_UNCONFIGURED;
3563
}
3564
}
3565
if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {
3566
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));
3567
return ERR_UNCONFIGURED;
3568
}
3569
if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) {
3570
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));
3571
return ERR_UNCONFIGURED;
3572
}
3573
String err_string;
3574
if (!has_valid_username_and_password(p_preset, err_string)) {
3575
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string));
3576
return ERR_UNCONFIGURED;
3577
}
3578
3579
if (use_gradle_build) {
3580
print_verbose("Starting gradle build...");
3581
//test that installed build version is alright
3582
{
3583
print_verbose("Checking build version...");
3584
String gradle_base_directory = gradle_build_directory.get_base_dir();
3585
Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ);
3586
if (f.is_null()) {
3587
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."));
3588
return ERR_UNCONFIGURED;
3589
}
3590
String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);
3591
String installed_version = f->get_line().strip_edges();
3592
print_verbose("- build version: " + installed_version);
3593
if (installed_version != current_version) {
3594
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version));
3595
return ERR_UNCONFIGURED;
3596
}
3597
}
3598
const String assets_directory = get_assets_directory(p_preset, export_format);
3599
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
3600
if (java_sdk_path.is_empty()) {
3601
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
3602
return ERR_UNCONFIGURED;
3603
}
3604
print_verbose("Java sdk path: " + java_sdk_path);
3605
3606
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
3607
if (sdk_path.is_empty()) {
3608
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));
3609
return ERR_UNCONFIGURED;
3610
}
3611
print_verbose("Android sdk path: " + sdk_path);
3612
3613
// TODO: should we use "package/name" or "application/config/name"?
3614
String project_name = get_project_name(p_preset, p_preset->get("package/name"));
3615
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.
3616
if (err != OK) {
3617
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));
3618
}
3619
// Copies the project icon files into the appropriate Gradle project directory.
3620
_copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome);
3621
// Write an AndroidManifest.xml file into the Gradle project directory.
3622
_write_tmp_manifest(p_preset, p_give_internet, p_debug);
3623
// Modify res/values/themes.xml file.
3624
_fix_themes_xml(p_preset);
3625
3626
//stores all the project files inside the Gradle project directory. Also includes all ABIs
3627
_clear_assets_directory(p_preset);
3628
String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);
3629
_remove_copied_libs(gdextension_libs_path);
3630
if (!apk_expansion) {
3631
print_verbose("Exporting project files...");
3632
CustomExportData user_data;
3633
user_data.assets_directory = assets_directory;
3634
user_data.libs_directory = gradle_build_directory.path_join("libs");
3635
user_data.debug = p_debug;
3636
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
3637
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);
3638
} else {
3639
user_data.pd.path = "assets.sparsepck";
3640
user_data.pd.use_sparse_pck = true;
3641
err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);
3642
3643
Vector<uint8_t> enc_data;
3644
err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data);
3645
if (err != OK) {
3646
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
3647
return err;
3648
}
3649
3650
err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data);
3651
if (err != OK) {
3652
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!"));
3653
return err;
3654
}
3655
}
3656
if (err != OK) {
3657
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
3658
return err;
3659
}
3660
if (user_data.libs.size() > 0) {
3661
Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);
3662
fa->store_string(JSON::stringify(user_data.libs, "\t"));
3663
}
3664
} else {
3665
print_verbose("Saving apk expansion file...");
3666
err = save_apk_expansion_file(p_preset, p_debug, p_path);
3667
if (err != OK) {
3668
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
3669
return err;
3670
}
3671
}
3672
print_verbose("Storing command line flags...");
3673
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
3674
3675
print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
3676
OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
3677
3678
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
3679
OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
3680
String build_command;
3681
3682
#ifdef WINDOWS_ENABLED
3683
build_command = "gradlew.bat";
3684
#else
3685
build_command = "gradlew";
3686
#endif
3687
3688
String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory);
3689
build_command = build_path.path_join(build_command);
3690
3691
String package_name = get_package_name(p_preset, p_preset->get("package/unique_name"));
3692
String version_code = itos(p_preset->get("version/code"));
3693
String version_name = p_preset->get_version("version/name");
3694
String min_sdk_version = p_preset->get("gradle_build/min_sdk");
3695
if (!min_sdk_version.is_valid_int()) {
3696
min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION);
3697
}
3698
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
3699
if (!target_sdk_version.is_valid_int()) {
3700
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
3701
}
3702
String enabled_abi_string = join_abis(enabled_abis, "|", false);
3703
String sign_flag = bool_to_string(should_sign);
3704
String zipalign_flag = "true";
3705
String compress_native_libraries_flag = bool_to_string(p_preset->get("gradle_build/compress_native_libraries"));
3706
3707
Vector<String> android_libraries;
3708
Vector<String> android_dependencies;
3709
Vector<String> android_dependencies_maven_repos;
3710
3711
#ifndef DISABLE_DEPRECATED
3712
Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
3713
PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries);
3714
PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies);
3715
PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);
3716
#endif // DISABLE_DEPRECATED
3717
3718
bool has_dotnet_project = false;
3719
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3720
for (int i = 0; i < export_plugins.size(); i++) {
3721
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3722
PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug);
3723
for (int k = 0; k < export_plugin_android_libraries.size(); k++) {
3724
const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]);
3725
if (!resolved_android_library_path.is_empty()) {
3726
android_libraries.push_back(resolved_android_library_path);
3727
}
3728
}
3729
3730
PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug);
3731
android_dependencies.append_array(export_plugin_android_dependencies);
3732
3733
PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);
3734
android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);
3735
}
3736
3737
PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug);
3738
if (features.has("dotnet")) {
3739
has_dotnet_project = true;
3740
}
3741
}
3742
3743
bool clean_build_required = _is_clean_build_required(p_preset);
3744
String combined_android_libraries = String("|").join(android_libraries);
3745
String combined_android_dependencies = String("|").join(android_dependencies);
3746
String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
3747
3748
List<String> cmdline;
3749
cmdline.push_back("validateJavaVersion");
3750
if (clean_build_required) {
3751
cmdline.push_back("clean");
3752
}
3753
3754
String edition = has_dotnet_project ? "Mono" : "Standard";
3755
String build_type = p_debug ? "Debug" : "Release";
3756
if (export_format == EXPORT_FORMAT_AAB) {
3757
String bundle_build_command = vformat("bundle%s%s", edition, build_type);
3758
cmdline.push_back(bundle_build_command);
3759
} else if (export_format == EXPORT_FORMAT_APK) {
3760
String apk_build_command = vformat("assemble%s%s", edition, build_type);
3761
cmdline.push_back(apk_build_command);
3762
}
3763
3764
String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons");
3765
3766
cmdline.push_back("-p"); // argument to specify the start directory.
3767
cmdline.push_back(build_path); // start directory.
3768
cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies
3769
cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
3770
cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.
3771
cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name.
3772
cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk.
3773
cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk.
3774
cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs.
3775
cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins.
3776
cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins.
3777
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.
3778
cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned.
3779
cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed.
3780
cmdline.push_back("-Pcompress_native_libraries=" + compress_native_libraries_flag); // argument to specify whether the build should compress native libraries.
3781
3782
// NOTE: The release keystore is not included in the verbose logging
3783
// to avoid accidentally leaking sensitive information when sharing verbose logs for troubleshooting.
3784
// Any non-sensitive additions to the command line arguments must be done above this section.
3785
// Sensitive additions must be done below the logging statement.
3786
print_verbose("Build Android project using gradle command: " + String("\n") + build_command + " " + join_list(cmdline, String(" ")));
3787
3788
if (should_sign) {
3789
if (p_debug) {
3790
String debug_keystore = _get_keystore_path(p_preset, true);
3791
String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
3792
String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
3793
3794
if (debug_keystore.is_empty()) {
3795
debug_keystore = EDITOR_GET("export/android/debug_keystore");
3796
debug_password = EDITOR_GET("export/android/debug_keystore_pass");
3797
debug_user = EDITOR_GET("export/android/debug_keystore_user");
3798
}
3799
if (debug_keystore.is_relative_path()) {
3800
debug_keystore = OS::get_singleton()->get_resource_dir().path_join(debug_keystore).simplify_path();
3801
}
3802
if (!FileAccess::exists(debug_keystore)) {
3803
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));
3804
return ERR_FILE_CANT_OPEN;
3805
}
3806
3807
cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file.
3808
cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias.
3809
cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.
3810
} else {
3811
// Pass the release keystore info as well
3812
String release_keystore = _get_keystore_path(p_preset, false);
3813
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
3814
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
3815
if (release_keystore.is_relative_path()) {
3816
release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path();
3817
}
3818
if (!FileAccess::exists(release_keystore)) {
3819
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));
3820
return ERR_FILE_CANT_OPEN;
3821
}
3822
3823
cmdline.push_back("-Prelease_keystore_file=" + release_keystore); // argument to specify the release keystore file.
3824
cmdline.push_back("-Prelease_keystore_alias=" + release_username); // argument to specify the release keystore alias.
3825
cmdline.push_back("-Prelease_keystore_password=" + release_password); // argument to specify the release keystore password.
3826
}
3827
}
3828
3829
String build_project_output;
3830
int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);
3831
if (result != 0) {
3832
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);
3833
return ERR_CANT_CREATE;
3834
} else {
3835
print_verbose(build_project_output);
3836
}
3837
3838
List<String> copy_args;
3839
String copy_command = "copyAndRenameBinary";
3840
copy_args.push_back(copy_command);
3841
3842
copy_args.push_back("-p"); // argument to specify the start directory.
3843
copy_args.push_back(build_path); // start directory.
3844
3845
copy_args.push_back("-Pexport_edition=" + edition.to_lower());
3846
3847
copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());
3848
3849
String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";
3850
copy_args.push_back("-Pexport_format=" + export_format_arg);
3851
3852
String export_filename = p_path.get_file();
3853
String export_path = p_path.get_base_dir();
3854
if (export_path.is_relative_path()) {
3855
export_path = OS::get_singleton()->get_resource_dir().path_join(export_path);
3856
}
3857
export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path();
3858
3859
copy_args.push_back("-Pexport_path=file:" + export_path);
3860
copy_args.push_back("-Pexport_filename=" + export_filename);
3861
3862
print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
3863
String copy_binary_output;
3864
int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, &copy_binary_output);
3865
if (copy_result != 0) {
3866
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output);
3867
return ERR_CANT_CREATE;
3868
} else {
3869
print_verbose(copy_binary_output);
3870
}
3871
3872
print_verbose("Successfully completed Android gradle build.");
3873
return OK;
3874
}
3875
// This is the start of the Legacy build system
3876
print_verbose("Starting legacy build system...");
3877
if (p_debug) {
3878
src_apk = p_preset->get("custom_template/debug");
3879
} else {
3880
src_apk = p_preset->get("custom_template/release");
3881
}
3882
src_apk = src_apk.strip_edges();
3883
if (src_apk.is_empty()) {
3884
if (p_debug) {
3885
src_apk = find_export_template("android_debug.apk");
3886
} else {
3887
src_apk = find_export_template("android_release.apk");
3888
}
3889
if (src_apk.is_empty()) {
3890
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));
3891
return ERR_FILE_NOT_FOUND;
3892
}
3893
}
3894
3895
Ref<FileAccess> io_fa;
3896
zlib_filefunc_def io = zipio_create_io(&io_fa);
3897
3898
if (ep.step(TTR("Creating APK..."), 0)) {
3899
return ERR_SKIP;
3900
}
3901
3902
unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io);
3903
if (!pkg) {
3904
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk));
3905
return ERR_FILE_NOT_FOUND;
3906
}
3907
3908
int ret = unzGoToFirstFile(pkg);
3909
3910
Ref<FileAccess> io2_fa;
3911
zlib_filefunc_def io2 = zipio_create_io(&io2_fa);
3912
3913
String tmp_unaligned_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
3914
3915
#define CLEANUP_AND_RETURN(m_err) \
3916
{ \
3917
DirAccess::remove_file_or_error(tmp_unaligned_path); \
3918
return m_err; \
3919
} \
3920
((void)0)
3921
3922
zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
3923
3924
String cmdline = p_preset->get("command_line/extra_args");
3925
3926
String version_name = p_preset->get_version("version/name");
3927
String package_name = p_preset->get("package/unique_name");
3928
3929
String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
3930
3931
Vector<ABI> invalid_abis(enabled_abis);
3932
3933
//To temporarily store icon xml data.
3934
Vector<uint8_t> themed_icon_xml_data;
3935
int icon_xml_compression_method = -1;
3936
3937
while (ret == UNZ_OK) {
3938
//get filename
3939
unz_file_info info;
3940
char fname[16384];
3941
ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
3942
if (ret != UNZ_OK) {
3943
break;
3944
}
3945
3946
bool skip = false;
3947
3948
String file = String::utf8(fname);
3949
3950
Vector<uint8_t> data;
3951
data.resize(info.uncompressed_size);
3952
3953
//read
3954
unzOpenCurrentFile(pkg);
3955
unzReadCurrentFile(pkg, data.ptrw(), data.size());
3956
unzCloseCurrentFile(pkg);
3957
3958
//write
3959
if (file == "AndroidManifest.xml") {
3960
_fix_manifest(p_preset, data, p_give_internet);
3961
3962
// Allow editor export plugins to update the prebuilt manifest as needed.
3963
Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
3964
for (int i = 0; i < export_plugins.size(); i++) {
3965
if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
3966
PackedByteArray export_plugin_data = export_plugins[i]->update_android_prebuilt_manifest(Ref<EditorExportPlatform>(this), data);
3967
if (!export_plugin_data.is_empty()) {
3968
data = export_plugin_data;
3969
}
3970
}
3971
}
3972
}
3973
if (file == "resources.arsc") {
3974
_fix_resources(p_preset, data);
3975
}
3976
3977
if (file == THEMED_ICON_XML_PATH) {
3978
// Store themed_icon.xml data.
3979
themed_icon_xml_data = data;
3980
skip = true;
3981
}
3982
3983
if (file == ICON_XML_PATH) {
3984
if (monochrome.is_valid() && !monochrome->is_empty()) {
3985
// Defer processing of icon.xml until after themed_icon.xml is read.
3986
icon_xml_compression_method = info.compression_method;
3987
skip = true;
3988
}
3989
}
3990
3991
if (file.ends_with(".png") && file.contains("mipmap")) {
3992
for (int i = 0; i < ICON_DENSITIES_COUNT; ++i) {
3993
if (main_image.is_valid() && !main_image->is_empty()) {
3994
if (file == LAUNCHER_ICONS[i].export_path) {
3995
_process_launcher_icons(file, main_image, LAUNCHER_ICONS[i].dimensions, data);
3996
}
3997
}
3998
if (foreground.is_valid() && !foreground->is_empty()) {
3999
if (file == LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path) {
4000
_process_launcher_icons(file, foreground, LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data);
4001
}
4002
}
4003
if (background.is_valid() && !background->is_empty()) {
4004
if (file == LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path) {
4005
_process_launcher_icons(file, background, LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data);
4006
}
4007
}
4008
if (monochrome.is_valid() && !monochrome->is_empty()) {
4009
if (file == LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path) {
4010
_process_launcher_icons(file, monochrome, LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data);
4011
}
4012
}
4013
}
4014
}
4015
4016
if (file.ends_with(".so")) {
4017
bool enabled = false;
4018
for (int i = 0; i < enabled_abis.size(); ++i) {
4019
if (file.begins_with("lib/" + enabled_abis[i].abi + "/")) {
4020
invalid_abis.erase(enabled_abis[i]);
4021
enabled = true;
4022
break;
4023
}
4024
}
4025
if (!enabled) {
4026
skip = true;
4027
}
4028
}
4029
4030
if (file.begins_with("META-INF") && should_sign) {
4031
skip = true;
4032
}
4033
4034
if (!skip) {
4035
print_line("ADDING: " + file);
4036
4037
// Respect decision on compression made by AAPT for the export template
4038
const bool uncompressed = info.compression_method == 0;
4039
4040
zip_fileinfo zipfi = get_zip_fileinfo();
4041
4042
zipOpenNewFileInZip(unaligned_apk,
4043
file.utf8().get_data(),
4044
&zipfi,
4045
nullptr,
4046
0,
4047
nullptr,
4048
0,
4049
nullptr,
4050
uncompressed ? 0 : Z_DEFLATED,
4051
Z_DEFAULT_COMPRESSION);
4052
4053
zipWriteInFileInZip(unaligned_apk, data.ptr(), data.size());
4054
zipCloseFileInZip(unaligned_apk);
4055
}
4056
4057
ret = unzGoToNextFile(pkg);
4058
}
4059
4060
// Process deferred icon.xml and replace it's data with themed_icon.xml.
4061
if (monochrome.is_valid() && !monochrome->is_empty()) {
4062
print_line("ADDING: " + ICON_XML_PATH + " (replacing with themed_icon.xml data)");
4063
4064
const bool uncompressed = icon_xml_compression_method == 0;
4065
zip_fileinfo zipfi = get_zip_fileinfo();
4066
4067
zipOpenNewFileInZip(unaligned_apk,
4068
ICON_XML_PATH.utf8().get_data(),
4069
&zipfi,
4070
nullptr,
4071
0,
4072
nullptr,
4073
0,
4074
nullptr,
4075
uncompressed ? 0 : Z_DEFLATED,
4076
Z_DEFAULT_COMPRESSION);
4077
4078
zipWriteInFileInZip(unaligned_apk, themed_icon_xml_data.ptr(), themed_icon_xml_data.size());
4079
zipCloseFileInZip(unaligned_apk);
4080
}
4081
4082
if (!invalid_abis.is_empty()) {
4083
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)));
4084
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
4085
}
4086
4087
if (ep.step(TTR("Adding files..."), 1)) {
4088
CLEANUP_AND_RETURN(ERR_SKIP);
4089
}
4090
err = OK;
4091
4092
if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {
4093
APKExportData ed;
4094
ed.ep = &ep;
4095
ed.apk = unaligned_apk;
4096
err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &ed, save_apk_so);
4097
} else {
4098
if (apk_expansion) {
4099
err = save_apk_expansion_file(p_preset, p_debug, p_path);
4100
if (err != OK) {
4101
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
4102
return err;
4103
}
4104
} else {
4105
APKExportData ed;
4106
ed.ep = &ep;
4107
ed.apk = unaligned_apk;
4108
ed.pd.path = "assets.sparsepck";
4109
ed.pd.use_sparse_pck = true;
4110
err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);
4111
4112
Vector<uint8_t> enc_data;
4113
err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data);
4114
if (err != OK) {
4115
add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));
4116
return err;
4117
}
4118
4119
store_in_apk(&ed, "assets/assets.sparsepck", enc_data, 0);
4120
}
4121
}
4122
4123
if (err != OK) {
4124
unzClose(pkg);
4125
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files.")));
4126
CLEANUP_AND_RETURN(ERR_SKIP);
4127
}
4128
4129
zip_fileinfo zipfi = get_zip_fileinfo();
4130
zipOpenNewFileInZip(unaligned_apk,
4131
"assets/_cl_",
4132
&zipfi,
4133
nullptr,
4134
0,
4135
nullptr,
4136
0,
4137
nullptr,
4138
0, // No compress (little size gain and potentially slower startup)
4139
Z_DEFAULT_COMPRESSION);
4140
zipWriteInFileInZip(unaligned_apk, command_line_flags.ptr(), command_line_flags.size());
4141
zipCloseFileInZip(unaligned_apk);
4142
zipClose(unaligned_apk, nullptr);
4143
unzClose(pkg);
4144
4145
// Let's zip-align (must be done before signing)
4146
4147
static const int PAGE_SIZE_KB = 16 * 1024;
4148
static const int ZIP_ALIGNMENT = 4;
4149
4150
// If we're not signing the apk, then the next step should be the last.
4151
const int next_step = should_sign ? 103 : 105;
4152
if (ep.step(TTR("Aligning APK..."), next_step)) {
4153
CLEANUP_AND_RETURN(ERR_SKIP);
4154
}
4155
4156
unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io);
4157
if (!tmp_unaligned) {
4158
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK.")));
4159
CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
4160
}
4161
4162
ret = unzGoToFirstFile(tmp_unaligned);
4163
4164
io2 = zipio_create_io(&io2_fa);
4165
zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
4166
4167
// Take files from the unaligned APK and write them out to the aligned one
4168
// in raw mode, i.e. not uncompressing and recompressing, aligning them as needed,
4169
// following what is done in https://github.com/android/platform_build/blob/master/tools/zipalign/ZipAlign.cpp
4170
int bias = 0;
4171
while (ret == UNZ_OK) {
4172
unz_file_info info;
4173
memset(&info, 0, sizeof(info));
4174
4175
char fname[16384];
4176
char extra[16384];
4177
ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0);
4178
if (ret != UNZ_OK) {
4179
break;
4180
}
4181
4182
String file = String::utf8(fname);
4183
4184
Vector<uint8_t> data;
4185
data.resize(info.compressed_size);
4186
4187
// read
4188
int method, level;
4189
unzOpenCurrentFile2(tmp_unaligned, &method, &level, 1); // raw read
4190
long file_offset = unzGetCurrentFileZStreamPos64(tmp_unaligned);
4191
unzReadCurrentFile(tmp_unaligned, data.ptrw(), data.size());
4192
unzCloseCurrentFile(tmp_unaligned);
4193
4194
// align
4195
int padding = 0;
4196
if (!info.compression_method) {
4197
// Uncompressed file => Align
4198
long new_offset = file_offset + bias;
4199
const char *ext = strrchr(fname, '.');
4200
if (ext && strcmp(ext, ".so") == 0) {
4201
padding = (PAGE_SIZE_KB - (new_offset % PAGE_SIZE_KB)) % PAGE_SIZE_KB;
4202
} else {
4203
padding = (ZIP_ALIGNMENT - (new_offset % ZIP_ALIGNMENT)) % ZIP_ALIGNMENT;
4204
}
4205
}
4206
4207
memset(extra + info.size_file_extra, 0, padding);
4208
4209
zip_fileinfo fileinfo = get_zip_fileinfo();
4210
zipOpenNewFileInZip2(final_apk,
4211
file.utf8().get_data(),
4212
&fileinfo,
4213
extra,
4214
info.size_file_extra + padding,
4215
nullptr,
4216
0,
4217
nullptr,
4218
method,
4219
level,
4220
1); // raw write
4221
zipWriteInFileInZip(final_apk, data.ptr(), data.size());
4222
zipCloseFileInZipRaw(final_apk, info.uncompressed_size, info.crc);
4223
4224
bias += padding;
4225
4226
ret = unzGoToNextFile(tmp_unaligned);
4227
}
4228
4229
zipClose(final_apk, nullptr);
4230
unzClose(tmp_unaligned);
4231
4232
if (should_sign) {
4233
// Signing must be done last as any additional modifications to the
4234
// file will invalidate the signature.
4235
err = sign_apk(p_preset, p_debug, p_path, ep);
4236
if (err != OK) {
4237
// Message is supplied by the subroutine method.
4238
CLEANUP_AND_RETURN(err);
4239
}
4240
}
4241
4242
CLEANUP_AND_RETURN(OK);
4243
}
4244
4245
void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) const {
4246
r_features->push_back("mobile");
4247
r_features->push_back("android");
4248
}
4249
4250
void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {
4251
}
4252
4253
void EditorExportPlatformAndroid::initialize() {
4254
if (EditorNode::get_singleton()) {
4255
Ref<Image> img = memnew(Image);
4256
const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
4257
4258
ImageLoaderSVG::create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false);
4259
logo = ImageTexture::create_from_image(img);
4260
4261
ImageLoaderSVG::create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false);
4262
run_icon = ImageTexture::create_from_image(img);
4263
4264
devices_changed.set();
4265
#ifndef DISABLE_DEPRECATED
4266
android_plugins_changed.set();
4267
#endif // DISABLE_DEPRECATED
4268
#ifndef ANDROID_ENABLED
4269
_create_editor_debug_keystore_if_needed();
4270
_update_preset_status();
4271
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
4272
#endif
4273
}
4274
}
4275
4276
EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
4277
#ifndef ANDROID_ENABLED
4278
quit_request.set();
4279
if (check_for_changes_thread.is_started()) {
4280
check_for_changes_thread.wait_to_finish();
4281
}
4282
#endif
4283
}
4284
4285