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