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 "gradle_export_util.h"
34#include "logo_svg.gen.h"
35#include "run_icon_svg.gen.h"
36
37#include "core/config/project_settings.h"
38#include "core/io/dir_access.h"
39#include "core/io/file_access.h"
40#include "core/io/image_loader.h"
41#include "core/io/json.h"
42#include "core/io/marshalls.h"
43#include "core/version.h"
44#include "drivers/png/png_driver_common.h"
45#include "editor/editor_log.h"
46#include "editor/editor_node.h"
47#include "editor/editor_paths.h"
48#include "editor/editor_scale.h"
49#include "editor/editor_settings.h"
50#include "editor/import/resource_importer_texture_settings.h"
51#include "main/splash.gen.h"
52#include "scene/resources/image_texture.h"
53
54#include "modules/modules_enabled.gen.h" // For mono and svg.
55#ifdef MODULE_SVG_ENABLED
56#include "modules/svg/image_loader_svg.h"
57#endif
58
59#include <string.h>
60
61static const char *android_perms[] = {
62 "ACCESS_CHECKIN_PROPERTIES",
63 "ACCESS_COARSE_LOCATION",
64 "ACCESS_FINE_LOCATION",
65 "ACCESS_LOCATION_EXTRA_COMMANDS",
66 "ACCESS_MOCK_LOCATION",
67 "ACCESS_NETWORK_STATE",
68 "ACCESS_SURFACE_FLINGER",
69 "ACCESS_WIFI_STATE",
70 "ACCOUNT_MANAGER",
71 "ADD_VOICEMAIL",
72 "AUTHENTICATE_ACCOUNTS",
73 "BATTERY_STATS",
74 "BIND_ACCESSIBILITY_SERVICE",
75 "BIND_APPWIDGET",
76 "BIND_DEVICE_ADMIN",
77 "BIND_INPUT_METHOD",
78 "BIND_NFC_SERVICE",
79 "BIND_NOTIFICATION_LISTENER_SERVICE",
80 "BIND_PRINT_SERVICE",
81 "BIND_REMOTEVIEWS",
82 "BIND_TEXT_SERVICE",
83 "BIND_VPN_SERVICE",
84 "BIND_WALLPAPER",
85 "BLUETOOTH",
86 "BLUETOOTH_ADMIN",
87 "BLUETOOTH_PRIVILEGED",
88 "BRICK",
89 "BROADCAST_PACKAGE_REMOVED",
90 "BROADCAST_SMS",
91 "BROADCAST_STICKY",
92 "BROADCAST_WAP_PUSH",
93 "CALL_PHONE",
94 "CALL_PRIVILEGED",
95 "CAMERA",
96 "CAPTURE_AUDIO_OUTPUT",
97 "CAPTURE_SECURE_VIDEO_OUTPUT",
98 "CAPTURE_VIDEO_OUTPUT",
99 "CHANGE_COMPONENT_ENABLED_STATE",
100 "CHANGE_CONFIGURATION",
101 "CHANGE_NETWORK_STATE",
102 "CHANGE_WIFI_MULTICAST_STATE",
103 "CHANGE_WIFI_STATE",
104 "CLEAR_APP_CACHE",
105 "CLEAR_APP_USER_DATA",
106 "CONTROL_LOCATION_UPDATES",
107 "DELETE_CACHE_FILES",
108 "DELETE_PACKAGES",
109 "DEVICE_POWER",
110 "DIAGNOSTIC",
111 "DISABLE_KEYGUARD",
112 "DUMP",
113 "EXPAND_STATUS_BAR",
114 "FACTORY_TEST",
115 "FLASHLIGHT",
116 "FORCE_BACK",
117 "GET_ACCOUNTS",
118 "GET_PACKAGE_SIZE",
119 "GET_TASKS",
120 "GET_TOP_ACTIVITY_INFO",
121 "GLOBAL_SEARCH",
122 "HARDWARE_TEST",
123 "INJECT_EVENTS",
124 "INSTALL_LOCATION_PROVIDER",
125 "INSTALL_PACKAGES",
126 "INSTALL_SHORTCUT",
127 "INTERNAL_SYSTEM_WINDOW",
128 "INTERNET",
129 "KILL_BACKGROUND_PROCESSES",
130 "LOCATION_HARDWARE",
131 "MANAGE_ACCOUNTS",
132 "MANAGE_APP_TOKENS",
133 "MANAGE_DOCUMENTS",
134 "MANAGE_EXTERNAL_STORAGE",
135 "MASTER_CLEAR",
136 "MEDIA_CONTENT_CONTROL",
137 "MODIFY_AUDIO_SETTINGS",
138 "MODIFY_PHONE_STATE",
139 "MOUNT_FORMAT_FILESYSTEMS",
140 "MOUNT_UNMOUNT_FILESYSTEMS",
141 "NFC",
142 "PERSISTENT_ACTIVITY",
143 "PROCESS_OUTGOING_CALLS",
144 "READ_CALENDAR",
145 "READ_CALL_LOG",
146 "READ_CONTACTS",
147 "READ_EXTERNAL_STORAGE",
148 "READ_FRAME_BUFFER",
149 "READ_HISTORY_BOOKMARKS",
150 "READ_INPUT_STATE",
151 "READ_LOGS",
152 "READ_PHONE_STATE",
153 "READ_PROFILE",
154 "READ_SMS",
155 "READ_SOCIAL_STREAM",
156 "READ_SYNC_SETTINGS",
157 "READ_SYNC_STATS",
158 "READ_USER_DICTIONARY",
159 "REBOOT",
160 "RECEIVE_BOOT_COMPLETED",
161 "RECEIVE_MMS",
162 "RECEIVE_SMS",
163 "RECEIVE_WAP_PUSH",
164 "RECORD_AUDIO",
165 "REORDER_TASKS",
166 "RESTART_PACKAGES",
167 "SEND_RESPOND_VIA_MESSAGE",
168 "SEND_SMS",
169 "SET_ACTIVITY_WATCHER",
170 "SET_ALARM",
171 "SET_ALWAYS_FINISH",
172 "SET_ANIMATION_SCALE",
173 "SET_DEBUG_APP",
174 "SET_ORIENTATION",
175 "SET_POINTER_SPEED",
176 "SET_PREFERRED_APPLICATIONS",
177 "SET_PROCESS_LIMIT",
178 "SET_TIME",
179 "SET_TIME_ZONE",
180 "SET_WALLPAPER",
181 "SET_WALLPAPER_HINTS",
182 "SIGNAL_PERSISTENT_PROCESSES",
183 "STATUS_BAR",
184 "SUBSCRIBED_FEEDS_READ",
185 "SUBSCRIBED_FEEDS_WRITE",
186 "SYSTEM_ALERT_WINDOW",
187 "TRANSMIT_IR",
188 "UNINSTALL_SHORTCUT",
189 "UPDATE_DEVICE_STATS",
190 "USE_CREDENTIALS",
191 "USE_SIP",
192 "VIBRATE",
193 "WAKE_LOCK",
194 "WRITE_APN_SETTINGS",
195 "WRITE_CALENDAR",
196 "WRITE_CALL_LOG",
197 "WRITE_CONTACTS",
198 "WRITE_EXTERNAL_STORAGE",
199 "WRITE_GSERVICES",
200 "WRITE_HISTORY_BOOKMARKS",
201 "WRITE_PROFILE",
202 "WRITE_SECURE_SETTINGS",
203 "WRITE_SETTINGS",
204 "WRITE_SMS",
205 "WRITE_SOCIAL_STREAM",
206 "WRITE_SYNC_SETTINGS",
207 "WRITE_USER_DICTIONARY",
208 nullptr
209};
210
211static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi/splash.png";
212static const char *LEGACY_BUILD_SPLASH_IMAGE_EXPORT_PATH = "res/drawable-nodpi-v4/splash.png";
213static const char *SPLASH_BG_COLOR_PATH = "res/drawable-nodpi/splash_bg_color.png";
214static const char *LEGACY_BUILD_SPLASH_BG_COLOR_PATH = "res/drawable-nodpi-v4/splash_bg_color.png";
215static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash_drawable.xml";
216static const char *GDEXTENSION_LIBS_PATH = "res://android/build/libs/gdextensionlibs.json";
217
218static const int icon_densities_count = 6;
219static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192");
220static const char *launcher_adaptive_icon_foreground_option = PNAME("launcher_icons/adaptive_foreground_432x432");
221static const char *launcher_adaptive_icon_background_option = PNAME("launcher_icons/adaptive_background_432x432");
222
223static const LauncherIcon launcher_icons[icon_densities_count] = {
224 { "res/mipmap-xxxhdpi-v4/icon.png", 192 },
225 { "res/mipmap-xxhdpi-v4/icon.png", 144 },
226 { "res/mipmap-xhdpi-v4/icon.png", 96 },
227 { "res/mipmap-hdpi-v4/icon.png", 72 },
228 { "res/mipmap-mdpi-v4/icon.png", 48 },
229 { "res/mipmap/icon.png", 192 }
230};
231
232static const LauncherIcon launcher_adaptive_icon_foregrounds[icon_densities_count] = {
233 { "res/mipmap-xxxhdpi-v4/icon_foreground.png", 432 },
234 { "res/mipmap-xxhdpi-v4/icon_foreground.png", 324 },
235 { "res/mipmap-xhdpi-v4/icon_foreground.png", 216 },
236 { "res/mipmap-hdpi-v4/icon_foreground.png", 162 },
237 { "res/mipmap-mdpi-v4/icon_foreground.png", 108 },
238 { "res/mipmap/icon_foreground.png", 432 }
239};
240
241static const LauncherIcon launcher_adaptive_icon_backgrounds[icon_densities_count] = {
242 { "res/mipmap-xxxhdpi-v4/icon_background.png", 432 },
243 { "res/mipmap-xxhdpi-v4/icon_background.png", 324 },
244 { "res/mipmap-xhdpi-v4/icon_background.png", 216 },
245 { "res/mipmap-hdpi-v4/icon_background.png", 162 },
246 { "res/mipmap-mdpi-v4/icon_background.png", 108 },
247 { "res/mipmap/icon_background.png", 432 }
248};
249
250static const int EXPORT_FORMAT_APK = 0;
251static const int EXPORT_FORMAT_AAB = 1;
252
253static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
254static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
255
256static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
257static const int VULKAN_MIN_SDK_VERSION = 24;
258static const int DEFAULT_TARGET_SDK_VERSION = 33; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
259
260#ifndef ANDROID_ENABLED
261void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
262 EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);
263
264 while (!ea->quit_request.is_set()) {
265#ifndef DISABLE_DEPRECATED
266 // Check for android plugins updates
267 {
268 // Nothing to do if we already know the plugins have changed.
269 if (!ea->android_plugins_changed.is_set()) {
270 Vector<PluginConfigAndroid> loaded_plugins = get_plugins();
271
272 MutexLock lock(ea->android_plugins_lock);
273
274 if (ea->android_plugins.size() != loaded_plugins.size()) {
275 ea->android_plugins_changed.set();
276 } else {
277 for (int i = 0; i < ea->android_plugins.size(); i++) {
278 if (ea->android_plugins[i].name != loaded_plugins[i].name) {
279 ea->android_plugins_changed.set();
280 break;
281 }
282 }
283 }
284
285 if (ea->android_plugins_changed.is_set()) {
286 ea->android_plugins = loaded_plugins;
287 }
288 }
289 }
290#endif // DISABLE_DEPRECATED
291
292 // Check for devices updates
293 String adb = get_adb_path();
294 if (FileAccess::exists(adb)) {
295 String devices;
296 List<String> args;
297 args.push_back("devices");
298 int ec;
299 OS::get_singleton()->execute(adb, args, &devices, &ec);
300
301 Vector<String> ds = devices.split("\n");
302 Vector<String> ldevices;
303 for (int i = 1; i < ds.size(); i++) {
304 String d = ds[i];
305 int dpos = d.find("device");
306 if (dpos == -1) {
307 continue;
308 }
309 d = d.substr(0, dpos).strip_edges();
310 ldevices.push_back(d);
311 }
312
313 MutexLock lock(ea->device_lock);
314
315 bool different = false;
316
317 if (ea->devices.size() != ldevices.size()) {
318 different = true;
319 } else {
320 for (int i = 0; i < ea->devices.size(); i++) {
321 if (ea->devices[i].id != ldevices[i]) {
322 different = true;
323 break;
324 }
325 }
326 }
327
328 if (different) {
329 Vector<Device> ndevices;
330
331 for (int i = 0; i < ldevices.size(); i++) {
332 Device d;
333 d.id = ldevices[i];
334 for (int j = 0; j < ea->devices.size(); j++) {
335 if (ea->devices[j].id == ldevices[i]) {
336 d.description = ea->devices[j].description;
337 d.name = ea->devices[j].name;
338 d.api_level = ea->devices[j].api_level;
339 }
340 }
341
342 if (d.description.is_empty()) {
343 //in the oven, request!
344 args.clear();
345 args.push_back("-s");
346 args.push_back(d.id);
347 args.push_back("shell");
348 args.push_back("getprop");
349 int ec2;
350 String dp;
351
352 OS::get_singleton()->execute(adb, args, &dp, &ec2);
353
354 Vector<String> props = dp.split("\n");
355 String vendor;
356 String device;
357 d.description = "Device ID: " + d.id + "\n";
358 d.api_level = 0;
359 for (int j = 0; j < props.size(); j++) {
360 // got information by `shell cat /system/build.prop` before and its format is "property=value"
361 // it's now changed to use `shell getporp` because of permission issue with Android 8.0 and above
362 // its format is "[property]: [value]" so changed it as like build.prop
363 String p = props[j];
364 p = p.replace("]: ", "=");
365 p = p.replace("[", "");
366 p = p.replace("]", "");
367
368 if (p.begins_with("ro.product.model=")) {
369 device = p.get_slice("=", 1).strip_edges();
370 } else if (p.begins_with("ro.product.brand=")) {
371 vendor = p.get_slice("=", 1).strip_edges().capitalize();
372 } else if (p.begins_with("ro.build.display.id=")) {
373 d.description += "Build: " + p.get_slice("=", 1).strip_edges() + "\n";
374 } else if (p.begins_with("ro.build.version.release=")) {
375 d.description += "Release: " + p.get_slice("=", 1).strip_edges() + "\n";
376 } else if (p.begins_with("ro.build.version.sdk=")) {
377 d.api_level = p.get_slice("=", 1).to_int();
378 } else if (p.begins_with("ro.product.cpu.abi=")) {
379 d.description += "CPU: " + p.get_slice("=", 1).strip_edges() + "\n";
380 } else if (p.begins_with("ro.product.manufacturer=")) {
381 d.description += "Manufacturer: " + p.get_slice("=", 1).strip_edges() + "\n";
382 } else if (p.begins_with("ro.board.platform=")) {
383 d.description += "Chipset: " + p.get_slice("=", 1).strip_edges() + "\n";
384 } else if (p.begins_with("ro.opengles.version=")) {
385 uint32_t opengl = p.get_slice("=", 1).to_int();
386 d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl)&0xFF) + "\n";
387 }
388 }
389
390 d.name = vendor + " " + device;
391 if (device.is_empty()) {
392 continue;
393 }
394 }
395
396 ndevices.push_back(d);
397 }
398
399 ea->devices = ndevices;
400 ea->devices_changed.set();
401 }
402 }
403
404 uint64_t sleep = 200;
405 uint64_t wait = 3000000;
406 uint64_t time = OS::get_singleton()->get_ticks_usec();
407 while (OS::get_singleton()->get_ticks_usec() - time < wait) {
408 OS::get_singleton()->delay_usec(1000 * sleep);
409 if (ea->quit_request.is_set()) {
410 break;
411 }
412 }
413 }
414
415 if (EDITOR_GET("export/android/shutdown_adb_on_exit")) {
416 String adb = get_adb_path();
417 if (!FileAccess::exists(adb)) {
418 return; //adb not configured
419 }
420
421 List<String> args;
422 args.push_back("kill-server");
423 OS::get_singleton()->execute(adb, args);
424 }
425}
426#endif
427
428String EditorExportPlatformAndroid::get_project_name(const String &p_name) const {
429 String aname;
430 if (!p_name.is_empty()) {
431 aname = p_name;
432 } else {
433 aname = GLOBAL_GET("application/config/name");
434 }
435
436 if (aname.is_empty()) {
437 aname = VERSION_NAME;
438 }
439
440 return aname;
441}
442
443String EditorExportPlatformAndroid::get_package_name(const String &p_package) const {
444 String pname = p_package;
445 String name = get_valid_basename();
446 pname = pname.replace("$genname", name);
447 return pname;
448}
449
450// Returns the project name without invalid characters
451// or the "noname" string if all characters are invalid.
452String EditorExportPlatformAndroid::get_valid_basename() const {
453 String basename = GLOBAL_GET("application/config/name");
454 basename = basename.to_lower();
455
456 String name;
457 bool first = true;
458 for (int i = 0; i < basename.length(); i++) {
459 char32_t c = basename[i];
460 if (is_digit(c) && first) {
461 continue;
462 }
463 if (is_ascii_alphanumeric_char(c)) {
464 name += String::chr(c);
465 first = false;
466 }
467 }
468
469 if (name.is_empty()) {
470 name = "noname";
471 }
472
473 return name;
474}
475
476String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const {
477 return p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY;
478}
479
480bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, String *r_error) const {
481 String pname = get_package_name(p_package);
482
483 if (pname.length() == 0) {
484 if (r_error) {
485 *r_error = TTR("Package name is missing.");
486 }
487 return false;
488 }
489
490 int segments = 0;
491 bool first = true;
492 for (int i = 0; i < pname.length(); i++) {
493 char32_t c = pname[i];
494 if (first && c == '.') {
495 if (r_error) {
496 *r_error = TTR("Package segments must be of non-zero length.");
497 }
498 return false;
499 }
500 if (c == '.') {
501 segments++;
502 first = true;
503 continue;
504 }
505 if (!is_ascii_identifier_char(c)) {
506 if (r_error) {
507 *r_error = vformat(TTR("The character '%s' is not allowed in Android application package names."), String::chr(c));
508 }
509 return false;
510 }
511 if (first && is_digit(c)) {
512 if (r_error) {
513 *r_error = TTR("A digit cannot be the first character in a package segment.");
514 }
515 return false;
516 }
517 if (first && is_underscore(c)) {
518 if (r_error) {
519 *r_error = vformat(TTR("The character '%s' cannot be the first character in a package segment."), String::chr(c));
520 }
521 return false;
522 }
523 first = false;
524 }
525
526 if (segments == 0) {
527 if (r_error) {
528 *r_error = TTR("The package must have at least one '.' separator.");
529 }
530 return false;
531 }
532
533 if (first) {
534 if (r_error) {
535 *r_error = TTR("Package segments must be of non-zero length.");
536 }
537 return false;
538 }
539
540 if (p_package.find("$genname") >= 0 && !is_project_name_valid()) {
541 if (r_error) {
542 *r_error = TTR("The project name does not meet the requirement for the package name format. Please explicitly specify the package name.");
543 }
544 return false;
545 }
546
547 return true;
548}
549
550bool EditorExportPlatformAndroid::is_project_name_valid() const {
551 // Get the original project name and convert to lowercase.
552 String basename = GLOBAL_GET("application/config/name");
553 basename = basename.to_lower();
554 // Check if there are invalid characters.
555 if (basename != get_valid_basename()) {
556 return false;
557 }
558 return true;
559}
560
561bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) {
562 /*
563 * By not compressing files with little or no benefit in doing so,
564 * a performance gain is expected at runtime. Moreover, if the APK is
565 * zip-aligned, assets stored as they are can be efficiently read by
566 * Android by memory-mapping them.
567 */
568
569 // -- Unconditional uncompress to mimic AAPT plus some other
570
571 static const char *unconditional_compress_ext[] = {
572 // From https://github.com/android/platform_frameworks_base/blob/master/tools/aapt/Package.cpp
573 // These formats are already compressed, or don't compress well:
574 ".jpg", ".jpeg", ".png", ".gif",
575 ".wav", ".mp2", ".mp3", ".ogg", ".aac",
576 ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
577 ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
578 ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
579 ".amr", ".awb", ".wma", ".wmv",
580 // Godot-specific:
581 ".webp", // Same reasoning as .png
582 ".cfb", // Don't let small config files slow-down startup
583 ".scn", // Binary scenes are usually already compressed
584 ".ctex", // Streamable textures are usually already compressed
585 // Trailer for easier processing
586 nullptr
587 };
588
589 for (const char **ext = unconditional_compress_ext; *ext; ++ext) {
590 if (p_path.to_lower().ends_with(String(*ext))) {
591 return false;
592 }
593 }
594
595 // -- Compressed resource?
596
597 if (p_data.size() >= 4 && p_data[0] == 'R' && p_data[1] == 'S' && p_data[2] == 'C' && p_data[3] == 'C') {
598 // Already compressed
599 return false;
600 }
601
602 // --- TODO: Decide on texture resources according to their image compression setting
603
604 return true;
605}
606
607zip_fileinfo EditorExportPlatformAndroid::get_zip_fileinfo() {
608 OS::DateTime dt = OS::get_singleton()->get_datetime();
609
610 zip_fileinfo zipfi;
611 zipfi.tmz_date.tm_year = dt.year;
612 zipfi.tmz_date.tm_mon = dt.month - 1; // tm_mon is zero indexed
613 zipfi.tmz_date.tm_mday = dt.day;
614 zipfi.tmz_date.tm_hour = dt.hour;
615 zipfi.tmz_date.tm_min = dt.minute;
616 zipfi.tmz_date.tm_sec = dt.second;
617 zipfi.dosDate = 0;
618 zipfi.external_fa = 0;
619 zipfi.internal_fa = 0;
620
621 return zipfi;
622}
623
624Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_abis() {
625 // Should have the same order and size as get_archs.
626 Vector<ABI> abis;
627 abis.push_back(ABI("armeabi-v7a", "arm32"));
628 abis.push_back(ABI("arm64-v8a", "arm64"));
629 abis.push_back(ABI("x86", "x86_32"));
630 abis.push_back(ABI("x86_64", "x86_64"));
631 return abis;
632}
633
634#ifndef DISABLE_DEPRECATED
635/// List the gdap files in the directory specified by the p_path parameter.
636Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) {
637 Vector<String> dir_files;
638 Ref<DirAccess> da = DirAccess::open(p_path);
639 if (da.is_valid()) {
640 da->list_dir_begin();
641 while (true) {
642 String file = da->get_next();
643 if (file.is_empty()) {
644 break;
645 }
646
647 if (da->current_is_dir() || da->current_is_hidden()) {
648 continue;
649 }
650
651 if (file.ends_with(PluginConfigAndroid::PLUGIN_CONFIG_EXT)) {
652 dir_files.push_back(file);
653 }
654 }
655 da->list_dir_end();
656 }
657
658 return dir_files;
659}
660
661Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_plugins() {
662 Vector<PluginConfigAndroid> loaded_plugins;
663
664 String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().path_join("android/plugins");
665
666 // Add the prebuilt plugins
667 loaded_plugins.append_array(PluginConfigAndroid::get_prebuilt_plugins(plugins_dir));
668
669 if (DirAccess::exists(plugins_dir)) {
670 Vector<String> plugins_filenames = list_gdap_files(plugins_dir);
671
672 if (!plugins_filenames.is_empty()) {
673 Ref<ConfigFile> config_file = memnew(ConfigFile);
674 for (int i = 0; i < plugins_filenames.size(); i++) {
675 PluginConfigAndroid config = PluginConfigAndroid::load_plugin_config(config_file, plugins_dir.path_join(plugins_filenames[i]));
676 if (config.valid_config) {
677 loaded_plugins.push_back(config);
678 } else {
679 print_error("Invalid plugin config file " + plugins_filenames[i]);
680 }
681 }
682 }
683 }
684
685 return loaded_plugins;
686}
687
688Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_enabled_plugins(const Ref<EditorExportPreset> &p_presets) {
689 Vector<PluginConfigAndroid> enabled_plugins;
690 Vector<PluginConfigAndroid> all_plugins = get_plugins();
691 for (int i = 0; i < all_plugins.size(); i++) {
692 PluginConfigAndroid plugin = all_plugins[i];
693 bool enabled = p_presets->get("plugins/" + plugin.name);
694 if (enabled) {
695 enabled_plugins.push_back(plugin);
696 }
697 }
698
699 return enabled_plugins;
700}
701#endif // DISABLE_DEPRECATED
702
703Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method) {
704 zip_fileinfo zipfi = get_zip_fileinfo();
705 zipOpenNewFileInZip(ed->apk,
706 p_path.utf8().get_data(),
707 &zipfi,
708 nullptr,
709 0,
710 nullptr,
711 0,
712 nullptr,
713 compression_method,
714 Z_DEFAULT_COMPRESSION);
715
716 zipWriteInFileInZip(ed->apk, p_data.ptr(), p_data.size());
717 zipCloseFileInZip(ed->apk);
718
719 return OK;
720}
721
722Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObject &p_so) {
723 if (!p_so.path.get_file().begins_with("lib")) {
724 String err = "Android .so file names must start with \"lib\", but got: " + p_so.path;
725 ERR_PRINT(err);
726 return FAILED;
727 }
728 APKExportData *ed = static_cast<APKExportData *>(p_userdata);
729 Vector<ABI> abis = get_abis();
730 bool exported = false;
731 for (int i = 0; i < p_so.tags.size(); ++i) {
732 // shared objects can be fat (compatible with multiple ABIs)
733 int abi_index = -1;
734 for (int j = 0; j < abis.size(); ++j) {
735 if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {
736 abi_index = j;
737 break;
738 }
739 }
740 if (abi_index != -1) {
741 exported = true;
742 String abi = abis[abi_index].abi;
743 String dst_path = String("lib").path_join(abi).path_join(p_so.path.get_file());
744 Vector<uint8_t> array = FileAccess::get_file_as_bytes(p_so.path);
745 Error store_err = store_in_apk(ed, dst_path, array);
746 ERR_FAIL_COND_V_MSG(store_err, store_err, "Cannot store in apk file '" + dst_path + "'.");
747 }
748 }
749 if (!exported) {
750 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));
751 return FAILED;
752 }
753 return OK;
754}
755
756Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
757 APKExportData *ed = static_cast<APKExportData *>(p_userdata);
758 String dst_path = p_path.replace_first("res://", "assets/");
759
760 store_in_apk(ed, dst_path, p_data, _should_compress_asset(p_path, p_data) ? Z_DEFLATED : 0);
761 return OK;
762}
763
764Error EditorExportPlatformAndroid::ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) {
765 return OK;
766}
767
768Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const SharedObject &p_so) {
769 ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED,
770 "Android .so file names must start with \"lib\", but got: " + p_so.path);
771 Vector<ABI> abis = get_abis();
772 CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);
773 bool exported = false;
774 for (int i = 0; i < p_so.tags.size(); ++i) {
775 int abi_index = -1;
776 for (int j = 0; j < abis.size(); ++j) {
777 if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {
778 abi_index = j;
779 break;
780 }
781 }
782 if (abi_index != -1) {
783 exported = true;
784 String base = "res://android/build/libs";
785 String type = export_data->debug ? "debug" : "release";
786 String abi = abis[abi_index].abi;
787 String filename = p_so.path.get_file();
788 String dst_path = base.path_join(type).path_join(abi).path_join(filename);
789 Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path);
790 print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);
791 Error err = store_file_at_path(dst_path, data);
792 ERR_FAIL_COND_V_MSG(err, err, "Failed to copy .so file from " + p_so.path + " to " + dst_path);
793 export_data->libs.push_back(dst_path);
794 }
795 }
796 ERR_FAIL_COND_V_MSG(!exported, FAILED,
797 "Cannot determine architecture for library \"" + p_so.path + "\". One of the supported architectures must be used as a tag:" + join_abis(abis, " ", true));
798 return OK;
799}
800
801bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) {
802 return p_permissions.find("android.permission.READ_EXTERNAL_STORAGE") != -1 || p_permissions.find("android.permission.WRITE_EXTERNAL_STORAGE") != -1;
803}
804
805bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) {
806 return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1;
807}
808
809bool EditorExportPlatformAndroid::_uses_vulkan() {
810 String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile");
811 bool uses_vulkan = (current_renderer == "forward_plus" || current_renderer == "mobile") && GLOBAL_GET("rendering/rendering_device/driver.android") == "vulkan";
812 return uses_vulkan;
813}
814
815void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
816 const char **aperms = android_perms;
817 while (*aperms) {
818 bool enabled = p_preset->get("permissions/" + String(*aperms).to_lower());
819 if (enabled) {
820 r_permissions.push_back("android.permission." + String(*aperms));
821 }
822 aperms++;
823 }
824 PackedStringArray user_perms = p_preset->get("permissions/custom_permissions");
825 for (int i = 0; i < user_perms.size(); i++) {
826 String user_perm = user_perms[i].strip_edges();
827 if (!user_perm.is_empty()) {
828 r_permissions.push_back(user_perm);
829 }
830 }
831 if (p_give_internet) {
832 if (r_permissions.find("android.permission.INTERNET") == -1) {
833 r_permissions.push_back("android.permission.INTERNET");
834 }
835 }
836}
837
838void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) {
839 print_verbose("Building temporary manifest...");
840 String manifest_text =
841 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
842 "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
843 " xmlns:tools=\"http://schemas.android.com/tools\">\n";
844
845 manifest_text += _get_screen_sizes_tag(p_preset);
846 manifest_text += _get_gles_tag();
847
848 Vector<String> perms;
849 _get_permissions(p_preset, p_give_internet, perms);
850 for (int i = 0; i < perms.size(); i++) {
851 String permission = perms.get(i);
852 if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {
853 manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);
854 } else {
855 manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission);
856 }
857 }
858
859 if (_uses_vulkan()) {
860 manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n";
861 manifest_text += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n";
862 }
863
864 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
865 for (int i = 0; i < export_plugins.size(); i++) {
866 if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
867 const String contents = export_plugins[i]->get_android_manifest_element_contents(Ref<EditorExportPlatform>(this), p_debug);
868 if (!contents.is_empty()) {
869 manifest_text += contents;
870 manifest_text += "\n";
871 }
872 }
873 }
874
875 manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug);
876 manifest_text += "</manifest>\n";
877 String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
878
879 print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text);
880 store_string_at_path(manifest_path, manifest_text);
881}
882
883void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet) {
884 // Leaving the unused types commented because looking these constants up
885 // again later would be annoying
886 // const int CHUNK_AXML_FILE = 0x00080003;
887 // const int CHUNK_RESOURCEIDS = 0x00080180;
888 const int CHUNK_STRINGS = 0x001C0001;
889 // const int CHUNK_XML_END_NAMESPACE = 0x00100101;
890 const int CHUNK_XML_END_TAG = 0x00100103;
891 // const int CHUNK_XML_START_NAMESPACE = 0x00100100;
892 const int CHUNK_XML_START_TAG = 0x00100102;
893 // const int CHUNK_XML_TEXT = 0x00100104;
894 const int UTF8_FLAG = 0x00000100;
895
896 Vector<String> string_table;
897
898 uint32_t ofs = 8;
899
900 uint32_t string_count = 0;
901 uint32_t string_flags = 0;
902 uint32_t string_data_offset = 0;
903
904 uint32_t string_table_begins = 0;
905 uint32_t string_table_ends = 0;
906 Vector<uint8_t> stable_extra;
907
908 String version_name = p_preset->get_version("version/name");
909 int version_code = p_preset->get("version/code");
910 String package_name = p_preset->get("package/unique_name");
911
912 const int screen_orientation =
913 _get_android_orientation_value(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation"))));
914
915 bool screen_support_small = p_preset->get("screen/support_small");
916 bool screen_support_normal = p_preset->get("screen/support_normal");
917 bool screen_support_large = p_preset->get("screen/support_large");
918 bool screen_support_xlarge = p_preset->get("screen/support_xlarge");
919
920 bool backup_allowed = p_preset->get("user_data_backup/allow");
921 int app_category = p_preset->get("package/app_category");
922 bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall");
923 bool exclude_from_recents = p_preset->get("package/exclude_from_recents");
924 bool is_resizeable = bool(GLOBAL_GET("display/window/size/resizable"));
925
926 Vector<String> perms;
927 // Write permissions into the perms variable.
928 _get_permissions(p_preset, p_give_internet, perms);
929 bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);
930
931 while (ofs < (uint32_t)p_manifest.size()) {
932 uint32_t chunk = decode_uint32(&p_manifest[ofs]);
933 uint32_t size = decode_uint32(&p_manifest[ofs + 4]);
934
935 switch (chunk) {
936 case CHUNK_STRINGS: {
937 int iofs = ofs + 8;
938
939 string_count = decode_uint32(&p_manifest[iofs]);
940 string_flags = decode_uint32(&p_manifest[iofs + 8]);
941 string_data_offset = decode_uint32(&p_manifest[iofs + 12]);
942
943 uint32_t st_offset = iofs + 20;
944 string_table.resize(string_count);
945 uint32_t string_end = 0;
946
947 string_table_begins = st_offset;
948
949 for (uint32_t i = 0; i < string_count; i++) {
950 uint32_t string_at = decode_uint32(&p_manifest[st_offset + i * 4]);
951 string_at += st_offset + string_count * 4;
952
953 ERR_FAIL_COND_MSG(string_flags & UTF8_FLAG, "Unimplemented, can't read UTF-8 string table.");
954
955 if (string_flags & UTF8_FLAG) {
956 } else {
957 uint32_t len = decode_uint16(&p_manifest[string_at]);
958 Vector<char32_t> ucstring;
959 ucstring.resize(len + 1);
960 for (uint32_t j = 0; j < len; j++) {
961 uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]);
962 ucstring.write[j] = c;
963 }
964 string_end = MAX(string_at + 2 + 2 * len, string_end);
965 ucstring.write[len] = 0;
966 string_table.write[i] = ucstring.ptr();
967 }
968 }
969
970 for (uint32_t i = string_end; i < (ofs + size); i++) {
971 stable_extra.push_back(p_manifest[i]);
972 }
973
974 string_table_ends = ofs + size;
975
976 } break;
977 case CHUNK_XML_START_TAG: {
978 int iofs = ofs + 8;
979 uint32_t name = decode_uint32(&p_manifest[iofs + 12]);
980
981 String tname = string_table[name];
982 uint32_t attrcount = decode_uint32(&p_manifest[iofs + 20]);
983 iofs += 28;
984
985 for (uint32_t i = 0; i < attrcount; i++) {
986 uint32_t attr_nspace = decode_uint32(&p_manifest[iofs]);
987 uint32_t attr_name = decode_uint32(&p_manifest[iofs + 4]);
988 uint32_t attr_value = decode_uint32(&p_manifest[iofs + 8]);
989 uint32_t attr_resid = decode_uint32(&p_manifest[iofs + 16]);
990
991 const String value = (attr_value != 0xFFFFFFFF) ? string_table[attr_value] : "Res #" + itos(attr_resid);
992 String attrname = string_table[attr_name];
993 const String nspace = (attr_nspace != 0xFFFFFFFF) ? string_table[attr_nspace] : "";
994
995 //replace project information
996 if (tname == "manifest" && attrname == "package") {
997 string_table.write[attr_value] = get_package_name(package_name);
998 }
999
1000 if (tname == "manifest" && attrname == "versionCode") {
1001 encode_uint32(version_code, &p_manifest.write[iofs + 16]);
1002 }
1003
1004 if (tname == "manifest" && attrname == "versionName") {
1005 if (attr_value == 0xFFFFFFFF) {
1006 WARN_PRINT("Version name in a resource, should be plain text");
1007 } else {
1008 string_table.write[attr_value] = version_name;
1009 }
1010 }
1011
1012 if (tname == "application" && attrname == "requestLegacyExternalStorage") {
1013 encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1014 }
1015
1016 if (tname == "application" && attrname == "allowBackup") {
1017 encode_uint32(backup_allowed, &p_manifest.write[iofs + 16]);
1018 }
1019
1020 if (tname == "application" && attrname == "appCategory") {
1021 encode_uint32(_get_app_category_value(app_category), &p_manifest.write[iofs + 16]);
1022 }
1023
1024 if (tname == "application" && attrname == "isGame") {
1025 encode_uint32(app_category == APP_CATEGORY_GAME, &p_manifest.write[iofs + 16]);
1026 }
1027
1028 if (tname == "application" && attrname == "hasFragileUserData") {
1029 encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]);
1030 }
1031
1032 if (tname == "activity" && attrname == "screenOrientation") {
1033 encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]);
1034 }
1035
1036 if (tname == "activity" && attrname == "excludeFromRecents") {
1037 encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]);
1038 }
1039
1040 if (tname == "activity" && attrname == "resizeableActivity") {
1041 encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]);
1042 }
1043
1044 if (tname == "provider" && attrname == "authorities") {
1045 string_table.write[attr_value] = get_package_name(package_name) + String(".fileprovider");
1046 }
1047
1048 if (tname == "supports-screens") {
1049 if (attrname == "smallScreens") {
1050 encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1051
1052 } else if (attrname == "normalScreens") {
1053 encode_uint32(screen_support_normal ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1054
1055 } else if (attrname == "largeScreens") {
1056 encode_uint32(screen_support_large ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1057
1058 } else if (attrname == "xlargeScreens") {
1059 encode_uint32(screen_support_xlarge ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
1060 }
1061 }
1062
1063 iofs += 20;
1064 }
1065
1066 } break;
1067 case CHUNK_XML_END_TAG: {
1068 int iofs = ofs + 8;
1069 uint32_t name = decode_uint32(&p_manifest[iofs + 12]);
1070 String tname = string_table[name];
1071
1072 if (tname == "uses-feature") {
1073 Vector<String> feature_names;
1074 Vector<bool> feature_required_list;
1075 Vector<int> feature_versions;
1076
1077 if (_uses_vulkan()) {
1078 // Require vulkan hardware level 1 support
1079 feature_names.push_back("android.hardware.vulkan.level");
1080 feature_required_list.push_back(false);
1081 feature_versions.push_back(1);
1082
1083 // Require vulkan version 1.0
1084 feature_names.push_back("android.hardware.vulkan.version");
1085 feature_required_list.push_back(true);
1086 feature_versions.push_back(0x400003); // Encoded value for api version 1.0
1087 }
1088
1089 if (feature_names.size() > 0) {
1090 ofs += 24; // skip over end tag
1091
1092 // save manifest ending so we can restore it
1093 Vector<uint8_t> manifest_end;
1094 uint32_t manifest_cur_size = p_manifest.size();
1095
1096 manifest_end.resize(p_manifest.size() - ofs);
1097 memcpy(manifest_end.ptrw(), &p_manifest[ofs], manifest_end.size());
1098
1099 int32_t attr_name_string = string_table.find("name");
1100 ERR_FAIL_COND_MSG(attr_name_string == -1, "Template does not have 'name' attribute.");
1101
1102 int32_t ns_android_string = string_table.find("http://schemas.android.com/apk/res/android");
1103 if (ns_android_string == -1) {
1104 string_table.push_back("http://schemas.android.com/apk/res/android");
1105 ns_android_string = string_table.size() - 1;
1106 }
1107
1108 int32_t attr_uses_feature_string = string_table.find("uses-feature");
1109 if (attr_uses_feature_string == -1) {
1110 string_table.push_back("uses-feature");
1111 attr_uses_feature_string = string_table.size() - 1;
1112 }
1113
1114 int32_t attr_required_string = string_table.find("required");
1115 if (attr_required_string == -1) {
1116 string_table.push_back("required");
1117 attr_required_string = string_table.size() - 1;
1118 }
1119
1120 for (int i = 0; i < feature_names.size(); i++) {
1121 String feature_name = feature_names[i];
1122 bool feature_required = feature_required_list[i];
1123 int feature_version = feature_versions[i];
1124 bool has_version_attribute = feature_version != -1;
1125
1126 print_line("Adding feature " + feature_name);
1127
1128 int32_t feature_string = string_table.find(feature_name);
1129 if (feature_string == -1) {
1130 string_table.push_back(feature_name);
1131 feature_string = string_table.size() - 1;
1132 }
1133
1134 String required_value_string = feature_required ? "true" : "false";
1135 int32_t required_value = string_table.find(required_value_string);
1136 if (required_value == -1) {
1137 string_table.push_back(required_value_string);
1138 required_value = string_table.size() - 1;
1139 }
1140
1141 int32_t attr_version_string = -1;
1142 int32_t version_value = -1;
1143 int tag_size;
1144 int attr_count;
1145 if (has_version_attribute) {
1146 attr_version_string = string_table.find("version");
1147 if (attr_version_string == -1) {
1148 string_table.push_back("version");
1149 attr_version_string = string_table.size() - 1;
1150 }
1151
1152 version_value = string_table.find(itos(feature_version));
1153 if (version_value == -1) {
1154 string_table.push_back(itos(feature_version));
1155 version_value = string_table.size() - 1;
1156 }
1157
1158 tag_size = 96; // node and three attrs + end node
1159 attr_count = 3;
1160 } else {
1161 tag_size = 76; // node and two attrs + end node
1162 attr_count = 2;
1163 }
1164 manifest_cur_size += tag_size + 24;
1165 p_manifest.resize(manifest_cur_size);
1166
1167 // start tag
1168 encode_uint16(0x102, &p_manifest.write[ofs]); // type
1169 encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1170 encode_uint32(tag_size, &p_manifest.write[ofs + 4]); // size
1171 encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1172 encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1173 encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1174 encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name
1175 encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start
1176 encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size
1177 encode_uint16(attr_count, &p_manifest.write[ofs + 28]); // num_attrs
1178 encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index
1179 encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index
1180 encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index
1181
1182 // android:name attribute
1183 encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns
1184 encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'
1185 encode_uint32(feature_string, &p_manifest.write[ofs + 44]); // raw_value
1186 encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size
1187 p_manifest.write[ofs + 50] = 0; // typedvalue_always0
1188 p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)
1189 encode_uint32(feature_string, &p_manifest.write[ofs + 52]); // typedvalue reference
1190
1191 // android:required attribute
1192 encode_uint32(ns_android_string, &p_manifest.write[ofs + 56]); // ns
1193 encode_uint32(attr_required_string, &p_manifest.write[ofs + 60]); // 'name'
1194 encode_uint32(required_value, &p_manifest.write[ofs + 64]); // raw_value
1195 encode_uint16(8, &p_manifest.write[ofs + 68]); // typedvalue_size
1196 p_manifest.write[ofs + 70] = 0; // typedvalue_always0
1197 p_manifest.write[ofs + 71] = 0x03; // typedvalue_type (string)
1198 encode_uint32(required_value, &p_manifest.write[ofs + 72]); // typedvalue reference
1199
1200 ofs += 76;
1201
1202 if (has_version_attribute) {
1203 // android:version attribute
1204 encode_uint32(ns_android_string, &p_manifest.write[ofs]); // ns
1205 encode_uint32(attr_version_string, &p_manifest.write[ofs + 4]); // 'name'
1206 encode_uint32(version_value, &p_manifest.write[ofs + 8]); // raw_value
1207 encode_uint16(8, &p_manifest.write[ofs + 12]); // typedvalue_size
1208 p_manifest.write[ofs + 14] = 0; // typedvalue_always0
1209 p_manifest.write[ofs + 15] = 0x03; // typedvalue_type (string)
1210 encode_uint32(version_value, &p_manifest.write[ofs + 16]); // typedvalue reference
1211
1212 ofs += 20;
1213 }
1214
1215 // end tag
1216 encode_uint16(0x103, &p_manifest.write[ofs]); // type
1217 encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1218 encode_uint32(24, &p_manifest.write[ofs + 4]); // size
1219 encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1220 encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1221 encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1222 encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name
1223
1224 ofs += 24;
1225 }
1226 memcpy(&p_manifest.write[ofs], manifest_end.ptr(), manifest_end.size());
1227 ofs -= 24; // go back over back end
1228 }
1229 }
1230 if (tname == "manifest") {
1231 // save manifest ending so we can restore it
1232 Vector<uint8_t> manifest_end;
1233 uint32_t manifest_cur_size = p_manifest.size();
1234
1235 manifest_end.resize(p_manifest.size() - ofs);
1236 memcpy(manifest_end.ptrw(), &p_manifest[ofs], manifest_end.size());
1237
1238 int32_t attr_name_string = string_table.find("name");
1239 ERR_FAIL_COND_MSG(attr_name_string == -1, "Template does not have 'name' attribute.");
1240
1241 int32_t ns_android_string = string_table.find("android");
1242 ERR_FAIL_COND_MSG(ns_android_string == -1, "Template does not have 'android' namespace.");
1243
1244 int32_t attr_uses_permission_string = string_table.find("uses-permission");
1245 if (attr_uses_permission_string == -1) {
1246 string_table.push_back("uses-permission");
1247 attr_uses_permission_string = string_table.size() - 1;
1248 }
1249
1250 for (int i = 0; i < perms.size(); ++i) {
1251 print_line("Adding permission " + perms[i]);
1252
1253 manifest_cur_size += 56 + 24; // node + end node
1254 p_manifest.resize(manifest_cur_size);
1255
1256 // Add permission to the string pool
1257 int32_t perm_string = string_table.find(perms[i]);
1258 if (perm_string == -1) {
1259 string_table.push_back(perms[i]);
1260 perm_string = string_table.size() - 1;
1261 }
1262
1263 // start tag
1264 encode_uint16(0x102, &p_manifest.write[ofs]); // type
1265 encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1266 encode_uint32(56, &p_manifest.write[ofs + 4]); // size
1267 encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1268 encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1269 encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1270 encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name
1271 encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start
1272 encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size
1273 encode_uint16(1, &p_manifest.write[ofs + 28]); // num_attrs
1274 encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index
1275 encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index
1276 encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index
1277
1278 // attribute
1279 encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns
1280 encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'
1281 encode_uint32(perm_string, &p_manifest.write[ofs + 44]); // raw_value
1282 encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size
1283 p_manifest.write[ofs + 50] = 0; // typedvalue_always0
1284 p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)
1285 encode_uint32(perm_string, &p_manifest.write[ofs + 52]); // typedvalue reference
1286
1287 ofs += 56;
1288
1289 // end tag
1290 encode_uint16(0x103, &p_manifest.write[ofs]); // type
1291 encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize
1292 encode_uint32(24, &p_manifest.write[ofs + 4]); // size
1293 encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno
1294 encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment
1295 encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns
1296 encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name
1297
1298 ofs += 24;
1299 }
1300
1301 // copy footer back in
1302 memcpy(&p_manifest.write[ofs], manifest_end.ptr(), manifest_end.size());
1303 }
1304 } break;
1305 }
1306
1307 ofs += size;
1308 }
1309
1310 //create new andriodmanifest binary
1311
1312 Vector<uint8_t> ret;
1313 ret.resize(string_table_begins + string_table.size() * 4);
1314
1315 for (uint32_t i = 0; i < string_table_begins; i++) {
1316 ret.write[i] = p_manifest[i];
1317 }
1318
1319 ofs = 0;
1320 for (int i = 0; i < string_table.size(); i++) {
1321 encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);
1322 ofs += string_table[i].length() * 2 + 2 + 2;
1323 }
1324
1325 ret.resize(ret.size() + ofs);
1326 string_data_offset = ret.size() - ofs;
1327 uint8_t *chars = &ret.write[string_data_offset];
1328 for (int i = 0; i < string_table.size(); i++) {
1329 String s = string_table[i];
1330 encode_uint16(s.length(), chars);
1331 chars += 2;
1332 for (int j = 0; j < s.length(); j++) {
1333 encode_uint16(s[j], chars);
1334 chars += 2;
1335 }
1336 encode_uint16(0, chars);
1337 chars += 2;
1338 }
1339
1340 for (int i = 0; i < stable_extra.size(); i++) {
1341 ret.push_back(stable_extra[i]);
1342 }
1343
1344 //pad
1345 while (ret.size() % 4) {
1346 ret.push_back(0);
1347 }
1348
1349 uint32_t new_stable_end = ret.size();
1350
1351 uint32_t extra = (p_manifest.size() - string_table_ends);
1352 ret.resize(new_stable_end + extra);
1353 for (uint32_t i = 0; i < extra; i++) {
1354 ret.write[new_stable_end + i] = p_manifest[string_table_ends + i];
1355 }
1356
1357 while (ret.size() % 4) {
1358 ret.push_back(0);
1359 }
1360 encode_uint32(ret.size(), &ret.write[4]); //update new file size
1361
1362 encode_uint32(new_stable_end - 8, &ret.write[12]); //update new string table size
1363 encode_uint32(string_table.size(), &ret.write[16]); //update new number of strings
1364 encode_uint32(string_data_offset - 8, &ret.write[28]); //update new string data offset
1365
1366 p_manifest = ret;
1367}
1368
1369String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) {
1370 uint32_t offset = 0;
1371 uint32_t len = 0;
1372
1373 if (p_utf8) {
1374 uint8_t byte = p_bytes[offset];
1375 if (byte & 0x80) {
1376 offset += 2;
1377 } else {
1378 offset += 1;
1379 }
1380 byte = p_bytes[offset];
1381 offset++;
1382 if (byte & 0x80) {
1383 len = byte & 0x7F;
1384 len = (len << 8) + p_bytes[offset];
1385 offset++;
1386 } else {
1387 len = byte;
1388 }
1389 } else {
1390 len = decode_uint16(&p_bytes[offset]);
1391 offset += 2;
1392 if (len & 0x8000) {
1393 len &= 0x7FFF;
1394 len = (len << 16) + decode_uint16(&p_bytes[offset]);
1395 offset += 2;
1396 }
1397 }
1398
1399 if (p_utf8) {
1400 Vector<uint8_t> str8;
1401 str8.resize(len + 1);
1402 for (uint32_t i = 0; i < len; i++) {
1403 str8.write[i] = p_bytes[offset + i];
1404 }
1405 str8.write[len] = 0;
1406 String str;
1407 str.parse_utf8((const char *)str8.ptr());
1408 return str;
1409 } else {
1410 String str;
1411 for (uint32_t i = 0; i < len; i++) {
1412 char32_t c = decode_uint16(&p_bytes[offset + i * 2]);
1413 if (c == 0) {
1414 break;
1415 }
1416 str += String::chr(c);
1417 }
1418 return str;
1419 }
1420}
1421
1422void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest) {
1423 const int UTF8_FLAG = 0x00000100;
1424
1425 uint32_t string_block_len = decode_uint32(&r_manifest[16]);
1426 uint32_t string_count = decode_uint32(&r_manifest[20]);
1427 uint32_t string_flags = decode_uint32(&r_manifest[28]);
1428 const uint32_t string_table_begins = 40;
1429
1430 Vector<String> string_table;
1431
1432 String package_name = p_preset->get("package/name");
1433 Dictionary appnames = GLOBAL_GET("application/config/name_localized");
1434
1435 for (uint32_t i = 0; i < string_count; i++) {
1436 uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]);
1437 offset += string_table_begins + string_count * 4;
1438
1439 String str = _parse_string(&r_manifest[offset], string_flags & UTF8_FLAG);
1440
1441 if (str.begins_with("godot-project-name")) {
1442 if (str == "godot-project-name") {
1443 //project name
1444 str = get_project_name(package_name);
1445
1446 } else {
1447 String lang = str.substr(str.rfind("-") + 1, str.length()).replace("-", "_");
1448 if (appnames.has(lang)) {
1449 str = appnames[lang];
1450 } else {
1451 str = get_project_name(package_name);
1452 }
1453 }
1454 }
1455
1456 string_table.push_back(str);
1457 }
1458
1459 //write a new string table, but use 16 bits
1460 Vector<uint8_t> ret;
1461 ret.resize(string_table_begins + string_table.size() * 4);
1462
1463 for (uint32_t i = 0; i < string_table_begins; i++) {
1464 ret.write[i] = r_manifest[i];
1465 }
1466
1467 int ofs = 0;
1468 for (int i = 0; i < string_table.size(); i++) {
1469 encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);
1470 ofs += string_table[i].length() * 2 + 2 + 2;
1471 }
1472
1473 ret.resize(ret.size() + ofs);
1474 uint8_t *chars = &ret.write[ret.size() - ofs];
1475 for (int i = 0; i < string_table.size(); i++) {
1476 String s = string_table[i];
1477 encode_uint16(s.length(), chars);
1478 chars += 2;
1479 for (int j = 0; j < s.length(); j++) {
1480 encode_uint16(s[j], chars);
1481 chars += 2;
1482 }
1483 encode_uint16(0, chars);
1484 chars += 2;
1485 }
1486
1487 //pad
1488 while (ret.size() % 4) {
1489 ret.push_back(0);
1490 }
1491
1492 //change flags to not use utf8
1493 encode_uint32(string_flags & ~0x100, &ret.write[28]);
1494 //change length
1495 encode_uint32(ret.size() - 12, &ret.write[16]);
1496 //append the rest...
1497 int rest_from = 12 + string_block_len;
1498 int rest_to = ret.size();
1499 int rest_len = (r_manifest.size() - rest_from);
1500 ret.resize(ret.size() + (r_manifest.size() - rest_from));
1501 for (int i = 0; i < rest_len; i++) {
1502 ret.write[rest_to + i] = r_manifest[rest_from + i];
1503 }
1504 //finally update the size
1505 encode_uint32(ret.size(), &ret.write[4]);
1506
1507 r_manifest = ret;
1508 //printf("end\n");
1509}
1510
1511void EditorExportPlatformAndroid::_load_image_data(const Ref<Image> &p_splash_image, Vector<uint8_t> &p_data) {
1512 Vector<uint8_t> png_buffer;
1513 Error err = PNGDriverCommon::image_to_png(p_splash_image, png_buffer);
1514 if (err == OK) {
1515 p_data.resize(png_buffer.size());
1516 memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size());
1517 } else {
1518 String err_str = String("Failed to convert splash image to png.");
1519 WARN_PRINT(err_str.utf8().get_data());
1520 }
1521}
1522
1523void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) {
1524 Ref<Image> working_image = p_source_image;
1525
1526 if (p_source_image->get_width() != dimension || p_source_image->get_height() != dimension) {
1527 working_image = p_source_image->duplicate();
1528 working_image->resize(dimension, dimension, Image::Interpolation::INTERPOLATE_LANCZOS);
1529 }
1530
1531 Vector<uint8_t> png_buffer;
1532 Error err = PNGDriverCommon::image_to_png(working_image, png_buffer);
1533 if (err == OK) {
1534 p_data.resize(png_buffer.size());
1535 memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size());
1536 } else {
1537 String err_str = String("Failed to convert resized icon (") + p_file_name + ") to png.";
1538 WARN_PRINT(err_str.utf8().get_data());
1539 }
1540}
1541
1542String EditorExportPlatformAndroid::load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) {
1543 bool scale_splash = GLOBAL_GET("application/boot_splash/fullsize");
1544 bool apply_filter = GLOBAL_GET("application/boot_splash/use_filter");
1545 String project_splash_path = GLOBAL_GET("application/boot_splash/image");
1546
1547 if (!project_splash_path.is_empty()) {
1548 splash_image.instantiate();
1549 print_verbose("Loading splash image: " + project_splash_path);
1550 const Error err = ImageLoader::load_image(project_splash_path, splash_image);
1551 if (err) {
1552 if (OS::get_singleton()->is_stdout_verbose()) {
1553 print_error("- unable to load splash image from " + project_splash_path + " (" + itos(err) + ")");
1554 }
1555 splash_image.unref();
1556 }
1557 }
1558
1559 if (splash_image.is_null()) {
1560 // Use the default
1561 print_verbose("Using default splash image.");
1562 splash_image = Ref<Image>(memnew(Image(boot_splash_png)));
1563 }
1564
1565 if (scale_splash) {
1566 Size2 screen_size = Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height"));
1567 int width, height;
1568 if (screen_size.width > screen_size.height) {
1569 // scale horizontally
1570 height = screen_size.height;
1571 width = splash_image->get_width() * screen_size.height / splash_image->get_height();
1572 } else {
1573 // scale vertically
1574 width = screen_size.width;
1575 height = splash_image->get_height() * screen_size.width / splash_image->get_width();
1576 }
1577 splash_image->resize(width, height);
1578 }
1579
1580 // Setup the splash bg color
1581 bool bg_color_valid;
1582 Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid);
1583 if (!bg_color_valid) {
1584 bg_color = boot_splash_bg_color;
1585 }
1586
1587 print_verbose("Creating splash background color image.");
1588 splash_bg_color_image.instantiate();
1589 splash_bg_color_image->initialize_data(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format());
1590 splash_bg_color_image->fill(bg_color);
1591
1592 String processed_splash_config_xml = vformat(SPLASH_CONFIG_XML_CONTENT, bool_to_string(apply_filter));
1593 return processed_splash_config_xml;
1594}
1595
1596void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background) {
1597 String project_icon_path = GLOBAL_GET("application/config/icon");
1598
1599 icon.instantiate();
1600 foreground.instantiate();
1601 background.instantiate();
1602
1603 // Regular icon: user selection -> project icon -> default.
1604 String path = static_cast<String>(p_preset->get(launcher_icon_option)).strip_edges();
1605 print_verbose("Loading regular icon from " + path);
1606 if (path.is_empty() || ImageLoader::load_image(path, icon) != OK) {
1607 print_verbose("- falling back to project icon: " + project_icon_path);
1608 ImageLoader::load_image(project_icon_path, icon);
1609 }
1610
1611 // Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default).
1612 path = static_cast<String>(p_preset->get(launcher_adaptive_icon_foreground_option)).strip_edges();
1613 print_verbose("Loading adaptive foreground icon from " + path);
1614 if (path.is_empty() || ImageLoader::load_image(path, foreground) != OK) {
1615 print_verbose("- falling back to using the regular icon");
1616 foreground = icon;
1617 }
1618
1619 // Adaptive background: user selection -> default.
1620 path = static_cast<String>(p_preset->get(launcher_adaptive_icon_background_option)).strip_edges();
1621 if (!path.is_empty()) {
1622 print_verbose("Loading adaptive background icon from " + path);
1623 ImageLoader::load_image(path, background);
1624 }
1625}
1626
1627void EditorExportPlatformAndroid::store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) {
1628 store_image(launcher_icon.export_path, data);
1629}
1630
1631void EditorExportPlatformAndroid::store_image(const String &export_path, const Vector<uint8_t> &data) {
1632 String img_path = export_path.insert(0, "res://android/build/");
1633 store_file_at_path(img_path, data);
1634}
1635
1636void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,
1637 const String &processed_splash_config_xml,
1638 const Ref<Image> &splash_image,
1639 const Ref<Image> &splash_bg_color_image,
1640 const Ref<Image> &main_image,
1641 const Ref<Image> &foreground,
1642 const Ref<Image> &background) {
1643 // Store the splash configuration
1644 if (!processed_splash_config_xml.is_empty()) {
1645 print_verbose("Storing processed splash configuration: " + String("\n") + processed_splash_config_xml);
1646 store_string_at_path(SPLASH_CONFIG_PATH, processed_splash_config_xml);
1647 }
1648
1649 // Store the splash image
1650 if (splash_image.is_valid() && !splash_image->is_empty()) {
1651 print_verbose("Storing splash image in " + String(SPLASH_IMAGE_EXPORT_PATH));
1652 Vector<uint8_t> data;
1653 _load_image_data(splash_image, data);
1654 store_image(SPLASH_IMAGE_EXPORT_PATH, data);
1655 }
1656
1657 // Store the splash bg color image
1658 if (splash_bg_color_image.is_valid() && !splash_bg_color_image->is_empty()) {
1659 print_verbose("Storing splash background image in " + String(SPLASH_BG_COLOR_PATH));
1660 Vector<uint8_t> data;
1661 _load_image_data(splash_bg_color_image, data);
1662 store_image(SPLASH_BG_COLOR_PATH, data);
1663 }
1664
1665 // Prepare images to be resized for the icons. If some image ends up being uninitialized,
1666 // the default image from the export template will be used.
1667
1668 for (int i = 0; i < icon_densities_count; ++i) {
1669 if (main_image.is_valid() && !main_image->is_empty()) {
1670 print_verbose("Processing launcher icon for dimension " + itos(launcher_icons[i].dimensions) + " into " + launcher_icons[i].export_path);
1671 Vector<uint8_t> data;
1672 _process_launcher_icons(launcher_icons[i].export_path, main_image, launcher_icons[i].dimensions, data);
1673 store_image(launcher_icons[i], data);
1674 }
1675
1676 if (foreground.is_valid() && !foreground->is_empty()) {
1677 print_verbose("Processing launcher adaptive icon foreground for dimension " + itos(launcher_adaptive_icon_foregrounds[i].dimensions) + " into " + launcher_adaptive_icon_foregrounds[i].export_path);
1678 Vector<uint8_t> data;
1679 _process_launcher_icons(launcher_adaptive_icon_foregrounds[i].export_path, foreground,
1680 launcher_adaptive_icon_foregrounds[i].dimensions, data);
1681 store_image(launcher_adaptive_icon_foregrounds[i], data);
1682 }
1683
1684 if (background.is_valid() && !background->is_empty()) {
1685 print_verbose("Processing launcher adaptive icon background for dimension " + itos(launcher_adaptive_icon_backgrounds[i].dimensions) + " into " + launcher_adaptive_icon_backgrounds[i].export_path);
1686 Vector<uint8_t> data;
1687 _process_launcher_icons(launcher_adaptive_icon_backgrounds[i].export_path, background,
1688 launcher_adaptive_icon_backgrounds[i].dimensions, data);
1689 store_image(launcher_adaptive_icon_backgrounds[i], data);
1690 }
1691 }
1692}
1693
1694Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_enabled_abis(const Ref<EditorExportPreset> &p_preset) {
1695 Vector<ABI> abis = get_abis();
1696 Vector<ABI> enabled_abis;
1697 for (int i = 0; i < abis.size(); ++i) {
1698 bool is_enabled = p_preset->get("architectures/" + abis[i].abi);
1699 if (is_enabled) {
1700 enabled_abis.push_back(abis[i]);
1701 }
1702 }
1703 return enabled_abis;
1704}
1705
1706void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
1707 r_features->push_back("etc2");
1708 r_features->push_back("astc");
1709
1710 Vector<ABI> abis = get_enabled_abis(p_preset);
1711 for (int i = 0; i < abis.size(); ++i) {
1712 r_features->push_back(abis[i].arch);
1713 r_features->push_back(abis[i].abi);
1714 }
1715}
1716
1717String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {
1718 if (p_preset) {
1719 if (p_name == ("apk_expansion/public_key")) {
1720 bool apk_expansion = p_preset->get("apk_expansion/enable");
1721 String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
1722 if (apk_expansion && apk_expansion_pkey.is_empty()) {
1723 return TTR("Invalid public key for APK expansion.");
1724 }
1725 } else if (p_name == "package/unique_name") {
1726 String pn = p_preset->get("package/unique_name");
1727 String pn_err;
1728
1729 if (!is_package_name_valid(pn, &pn_err)) {
1730 return TTR("Invalid package name:") + " " + pn_err;
1731 }
1732 } else if (p_name == "gradle_build/use_gradle_build") {
1733 bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
1734 String enabled_plugins_names = _get_plugins_names(Ref<EditorExportPreset>(p_preset));
1735 if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) {
1736 return TTR("\"Use Gradle Build\" must be enabled to use the plugins.");
1737 }
1738 } else if (p_name == "xr_features/xr_mode") {
1739 bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
1740 int xr_mode_index = p_preset->get("xr_features/xr_mode");
1741 if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) {
1742 return TTR("OpenXR requires \"Use Gradle Build\" to be enabled");
1743 }
1744 } else if (p_name == "gradle_build/export_format") {
1745 bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
1746 if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) {
1747 return TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled.");
1748 }
1749 } else if (p_name == "gradle_build/min_sdk") {
1750 String min_sdk_str = p_preset->get("gradle_build/min_sdk");
1751 int min_sdk_int = VULKAN_MIN_SDK_VERSION;
1752 bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
1753 if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
1754 if (!gradle_build_enabled) {
1755 return TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");
1756 }
1757 if (!min_sdk_str.is_valid_int()) {
1758 return vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str);
1759 } else {
1760 min_sdk_int = min_sdk_str.to_int();
1761 if (min_sdk_int < OPENGL_MIN_SDK_VERSION) {
1762 return vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION);
1763 }
1764 }
1765 }
1766 } else if (p_name == "gradle_build/target_sdk") {
1767 String target_sdk_str = p_preset->get("gradle_build/target_sdk");
1768 int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
1769
1770 String min_sdk_str = p_preset->get("gradle_build/min_sdk");
1771 int min_sdk_int = VULKAN_MIN_SDK_VERSION;
1772 if (min_sdk_str.is_valid_int()) {
1773 min_sdk_int = min_sdk_str.to_int();
1774 }
1775 bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
1776 if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
1777 if (!gradle_build_enabled) {
1778 return TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");
1779 }
1780 if (!target_sdk_str.is_valid_int()) {
1781 return vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str);
1782 } else {
1783 target_sdk_int = target_sdk_str.to_int();
1784 if (target_sdk_int < min_sdk_int) {
1785 return TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version.");
1786 }
1787 }
1788 }
1789 }
1790 }
1791 return String();
1792}
1793
1794void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) const {
1795 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
1796 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
1797
1798 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, false, true));
1799 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));
1800 // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).
1801 // This implies doing validation that the string is a proper int.
1802 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "", false, true));
1803 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));
1804
1805#ifndef DISABLE_DEPRECATED
1806 Vector<PluginConfigAndroid> plugins_configs = get_plugins();
1807 for (int i = 0; i < plugins_configs.size(); i++) {
1808 print_verbose("Found Android plugin " + plugins_configs[i].name);
1809 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false));
1810 }
1811 android_plugins_changed.clear();
1812#endif // DISABLE_DEPRECATED
1813
1814 // Android supports multiple architectures in an app bundle, so
1815 // we expose each option as a checkbox in the export dialog.
1816 const Vector<ABI> abis = get_abis();
1817 for (int i = 0; i < abis.size(); ++i) {
1818 const String abi = abis[i].abi;
1819 // All Android devices supporting Vulkan run 64-bit Android,
1820 // so there is usually no point in exporting for 32-bit Android.
1821 const bool is_default = abi == "arm64-v8a";
1822 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default));
1823 }
1824
1825 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1826 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1827 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1828 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1829 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1830 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));
1831
1832 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));
1833 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
1834
1835 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "com.example.$genname", false, true));
1836 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), ""));
1837 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true));
1838 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME));
1839 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/retain_data_on_uninstall"), false));
1840 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/exclude_from_recents"), false));
1841 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_android_tv"), false));
1842 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_app_library"), true));
1843 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_as_launcher_app"), false));
1844
1845 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_icon_option, PROPERTY_HINT_FILE, "*.png"), ""));
1846 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_foreground_option, PROPERTY_HINT_FILE, "*.png"), ""));
1847 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, launcher_adaptive_icon_background_option, PROPERTY_HINT_FILE, "*.png"), ""));
1848
1849 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false));
1850
1851 r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true));
1852
1853 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true));
1854 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true));
1855 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true));
1856 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true));
1857 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));
1858
1859 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));
1860
1861 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), ""));
1862
1863 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true));
1864 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), ""));
1865 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "", false, true));
1866
1867 r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray()));
1868
1869 const char **perms = android_perms;
1870 while (*perms) {
1871 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false));
1872 perms++;
1873 }
1874}
1875
1876String EditorExportPlatformAndroid::get_name() const {
1877 return "Android";
1878}
1879
1880String EditorExportPlatformAndroid::get_os_name() const {
1881 return "Android";
1882}
1883
1884Ref<Texture2D> EditorExportPlatformAndroid::get_logo() const {
1885 return logo;
1886}
1887
1888bool EditorExportPlatformAndroid::should_update_export_options() {
1889#ifndef DISABLE_DEPRECATED
1890 if (android_plugins_changed.is_set()) {
1891 // don't clear unless we're reporting true, to avoid race
1892 android_plugins_changed.clear();
1893 return true;
1894 }
1895#endif // DISABLE_DEPRECATED
1896 return false;
1897}
1898
1899bool EditorExportPlatformAndroid::poll_export() {
1900 bool dc = devices_changed.is_set();
1901 if (dc) {
1902 // don't clear unless we're reporting true, to avoid race
1903 devices_changed.clear();
1904 }
1905 return dc;
1906}
1907
1908int EditorExportPlatformAndroid::get_options_count() const {
1909 MutexLock lock(device_lock);
1910 return devices.size();
1911}
1912
1913String EditorExportPlatformAndroid::get_options_tooltip() const {
1914 return TTR("Select device from the list");
1915}
1916
1917String EditorExportPlatformAndroid::get_option_label(int p_index) const {
1918 ERR_FAIL_INDEX_V(p_index, devices.size(), "");
1919 MutexLock lock(device_lock);
1920 return devices[p_index].name;
1921}
1922
1923String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const {
1924 ERR_FAIL_INDEX_V(p_index, devices.size(), "");
1925 MutexLock lock(device_lock);
1926 String s = devices[p_index].description;
1927 if (devices.size() == 1) {
1928 // Tooltip will be:
1929 // Name
1930 // Description
1931 s = devices[p_index].name + "\n\n" + s;
1932 }
1933 return s;
1934}
1935
1936Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
1937 ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER);
1938
1939 String can_export_error;
1940 bool can_export_missing_templates;
1941 if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {
1942 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);
1943 return ERR_UNCONFIGURED;
1944 }
1945
1946 MutexLock lock(device_lock);
1947
1948 EditorProgress ep("run", vformat(TTR("Running on %s"), devices[p_device].name), 3);
1949
1950 String adb = get_adb_path();
1951
1952 // Export_temp APK.
1953 if (ep.step(TTR("Exporting APK..."), 0)) {
1954 return ERR_SKIP;
1955 }
1956
1957 const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug");
1958 const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT);
1959 const bool use_reverse = devices[p_device].api_level >= 21 && !use_wifi_for_remote_debug;
1960
1961 if (use_reverse) {
1962 p_debug_flags |= DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST;
1963 }
1964
1965 String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
1966
1967#define CLEANUP_AND_RETURN(m_err) \
1968 { \
1969 DirAccess::remove_file_or_error(tmp_export_path); \
1970 return m_err; \
1971 } \
1972 ((void)0)
1973
1974 // Export to temporary APK before sending to device.
1975 Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags);
1976
1977 if (err != OK) {
1978 CLEANUP_AND_RETURN(err);
1979 }
1980
1981 List<String> args;
1982 int rv;
1983 String output;
1984
1985 bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install");
1986 String version_name = p_preset->get_version("version/name");
1987 String package_name = p_preset->get("package/unique_name");
1988
1989 if (remove_prev) {
1990 if (ep.step(TTR("Uninstalling..."), 1)) {
1991 CLEANUP_AND_RETURN(ERR_SKIP);
1992 }
1993
1994 print_line("Uninstalling previous version: " + devices[p_device].name);
1995
1996 args.push_back("-s");
1997 args.push_back(devices[p_device].id);
1998 args.push_back("uninstall");
1999 args.push_back(get_package_name(package_name));
2000
2001 output.clear();
2002 err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2003 print_verbose(output);
2004 }
2005
2006 print_line("Installing to device (please wait...): " + devices[p_device].name);
2007 if (ep.step(TTR("Installing to device, please wait..."), 2)) {
2008 CLEANUP_AND_RETURN(ERR_SKIP);
2009 }
2010
2011 args.clear();
2012 args.push_back("-s");
2013 args.push_back(devices[p_device].id);
2014 args.push_back("install");
2015 args.push_back("-r");
2016 args.push_back(tmp_export_path);
2017
2018 output.clear();
2019 err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2020 print_verbose(output);
2021 if (err || rv != 0) {
2022 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output));
2023 CLEANUP_AND_RETURN(ERR_CANT_CREATE);
2024 }
2025
2026 if (use_remote) {
2027 if (use_reverse) {
2028 static const char *const msg = "--- Device API >= 21; debugging over USB ---";
2029 EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);
2030 print_line(String(msg).to_upper());
2031
2032 args.clear();
2033 args.push_back("-s");
2034 args.push_back(devices[p_device].id);
2035 args.push_back("reverse");
2036 args.push_back("--remove-all");
2037 output.clear();
2038 OS::get_singleton()->execute(adb, args, &output, &rv, true);
2039 print_verbose(output);
2040
2041 if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) {
2042 int dbg_port = EDITOR_GET("network/debug/remote_port");
2043 args.clear();
2044 args.push_back("-s");
2045 args.push_back(devices[p_device].id);
2046 args.push_back("reverse");
2047 args.push_back("tcp:" + itos(dbg_port));
2048 args.push_back("tcp:" + itos(dbg_port));
2049
2050 output.clear();
2051 OS::get_singleton()->execute(adb, args, &output, &rv, true);
2052 print_verbose(output);
2053 print_line("Reverse result: " + itos(rv));
2054 }
2055
2056 if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) {
2057 int fs_port = EDITOR_GET("filesystem/file_server/port");
2058
2059 args.clear();
2060 args.push_back("-s");
2061 args.push_back(devices[p_device].id);
2062 args.push_back("reverse");
2063 args.push_back("tcp:" + itos(fs_port));
2064 args.push_back("tcp:" + itos(fs_port));
2065
2066 output.clear();
2067 err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2068 print_verbose(output);
2069 print_line("Reverse result2: " + itos(rv));
2070 }
2071 } else {
2072 static const char *const api_version_msg = "--- Device API < 21; debugging over Wi-Fi ---";
2073 static const char *const manual_override_msg = "--- Wi-Fi remote debug enabled in project settings; debugging over Wi-Fi ---";
2074
2075 const char *const msg = use_wifi_for_remote_debug ? manual_override_msg : api_version_msg;
2076 EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);
2077 print_line(String(msg).to_upper());
2078 }
2079 }
2080
2081 if (ep.step(TTR("Running on device..."), 3)) {
2082 CLEANUP_AND_RETURN(ERR_SKIP);
2083 }
2084 args.clear();
2085 args.push_back("-s");
2086 args.push_back(devices[p_device].id);
2087 args.push_back("shell");
2088 args.push_back("am");
2089 args.push_back("start");
2090 if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device].api_level >= 17) { // Multi-user introduced in Android 17
2091 args.push_back("--user");
2092 args.push_back("0");
2093 }
2094 args.push_back("-a");
2095 args.push_back("android.intent.action.MAIN");
2096 args.push_back("-n");
2097 args.push_back(get_package_name(package_name) + "/com.godot.game.GodotApp");
2098
2099 output.clear();
2100 err = OS::get_singleton()->execute(adb, args, &output, &rv, true);
2101 print_verbose(output);
2102 if (err || rv != 0) {
2103 add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device."));
2104 CLEANUP_AND_RETURN(ERR_CANT_CREATE);
2105 }
2106
2107 CLEANUP_AND_RETURN(OK);
2108#undef CLEANUP_AND_RETURN
2109}
2110
2111Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {
2112 return run_icon;
2113}
2114
2115String EditorExportPlatformAndroid::get_adb_path() {
2116 String exe_ext = "";
2117 if (OS::get_singleton()->get_name() == "Windows") {
2118 exe_ext = ".exe";
2119 }
2120 String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2121 return sdk_path.path_join("platform-tools/adb" + exe_ext);
2122}
2123
2124String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_check_executes) {
2125 if (p_target_sdk == -1) {
2126 p_target_sdk = DEFAULT_TARGET_SDK_VERSION;
2127 }
2128 String exe_ext = "";
2129 if (OS::get_singleton()->get_name() == "Windows") {
2130 exe_ext = ".bat";
2131 }
2132 String apksigner_command_name = "apksigner" + exe_ext;
2133 String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2134 String apksigner_path = "";
2135
2136 Error errn;
2137 String build_tools_dir = sdk_path.path_join("build-tools");
2138 Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn);
2139 if (errn != OK) {
2140 print_error("Unable to open Android 'build-tools' directory.");
2141 return apksigner_path;
2142 }
2143
2144 // There are additional versions directories we need to go through.
2145 Vector<String> dir_list = da->get_directories();
2146
2147 // We need to use the version of build_tools that matches the Target SDK
2148 // If somehow we can't find that, we see if a version between 28 and the default target SDK exists.
2149 // We need to avoid versions <= 27 because they fail on Java versions >9
2150 // If we can't find that, we just use the first valid version.
2151 Vector<String> ideal_versions;
2152 Vector<String> other_versions;
2153 Vector<String> versions;
2154 bool found_target_sdk = false;
2155 // We only allow for versions <= 27 if specifically set
2156 int min_version = p_target_sdk <= 27 ? p_target_sdk : 28;
2157 for (String sub_dir : dir_list) {
2158 if (!sub_dir.begins_with(".")) {
2159 Vector<String> ver_numbers = sub_dir.split(".");
2160 // Dir not a version number, will use as last resort
2161 if (!ver_numbers.size() || !ver_numbers[0].is_valid_int()) {
2162 other_versions.push_back(sub_dir);
2163 continue;
2164 }
2165 int ver_number = ver_numbers[0].to_int();
2166 if (ver_number == p_target_sdk) {
2167 found_target_sdk = true;
2168 //ensure this is in front of the ones we check
2169 versions.push_back(sub_dir);
2170 } else {
2171 if (ver_number >= min_version && ver_number <= DEFAULT_TARGET_SDK_VERSION) {
2172 ideal_versions.push_back(sub_dir);
2173 } else {
2174 other_versions.push_back(sub_dir);
2175 }
2176 }
2177 }
2178 }
2179 // we will check ideal versions first, then other versions.
2180 versions.append_array(ideal_versions);
2181 versions.append_array(other_versions);
2182
2183 if (!versions.size()) {
2184 print_error("Unable to find the 'apksigner' tool.");
2185 return apksigner_path;
2186 }
2187
2188 int i;
2189 bool failed = false;
2190 String version_to_use;
2191
2192 List<String> args;
2193 args.push_back("--version");
2194 String output;
2195 int retval;
2196 Error err;
2197 for (i = 0; i < versions.size(); i++) {
2198 // Check if the tool is here.
2199 apksigner_path = build_tools_dir.path_join(versions[i]).path_join(apksigner_command_name);
2200 if (FileAccess::exists(apksigner_path)) {
2201 version_to_use = versions[i];
2202 // If we aren't exporting, just break here.
2203 if (!p_check_executes) {
2204 break;
2205 }
2206 // we only check to see if it executes on export because it is slow to load
2207 err = OS::get_singleton()->execute(apksigner_path, args, &output, &retval, false);
2208 if (err || retval) {
2209 failed = true;
2210 } else {
2211 break;
2212 }
2213 }
2214 }
2215 if (i == versions.size()) {
2216 if (failed) {
2217 print_error("All located 'apksigner' tools in " + build_tools_dir + " failed to execute");
2218 return "<FAILED>";
2219 } else {
2220 print_error("Unable to find the 'apksigner' tool.");
2221 return "";
2222 }
2223 }
2224 if (!found_target_sdk) {
2225 print_line("Could not find version of build tools that matches Target SDK, using " + version_to_use);
2226 } else if (failed && found_target_sdk) {
2227 print_line("Version of build tools that matches Target SDK failed to execute, using " + version_to_use);
2228 }
2229
2230 return apksigner_path;
2231}
2232
2233bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
2234 String err;
2235 bool valid = false;
2236 const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
2237
2238#ifdef MODULE_MONO_ENABLED
2239 // Android export is still a work in progress, keep a message as a warning.
2240 err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n";
2241#endif
2242
2243 // Look for export templates (first official, and if defined custom templates).
2244
2245 if (!gradle_build_enabled) {
2246 String template_err;
2247 bool dvalid = false;
2248 bool rvalid = false;
2249 bool has_export_templates = false;
2250
2251 if (p_preset->get("custom_template/debug") != "") {
2252 dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
2253 if (!dvalid) {
2254 template_err += TTR("Custom debug template not found.") + "\n";
2255 }
2256 } else {
2257 has_export_templates |= exists_export_template("android_debug.apk", &template_err);
2258 }
2259
2260 if (p_preset->get("custom_template/release") != "") {
2261 rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
2262 if (!rvalid) {
2263 template_err += TTR("Custom release template not found.") + "\n";
2264 }
2265 } else {
2266 has_export_templates |= exists_export_template("android_release.apk", &template_err);
2267 }
2268
2269 r_missing_templates = !has_export_templates;
2270 valid = dvalid || rvalid || has_export_templates;
2271 if (!valid) {
2272 err += template_err;
2273 }
2274 } else {
2275 bool installed_android_build_template = FileAccess::exists("res://android/build/build.gradle");
2276 if (!installed_android_build_template) {
2277 r_missing_templates = !exists_export_template("android_source.zip", &err);
2278 err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";
2279 } else {
2280 r_missing_templates = false;
2281 }
2282
2283 valid = installed_android_build_template && !r_missing_templates;
2284 }
2285
2286 // Validate the rest of the export configuration.
2287
2288 String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
2289 String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2290 String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2291
2292 if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) {
2293 valid = false;
2294 err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n";
2295 }
2296
2297 // Use OR to make the export UI able to show this error.
2298 if ((p_debug || !dk.is_empty()) && !FileAccess::exists(dk)) {
2299 dk = EDITOR_GET("export/android/debug_keystore");
2300 if (!FileAccess::exists(dk)) {
2301 valid = false;
2302 err += TTR("Debug keystore not configured in the Editor Settings nor in the preset.") + "\n";
2303 }
2304 }
2305
2306 String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
2307 String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
2308 String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
2309
2310 if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) {
2311 valid = false;
2312 err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n";
2313 }
2314
2315 if (!p_debug && !rk.is_empty() && !FileAccess::exists(rk)) {
2316 valid = false;
2317 err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
2318 }
2319
2320 String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2321 if (sdk_path.is_empty()) {
2322 err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
2323 valid = false;
2324 } else {
2325 Error errn;
2326 // Check for the platform-tools directory.
2327 Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);
2328 if (errn != OK) {
2329 err += TTR("Invalid Android SDK path in Editor Settings.");
2330 err += TTR("Missing 'platform-tools' directory!");
2331 err += "\n";
2332 valid = false;
2333 }
2334
2335 // Validate that adb is available.
2336 String adb_path = get_adb_path();
2337 if (!FileAccess::exists(adb_path)) {
2338 err += TTR("Unable to find Android SDK platform-tools' adb command.");
2339 err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2340 err += "\n";
2341 valid = false;
2342 }
2343
2344 // Check for the build-tools directory.
2345 Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);
2346 if (errn != OK) {
2347 err += TTR("Invalid Android SDK path in Editor Settings.");
2348 err += TTR("Missing 'build-tools' directory!");
2349 err += "\n";
2350 valid = false;
2351 }
2352
2353 String target_sdk_version = p_preset->get("gradle_build/target_sdk");
2354 if (!target_sdk_version.is_valid_int()) {
2355 target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
2356 }
2357 // Validate that apksigner is available.
2358 String apksigner_path = get_apksigner_path(target_sdk_version.to_int());
2359 if (!FileAccess::exists(apksigner_path)) {
2360 err += TTR("Unable to find Android SDK build-tools' apksigner command.");
2361 err += TTR("Please check in the Android SDK directory specified in Editor Settings.");
2362 err += "\n";
2363 valid = false;
2364 }
2365 }
2366
2367 if (!err.is_empty()) {
2368 r_error = err;
2369 }
2370
2371 return valid;
2372}
2373
2374bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
2375 String err;
2376 bool valid = true;
2377
2378 List<ExportOption> options;
2379 get_export_options(&options);
2380 for (const EditorExportPlatform::ExportOption &E : options) {
2381 if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {
2382 String warn = get_export_option_warning(p_preset.ptr(), E.option.name);
2383 if (!warn.is_empty()) {
2384 err += warn + "\n";
2385 if (E.required) {
2386 valid = false;
2387 }
2388 }
2389 }
2390 }
2391
2392 if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
2393 valid = false;
2394 }
2395
2396 String min_sdk_str = p_preset->get("gradle_build/min_sdk");
2397 int min_sdk_int = VULKAN_MIN_SDK_VERSION;
2398 if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.
2399 if (min_sdk_str.is_valid_int()) {
2400 min_sdk_int = min_sdk_str.to_int();
2401 }
2402 }
2403
2404 String target_sdk_str = p_preset->get("gradle_build/target_sdk");
2405 int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;
2406 if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.
2407 if (target_sdk_str.is_valid_int()) {
2408 target_sdk_int = target_sdk_str.to_int();
2409 if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) {
2410 // Warning only, so don't override `valid`.
2411 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);
2412 err += "\n";
2413 }
2414 }
2415 }
2416
2417 String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile");
2418 if (current_renderer == "forward_plus") {
2419 // Warning only, so don't override `valid`.
2420 err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer);
2421 err += "\n";
2422 }
2423
2424 if (_uses_vulkan() && min_sdk_int < VULKAN_MIN_SDK_VERSION) {
2425 // Warning only, so don't override `valid`.
2426 err += vformat(TTR("\"Min SDK\" should be greater or equal to %d for the \"%s\" renderer."), VULKAN_MIN_SDK_VERSION, current_renderer);
2427 err += "\n";
2428 }
2429
2430 r_error = err;
2431 return valid;
2432}
2433
2434List<String> EditorExportPlatformAndroid::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
2435 List<String> list;
2436 list.push_back("apk");
2437 list.push_back("aab");
2438 return list;
2439}
2440
2441String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
2442 int version_code = p_preset->get("version/code");
2443 String package_name = p_preset->get("package/unique_name");
2444 String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb";
2445 String fullpath = p_path.get_base_dir().path_join(apk_file_name);
2446 return fullpath;
2447}
2448
2449Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {
2450 String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
2451 Error err = save_pack(p_preset, p_debug, fullpath);
2452 return err;
2453}
2454
2455void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) {
2456 String cmdline = p_preset->get("command_line/extra_args");
2457 Vector<String> command_line_strings = cmdline.strip_edges().split(" ");
2458 for (int i = 0; i < command_line_strings.size(); i++) {
2459 if (command_line_strings[i].strip_edges().length() == 0) {
2460 command_line_strings.remove_at(i);
2461 i--;
2462 }
2463 }
2464
2465 gen_export_flags(command_line_strings, p_flags);
2466
2467 bool apk_expansion = p_preset->get("apk_expansion/enable");
2468 if (apk_expansion) {
2469 String fullpath = get_apk_expansion_fullpath(p_preset, p_path);
2470 String apk_expansion_public_key = p_preset->get("apk_expansion/public_key");
2471
2472 command_line_strings.push_back("--use_apk_expansion");
2473 command_line_strings.push_back("--apk_expansion_md5");
2474 command_line_strings.push_back(FileAccess::get_md5(fullpath));
2475 command_line_strings.push_back("--apk_expansion_key");
2476 command_line_strings.push_back(apk_expansion_public_key.strip_edges());
2477 }
2478
2479 int xr_mode_index = p_preset->get("xr_features/xr_mode");
2480 if (xr_mode_index == XR_MODE_OPENXR) {
2481 command_line_strings.push_back("--xr_mode_openxr");
2482 } else { // XRMode.REGULAR is the default.
2483 command_line_strings.push_back("--xr_mode_regular");
2484 }
2485
2486 bool immersive = p_preset->get("screen/immersive_mode");
2487 if (immersive) {
2488 command_line_strings.push_back("--use_immersive");
2489 }
2490
2491 bool debug_opengl = p_preset->get("graphics/opengl_debug");
2492 if (debug_opengl) {
2493 command_line_strings.push_back("--debug_opengl");
2494 }
2495
2496 if (command_line_strings.size()) {
2497 r_command_line_flags.resize(4);
2498 encode_uint32(command_line_strings.size(), &r_command_line_flags.write[0]);
2499 for (int i = 0; i < command_line_strings.size(); i++) {
2500 print_line(itos(i) + " param: " + command_line_strings[i]);
2501 CharString command_line_argument = command_line_strings[i].utf8();
2502 int base = r_command_line_flags.size();
2503 int length = command_line_argument.length();
2504 if (length == 0) {
2505 continue;
2506 }
2507 r_command_line_flags.resize(base + 4 + length);
2508 encode_uint32(length, &r_command_line_flags.write[base]);
2509 memcpy(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length);
2510 }
2511 }
2512}
2513
2514Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
2515 int export_format = int(p_preset->get("gradle_build/export_format"));
2516 String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
2517 String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
2518 String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
2519 String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
2520 String target_sdk_version = p_preset->get("gradle_build/target_sdk");
2521 if (!target_sdk_version.is_valid_int()) {
2522 target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
2523 }
2524 String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
2525 print_verbose("Starting signing of the " + export_label + " binary using " + apksigner);
2526 if (apksigner == "<FAILED>") {
2527 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(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 %s is unsigned."), export_label));
2528 return OK;
2529 }
2530 if (!FileAccess::exists(apksigner)) {
2531 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label));
2532 return OK;
2533 }
2534
2535 String keystore;
2536 String password;
2537 String user;
2538 if (p_debug) {
2539 keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
2540 password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2541 user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2542
2543 if (keystore.is_empty()) {
2544 keystore = EDITOR_GET("export/android/debug_keystore");
2545 password = EDITOR_GET("export/android/debug_keystore_pass");
2546 user = EDITOR_GET("export/android/debug_keystore_user");
2547 }
2548
2549 if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) {
2550 return ERR_SKIP;
2551 }
2552 } else {
2553 keystore = release_keystore;
2554 password = release_password;
2555 user = release_username;
2556
2557 if (ep.step(vformat(TTR("Signing release %s..."), export_label), 104)) {
2558 return ERR_SKIP;
2559 }
2560 }
2561
2562 if (!FileAccess::exists(keystore)) {
2563 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
2564 return ERR_FILE_CANT_OPEN;
2565 }
2566
2567 String output;
2568 List<String> args;
2569 args.push_back("sign");
2570 args.push_back("--verbose");
2571 args.push_back("--ks");
2572 args.push_back(keystore);
2573 args.push_back("--ks-pass");
2574 args.push_back("pass:" + password);
2575 args.push_back("--ks-key-alias");
2576 args.push_back(user);
2577 args.push_back(export_path);
2578 if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
2579 // We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
2580 print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
2581 } else {
2582 List<String> redacted_args = List<String>(args);
2583 redacted_args.find(keystore)->set("<REDACTED>");
2584 redacted_args.find("pass:" + password)->set("pass:<REDACTED>");
2585 redacted_args.find(user)->set("<REDACTED>");
2586 print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
2587 }
2588 int retval;
2589 Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
2590 if (err != OK) {
2591 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
2592 return err;
2593 }
2594 // By design, apksigner does not output credentials in its output unless --verbose is used
2595 print_line(output);
2596 if (retval) {
2597 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval));
2598 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
2599 return ERR_CANT_CREATE;
2600 }
2601
2602 if (ep.step(vformat(TTR("Verifying %s..."), export_label), 105)) {
2603 return ERR_SKIP;
2604 }
2605
2606 args.clear();
2607 args.push_back("verify");
2608 args.push_back("--verbose");
2609 args.push_back(export_path);
2610 if (p_debug) {
2611 print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
2612 }
2613
2614 output.clear();
2615 err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
2616 if (err != OK) {
2617 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
2618 return err;
2619 }
2620 print_verbose(output);
2621 if (retval) {
2622 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label));
2623 add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
2624 return ERR_CANT_CREATE;
2625 }
2626
2627 print_verbose("Successfully completed signing build.");
2628 return OK;
2629}
2630
2631void EditorExportPlatformAndroid::_clear_assets_directory() {
2632 Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);
2633
2634 // Clear the APK assets directory
2635 if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) {
2636 print_verbose("Clearing APK assets directory...");
2637 Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY);
2638 da_assets->erase_contents_recursive();
2639 da_res->remove(APK_ASSETS_DIRECTORY);
2640 }
2641
2642 // Clear the AAB assets directory
2643 if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) {
2644 print_verbose("Clearing AAB assets directory...");
2645 Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY);
2646 da_assets->erase_contents_recursive();
2647 da_res->remove(AAB_ASSETS_DIRECTORY);
2648 }
2649}
2650
2651void EditorExportPlatformAndroid::_remove_copied_libs() {
2652 print_verbose("Removing previously installed libraries...");
2653 Error error;
2654 String libs_json = FileAccess::get_file_as_string(GDEXTENSION_LIBS_PATH, &error);
2655 if (error || libs_json.is_empty()) {
2656 print_verbose("No previously installed libraries found");
2657 return;
2658 }
2659
2660 JSON json;
2661 error = json.parse(libs_json);
2662 ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message());
2663
2664 Vector<String> libs = json.get_data();
2665 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
2666 for (int i = 0; i < libs.size(); i++) {
2667 print_verbose("Removing previously installed library " + libs[i]);
2668 da->remove(libs[i]);
2669 }
2670 da->remove(GDEXTENSION_LIBS_PATH);
2671}
2672
2673String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {
2674 String ret;
2675 for (int i = 0; i < p_parts.size(); ++i) {
2676 if (i > 0) {
2677 ret += p_separator;
2678 }
2679 ret += p_parts[i];
2680 }
2681 return ret;
2682}
2683
2684String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformAndroid::ABI> &p_parts, const String &p_separator, bool p_use_arch) {
2685 String ret;
2686 for (int i = 0; i < p_parts.size(); ++i) {
2687 if (i > 0) {
2688 ret += p_separator;
2689 }
2690 ret += (p_use_arch) ? p_parts[i].arch : p_parts[i].abi;
2691 }
2692 return ret;
2693}
2694
2695String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const {
2696 Vector<String> names;
2697
2698#ifndef DISABLE_DEPRECATED
2699 PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);
2700#endif // DISABLE_DEPRECATED
2701
2702 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
2703 for (int i = 0; i < export_plugins.size(); i++) {
2704 if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
2705 names.push_back(export_plugins[i]->get_name());
2706 }
2707 }
2708
2709 String plugins_names = String("|").join(names);
2710 return plugins_names;
2711}
2712
2713String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const {
2714 String absolute_path;
2715 if (!p_android_library_path.is_empty()) {
2716 if (p_android_library_path.is_absolute_path()) {
2717 absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path);
2718 } else {
2719 const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path);
2720 absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path);
2721 }
2722 }
2723 return absolute_path;
2724}
2725
2726bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {
2727 bool first_build = last_gradle_build_time == 0;
2728 bool have_plugins_changed = false;
2729
2730 String plugin_names = _get_plugins_names(p_preset);
2731
2732 if (!first_build) {
2733 have_plugins_changed = plugin_names != last_plugin_names;
2734#ifndef DISABLE_DEPRECATED
2735 if (!have_plugins_changed) {
2736 Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
2737 for (int i = 0; i < enabled_plugins.size(); i++) {
2738 if (enabled_plugins.get(i).last_updated > last_gradle_build_time) {
2739 have_plugins_changed = true;
2740 break;
2741 }
2742 }
2743 }
2744#endif // DISABLE_DEPRECATED
2745 }
2746
2747 last_gradle_build_time = OS::get_singleton()->get_unix_time();
2748 last_plugin_names = plugin_names;
2749
2750 return have_plugins_changed || first_build;
2751}
2752
2753Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
2754 int export_format = int(p_preset->get("gradle_build/export_format"));
2755 bool should_sign = p_preset->get("package/signed");
2756 return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);
2757}
2758
2759Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags) {
2760 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
2761
2762 String src_apk;
2763 Error err;
2764
2765 EditorProgress ep("export", TTR("Exporting for Android"), 105, true);
2766
2767 bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));
2768 bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG);
2769 bool apk_expansion = p_preset->get("apk_expansion/enable");
2770 Vector<ABI> enabled_abis = get_enabled_abis(p_preset);
2771
2772 print_verbose("Exporting for Android...");
2773 print_verbose("- debug build: " + bool_to_string(p_debug));
2774 print_verbose("- export path: " + p_path);
2775 print_verbose("- export format: " + itos(export_format));
2776 print_verbose("- sign build: " + bool_to_string(should_sign));
2777 print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build));
2778 print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion));
2779 print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false));
2780 print_verbose("- export filter: " + itos(p_preset->get_export_filter()));
2781 print_verbose("- include filter: " + p_preset->get_include_filter());
2782 print_verbose("- exclude filter: " + p_preset->get_exclude_filter());
2783
2784 Ref<Image> splash_image;
2785 Ref<Image> splash_bg_color_image;
2786 String processed_splash_config_xml = load_splash_refs(splash_image, splash_bg_color_image);
2787
2788 Ref<Image> main_image;
2789 Ref<Image> foreground;
2790 Ref<Image> background;
2791
2792 load_icon_refs(p_preset, main_image, foreground, background);
2793
2794 Vector<uint8_t> command_line_flags;
2795 // Write command line flags into the command_line_flags variable.
2796 get_command_line_flags(p_preset, p_path, p_flags, command_line_flags);
2797
2798 if (export_format == EXPORT_FORMAT_AAB) {
2799 if (!p_path.ends_with(".aab")) {
2800 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));
2801 return ERR_UNCONFIGURED;
2802 }
2803 if (apk_expansion) {
2804 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));
2805 return ERR_UNCONFIGURED;
2806 }
2807 }
2808 if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {
2809 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));
2810 return ERR_UNCONFIGURED;
2811 }
2812 if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) {
2813 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));
2814 return ERR_UNCONFIGURED;
2815 }
2816
2817 if (use_gradle_build) {
2818 print_verbose("Starting gradle build...");
2819 //test that installed build version is alright
2820 {
2821 print_verbose("Checking build version...");
2822 Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ);
2823 if (f.is_null()) {
2824 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."));
2825 return ERR_UNCONFIGURED;
2826 }
2827 String version = f->get_line().strip_edges();
2828 print_verbose("- build version: " + version);
2829 if (version != VERSION_FULL_CONFIG) {
2830 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG));
2831 return ERR_UNCONFIGURED;
2832 }
2833 }
2834 const String assets_directory = get_assets_directory(p_preset, export_format);
2835 String sdk_path = EDITOR_GET("export/android/android_sdk_path");
2836 ERR_FAIL_COND_V_MSG(sdk_path.is_empty(), ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'.");
2837 print_verbose("Android sdk path: " + sdk_path);
2838
2839 // TODO: should we use "package/name" or "application/config/name"?
2840 String project_name = get_project_name(p_preset->get("package/name"));
2841 err = _create_project_name_strings_files(p_preset, project_name); //project name localization.
2842 if (err != OK) {
2843 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name."));
2844 }
2845 // Copies the project icon files into the appropriate Gradle project directory.
2846 _copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background);
2847 // Write an AndroidManifest.xml file into the Gradle project directory.
2848 _write_tmp_manifest(p_preset, p_give_internet, p_debug);
2849
2850 //stores all the project files inside the Gradle project directory. Also includes all ABIs
2851 _clear_assets_directory();
2852 _remove_copied_libs();
2853 if (!apk_expansion) {
2854 print_verbose("Exporting project files...");
2855 CustomExportData user_data;
2856 user_data.assets_directory = assets_directory;
2857 user_data.debug = p_debug;
2858 if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
2859 err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so);
2860 } else {
2861 err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so);
2862 }
2863 if (err != OK) {
2864 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));
2865 return err;
2866 }
2867 if (user_data.libs.size() > 0) {
2868 Ref<FileAccess> fa = FileAccess::open(GDEXTENSION_LIBS_PATH, FileAccess::WRITE);
2869 fa->store_string(JSON::stringify(user_data.libs, "\t"));
2870 }
2871 } else {
2872 print_verbose("Saving apk expansion file...");
2873 err = save_apk_expansion_file(p_preset, p_debug, p_path);
2874 if (err != OK) {
2875 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
2876 return err;
2877 }
2878 }
2879 print_verbose("Storing command line flags...");
2880 store_file_at_path(assets_directory + "/_cl_", command_line_flags);
2881
2882 print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
2883 OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required
2884 String build_command;
2885
2886#ifdef WINDOWS_ENABLED
2887 build_command = "gradlew.bat";
2888#else
2889 build_command = "gradlew";
2890#endif
2891
2892 String build_path = ProjectSettings::get_singleton()->get_resource_path().path_join("android/build");
2893 build_command = build_path.path_join(build_command);
2894
2895 String package_name = get_package_name(p_preset->get("package/unique_name"));
2896 String version_code = itos(p_preset->get("version/code"));
2897 String version_name = p_preset->get_version("version/name");
2898 String min_sdk_version = p_preset->get("gradle_build/min_sdk");
2899 if (!min_sdk_version.is_valid_int()) {
2900 min_sdk_version = itos(VULKAN_MIN_SDK_VERSION);
2901 }
2902 String target_sdk_version = p_preset->get("gradle_build/target_sdk");
2903 if (!target_sdk_version.is_valid_int()) {
2904 target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
2905 }
2906 String enabled_abi_string = join_abis(enabled_abis, "|", false);
2907 String sign_flag = should_sign ? "true" : "false";
2908 String zipalign_flag = "true";
2909
2910 Vector<String> android_libraries;
2911 Vector<String> android_dependencies;
2912 Vector<String> android_dependencies_maven_repos;
2913
2914#ifndef DISABLE_DEPRECATED
2915 Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);
2916 PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries);
2917 PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies);
2918 PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);
2919#endif // DISABLE_DEPRECATED
2920
2921 Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
2922 for (int i = 0; i < export_plugins.size(); i++) {
2923 if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {
2924 PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug);
2925 for (int k = 0; k < export_plugin_android_libraries.size(); k++) {
2926 const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]);
2927 if (!resolved_android_library_path.is_empty()) {
2928 android_libraries.push_back(resolved_android_library_path);
2929 }
2930 }
2931
2932 PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug);
2933 android_dependencies.append_array(export_plugin_android_dependencies);
2934
2935 PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);
2936 android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);
2937 }
2938 }
2939
2940 bool clean_build_required = _is_clean_build_required(p_preset);
2941 String combined_android_libraries = String("|").join(android_libraries);
2942 String combined_android_dependencies = String("|").join(android_dependencies);
2943 String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
2944
2945 List<String> cmdline;
2946 if (clean_build_required) {
2947 cmdline.push_back("clean");
2948 }
2949
2950 String build_type = p_debug ? "Debug" : "Release";
2951 if (export_format == EXPORT_FORMAT_AAB) {
2952 String bundle_build_command = vformat("bundle%s", build_type);
2953 cmdline.push_back(bundle_build_command);
2954 } else if (export_format == EXPORT_FORMAT_APK) {
2955 String apk_build_command = vformat("assemble%s", build_type);
2956 cmdline.push_back(apk_build_command);
2957 }
2958
2959 cmdline.push_back("-p"); // argument to specify the start directory.
2960 cmdline.push_back(build_path); // start directory.
2961 cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
2962 cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.
2963 cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name.
2964 cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk.
2965 cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk.
2966 cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs.
2967 cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins.
2968 cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins.
2969 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.
2970 cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned.
2971 cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed.
2972 cmdline.push_back("-Pgodot_editor_version=" + String(VERSION_FULL_CONFIG));
2973
2974 // NOTE: The release keystore is not included in the verbose logging
2975 // to avoid accidentally leaking sensitive information when sharing verbose logs for troubleshooting.
2976 // Any non-sensitive additions to the command line arguments must be done above this section.
2977 // Sensitive additions must be done below the logging statement.
2978 print_verbose("Build Android project using gradle command: " + String("\n") + build_command + " " + join_list(cmdline, String(" ")));
2979
2980 if (should_sign) {
2981 if (p_debug) {
2982 String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH);
2983 String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);
2984 String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);
2985
2986 if (debug_keystore.is_empty()) {
2987 debug_keystore = EDITOR_GET("export/android/debug_keystore");
2988 debug_password = EDITOR_GET("export/android/debug_keystore_pass");
2989 debug_user = EDITOR_GET("export/android/debug_keystore_user");
2990 }
2991 if (debug_keystore.is_relative_path()) {
2992 debug_keystore = OS::get_singleton()->get_resource_dir().path_join(debug_keystore).simplify_path();
2993 }
2994 if (!FileAccess::exists(debug_keystore)) {
2995 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
2996 return ERR_FILE_CANT_OPEN;
2997 }
2998
2999 cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file.
3000 cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias.
3001 cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.
3002 } else {
3003 // Pass the release keystore info as well
3004 String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH);
3005 String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
3006 String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
3007 if (release_keystore.is_relative_path()) {
3008 release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path();
3009 }
3010 if (!FileAccess::exists(release_keystore)) {
3011 add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export."));
3012 return ERR_FILE_CANT_OPEN;
3013 }
3014
3015 cmdline.push_back("-Prelease_keystore_file=" + release_keystore); // argument to specify the release keystore file.
3016 cmdline.push_back("-Prelease_keystore_alias=" + release_username); // argument to specify the release keystore alias.
3017 cmdline.push_back("-Prelease_keystore_password=" + release_password); // argument to specify the release keystore password.
3018 }
3019 }
3020
3021 int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline);
3022 if (result != 0) {
3023 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation."));
3024 return ERR_CANT_CREATE;
3025 }
3026
3027 List<String> copy_args;
3028 String copy_command;
3029 if (export_format == EXPORT_FORMAT_AAB) {
3030 copy_command = vformat("copyAndRename%sAab", build_type);
3031 } else if (export_format == EXPORT_FORMAT_APK) {
3032 copy_command = vformat("copyAndRename%sApk", build_type);
3033 }
3034
3035 copy_args.push_back(copy_command);
3036
3037 copy_args.push_back("-p"); // argument to specify the start directory.
3038 copy_args.push_back(build_path); // start directory.
3039
3040 String export_filename = p_path.get_file();
3041 String export_path = p_path.get_base_dir();
3042 if (export_path.is_relative_path()) {
3043 export_path = OS::get_singleton()->get_resource_dir().path_join(export_path);
3044 }
3045 export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path();
3046
3047 copy_args.push_back("-Pexport_path=file:" + export_path);
3048 copy_args.push_back("-Pexport_filename=" + export_filename);
3049
3050 print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
3051 int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args);
3052 if (copy_result != 0) {
3053 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs."));
3054 return ERR_CANT_CREATE;
3055 }
3056
3057 print_verbose("Successfully completed Android gradle build.");
3058 return OK;
3059 }
3060 // This is the start of the Legacy build system
3061 print_verbose("Starting legacy build system...");
3062 if (p_debug) {
3063 src_apk = p_preset->get("custom_template/debug");
3064 } else {
3065 src_apk = p_preset->get("custom_template/release");
3066 }
3067 src_apk = src_apk.strip_edges();
3068 if (src_apk.is_empty()) {
3069 if (p_debug) {
3070 src_apk = find_export_template("android_debug.apk");
3071 } else {
3072 src_apk = find_export_template("android_release.apk");
3073 }
3074 if (src_apk.is_empty()) {
3075 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk));
3076 return ERR_FILE_NOT_FOUND;
3077 }
3078 }
3079
3080 if (!DirAccess::exists(p_path.get_base_dir())) {
3081 return ERR_FILE_BAD_PATH;
3082 }
3083
3084 Ref<FileAccess> io_fa;
3085 zlib_filefunc_def io = zipio_create_io(&io_fa);
3086
3087 if (ep.step(TTR("Creating APK..."), 0)) {
3088 return ERR_SKIP;
3089 }
3090
3091 unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io);
3092 if (!pkg) {
3093 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk));
3094 return ERR_FILE_NOT_FOUND;
3095 }
3096
3097 int ret = unzGoToFirstFile(pkg);
3098
3099 Ref<FileAccess> io2_fa;
3100 zlib_filefunc_def io2 = zipio_create_io(&io2_fa);
3101
3102 String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");
3103
3104#define CLEANUP_AND_RETURN(m_err) \
3105 { \
3106 DirAccess::remove_file_or_error(tmp_unaligned_path); \
3107 return m_err; \
3108 } \
3109 ((void)0)
3110
3111 zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
3112
3113 String cmdline = p_preset->get("command_line/extra_args");
3114
3115 String version_name = p_preset->get_version("version/name");
3116 String package_name = p_preset->get("package/unique_name");
3117
3118 String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");
3119
3120 Vector<ABI> invalid_abis(enabled_abis);
3121 while (ret == UNZ_OK) {
3122 //get filename
3123 unz_file_info info;
3124 char fname[16384];
3125 ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
3126 if (ret != UNZ_OK) {
3127 break;
3128 }
3129
3130 bool skip = false;
3131
3132 String file = String::utf8(fname);
3133
3134 Vector<uint8_t> data;
3135 data.resize(info.uncompressed_size);
3136
3137 //read
3138 unzOpenCurrentFile(pkg);
3139 unzReadCurrentFile(pkg, data.ptrw(), data.size());
3140 unzCloseCurrentFile(pkg);
3141
3142 //write
3143 if (file == "AndroidManifest.xml") {
3144 _fix_manifest(p_preset, data, p_give_internet);
3145 }
3146 if (file == "resources.arsc") {
3147 _fix_resources(p_preset, data);
3148 }
3149
3150 // Process the splash image
3151 if ((file == SPLASH_IMAGE_EXPORT_PATH || file == LEGACY_BUILD_SPLASH_IMAGE_EXPORT_PATH) && splash_image.is_valid() && !splash_image->is_empty()) {
3152 _load_image_data(splash_image, data);
3153 }
3154
3155 // Process the splash bg color image
3156 if ((file == SPLASH_BG_COLOR_PATH || file == LEGACY_BUILD_SPLASH_BG_COLOR_PATH) && splash_bg_color_image.is_valid() && !splash_bg_color_image->is_empty()) {
3157 _load_image_data(splash_bg_color_image, data);
3158 }
3159
3160 if (file.ends_with(".png") && file.contains("mipmap")) {
3161 for (int i = 0; i < icon_densities_count; ++i) {
3162 if (main_image.is_valid() && !main_image->is_empty()) {
3163 if (file == launcher_icons[i].export_path) {
3164 _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data);
3165 }
3166 }
3167 if (foreground.is_valid() && !foreground->is_empty()) {
3168 if (file == launcher_adaptive_icon_foregrounds[i].export_path) {
3169 _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data);
3170 }
3171 }
3172 if (background.is_valid() && !background->is_empty()) {
3173 if (file == launcher_adaptive_icon_backgrounds[i].export_path) {
3174 _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data);
3175 }
3176 }
3177 }
3178 }
3179
3180 if (file.ends_with(".so")) {
3181 bool enabled = false;
3182 for (int i = 0; i < enabled_abis.size(); ++i) {
3183 if (file.begins_with("lib/" + enabled_abis[i].abi + "/")) {
3184 invalid_abis.erase(enabled_abis[i]);
3185 enabled = true;
3186 break;
3187 }
3188 }
3189 if (!enabled) {
3190 skip = true;
3191 }
3192 }
3193
3194 if (file.begins_with("META-INF") && should_sign) {
3195 skip = true;
3196 }
3197
3198 if (!skip) {
3199 print_line("ADDING: " + file);
3200
3201 // Respect decision on compression made by AAPT for the export template
3202 const bool uncompressed = info.compression_method == 0;
3203
3204 zip_fileinfo zipfi = get_zip_fileinfo();
3205
3206 zipOpenNewFileInZip(unaligned_apk,
3207 file.utf8().get_data(),
3208 &zipfi,
3209 nullptr,
3210 0,
3211 nullptr,
3212 0,
3213 nullptr,
3214 uncompressed ? 0 : Z_DEFLATED,
3215 Z_DEFAULT_COMPRESSION);
3216
3217 zipWriteInFileInZip(unaligned_apk, data.ptr(), data.size());
3218 zipCloseFileInZip(unaligned_apk);
3219 }
3220
3221 ret = unzGoToNextFile(pkg);
3222 }
3223
3224 if (!invalid_abis.is_empty()) {
3225 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)));
3226 CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
3227 }
3228
3229 if (ep.step(TTR("Adding files..."), 1)) {
3230 CLEANUP_AND_RETURN(ERR_SKIP);
3231 }
3232 err = OK;
3233
3234 if (p_flags & DEBUG_FLAG_DUMB_CLIENT) {
3235 APKExportData ed;
3236 ed.ep = &ep;
3237 ed.apk = unaligned_apk;
3238 err = export_project_files(p_preset, p_debug, ignore_apk_file, &ed, save_apk_so);
3239 } else {
3240 if (apk_expansion) {
3241 err = save_apk_expansion_file(p_preset, p_debug, p_path);
3242 if (err != OK) {
3243 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));
3244 return err;
3245 }
3246 } else {
3247 APKExportData ed;
3248 ed.ep = &ep;
3249 ed.apk = unaligned_apk;
3250 err = export_project_files(p_preset, p_debug, save_apk_file, &ed, save_apk_so);
3251 }
3252 }
3253
3254 if (err != OK) {
3255 unzClose(pkg);
3256 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files.")));
3257 CLEANUP_AND_RETURN(ERR_SKIP);
3258 }
3259
3260 zip_fileinfo zipfi = get_zip_fileinfo();
3261 zipOpenNewFileInZip(unaligned_apk,
3262 "assets/_cl_",
3263 &zipfi,
3264 nullptr,
3265 0,
3266 nullptr,
3267 0,
3268 nullptr,
3269 0, // No compress (little size gain and potentially slower startup)
3270 Z_DEFAULT_COMPRESSION);
3271 zipWriteInFileInZip(unaligned_apk, command_line_flags.ptr(), command_line_flags.size());
3272 zipCloseFileInZip(unaligned_apk);
3273 zipClose(unaligned_apk, nullptr);
3274 unzClose(pkg);
3275
3276 if (err != OK) {
3277 CLEANUP_AND_RETURN(err);
3278 }
3279
3280 // Let's zip-align (must be done before signing)
3281
3282 static const int ZIP_ALIGNMENT = 4;
3283
3284 // If we're not signing the apk, then the next step should be the last.
3285 const int next_step = should_sign ? 103 : 105;
3286 if (ep.step(TTR("Aligning APK..."), next_step)) {
3287 CLEANUP_AND_RETURN(ERR_SKIP);
3288 }
3289
3290 unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io);
3291 if (!tmp_unaligned) {
3292 add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK.")));
3293 CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);
3294 }
3295
3296 ret = unzGoToFirstFile(tmp_unaligned);
3297
3298 io2 = zipio_create_io(&io2_fa);
3299 zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);
3300
3301 // Take files from the unaligned APK and write them out to the aligned one
3302 // in raw mode, i.e. not uncompressing and recompressing, aligning them as needed,
3303 // following what is done in https://github.com/android/platform_build/blob/master/tools/zipalign/ZipAlign.cpp
3304 int bias = 0;
3305 while (ret == UNZ_OK) {
3306 unz_file_info info;
3307 memset(&info, 0, sizeof(info));
3308
3309 char fname[16384];
3310 char extra[16384];
3311 ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0);
3312 if (ret != UNZ_OK) {
3313 break;
3314 }
3315
3316 String file = String::utf8(fname);
3317
3318 Vector<uint8_t> data;
3319 data.resize(info.compressed_size);
3320
3321 // read
3322 int method, level;
3323 unzOpenCurrentFile2(tmp_unaligned, &method, &level, 1); // raw read
3324 long file_offset = unzGetCurrentFileZStreamPos64(tmp_unaligned);
3325 unzReadCurrentFile(tmp_unaligned, data.ptrw(), data.size());
3326 unzCloseCurrentFile(tmp_unaligned);
3327
3328 // align
3329 int padding = 0;
3330 if (!info.compression_method) {
3331 // Uncompressed file => Align
3332 long new_offset = file_offset + bias;
3333 padding = (ZIP_ALIGNMENT - (new_offset % ZIP_ALIGNMENT)) % ZIP_ALIGNMENT;
3334 }
3335
3336 memset(extra + info.size_file_extra, 0, padding);
3337
3338 zip_fileinfo fileinfo = get_zip_fileinfo();
3339 zipOpenNewFileInZip2(final_apk,
3340 file.utf8().get_data(),
3341 &fileinfo,
3342 extra,
3343 info.size_file_extra + padding,
3344 nullptr,
3345 0,
3346 nullptr,
3347 method,
3348 level,
3349 1); // raw write
3350 zipWriteInFileInZip(final_apk, data.ptr(), data.size());
3351 zipCloseFileInZipRaw(final_apk, info.uncompressed_size, info.crc);
3352
3353 bias += padding;
3354
3355 ret = unzGoToNextFile(tmp_unaligned);
3356 }
3357
3358 zipClose(final_apk, nullptr);
3359 unzClose(tmp_unaligned);
3360
3361 if (should_sign) {
3362 // Signing must be done last as any additional modifications to the
3363 // file will invalidate the signature.
3364 err = sign_apk(p_preset, p_debug, p_path, ep);
3365 if (err != OK) {
3366 CLEANUP_AND_RETURN(err);
3367 }
3368 }
3369
3370 CLEANUP_AND_RETURN(OK);
3371}
3372
3373void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) const {
3374 r_features->push_back("mobile");
3375 r_features->push_back("android");
3376}
3377
3378void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {
3379}
3380
3381EditorExportPlatformAndroid::EditorExportPlatformAndroid() {
3382 if (EditorNode::get_singleton()) {
3383#ifdef MODULE_SVG_ENABLED
3384 Ref<Image> img = memnew(Image);
3385 const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
3386
3387 ImageLoaderSVG::create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false);
3388 logo = ImageTexture::create_from_image(img);
3389
3390 ImageLoaderSVG::create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false);
3391 run_icon = ImageTexture::create_from_image(img);
3392#endif
3393
3394 devices_changed.set();
3395#ifndef DISABLE_DEPRECATED
3396 android_plugins_changed.set();
3397#endif // DISABLE_DEPRECATED
3398#ifndef ANDROID_ENABLED
3399 check_for_changes_thread.start(_check_for_changes_poll_thread, this);
3400#endif
3401 }
3402}
3403
3404EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
3405#ifndef ANDROID_ENABLED
3406 quit_request.set();
3407 if (check_for_changes_thread.is_started()) {
3408 check_for_changes_thread.wait_to_finish();
3409 }
3410#endif
3411}
3412