Path: blob/master/platform/android/export/export_plugin.cpp
20920 views
/**************************************************************************/1/* export_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "export_plugin.h"3132#include "logo_svg.gen.h"33#include "run_icon_svg.gen.h"3435#include "core/config/project_settings.h"36#include "core/io/dir_access.h"37#include "core/io/file_access.h"38#include "core/io/image_loader.h"39#include "core/io/json.h"40#include "core/io/marshalls.h"41#include "core/math/random_pcg.h"42#include "core/string/translation_server.h"43#include "core/version.h"44#include "editor/editor_log.h"45#include "editor/editor_node.h"46#include "editor/editor_string_names.h"47#include "editor/export/export_template_manager.h"48#include "editor/file_system/editor_paths.h"49#include "editor/import/resource_importer_texture_settings.h"50#include "editor/settings/editor_settings.h"51#include "editor/themes/editor_scale.h"52#include "main/splash.gen.h"53#include "scene/resources/image_texture.h"5455#include "modules/modules_enabled.gen.h" // For mono.56#include "modules/svg/image_loader_svg.h"5758#ifdef MODULE_MONO_ENABLED59#include "modules/mono/utils/path_utils.h"60#endif6162#ifdef ANDROID_ENABLED63#include "../java_godot_wrapper.h"64#include "../os_android.h"65#include "android_editor_gradle_runner.h"66#endif6768static const char *ANDROID_PERMS[] = {69"ACCESS_CHECKIN_PROPERTIES",70"ACCESS_COARSE_LOCATION",71"ACCESS_FINE_LOCATION",72"ACCESS_LOCATION_EXTRA_COMMANDS",73"ACCESS_MEDIA_LOCATION",74"ACCESS_MOCK_LOCATION",75"ACCESS_NETWORK_STATE",76"ACCESS_SURFACE_FLINGER",77"ACCESS_WIFI_STATE",78"ACCOUNT_MANAGER",79"ADD_VOICEMAIL",80"AUTHENTICATE_ACCOUNTS",81"BATTERY_STATS",82"BIND_ACCESSIBILITY_SERVICE",83"BIND_APPWIDGET",84"BIND_DEVICE_ADMIN",85"BIND_INPUT_METHOD",86"BIND_NFC_SERVICE",87"BIND_NOTIFICATION_LISTENER_SERVICE",88"BIND_PRINT_SERVICE",89"BIND_REMOTEVIEWS",90"BIND_TEXT_SERVICE",91"BIND_VPN_SERVICE",92"BIND_WALLPAPER",93"BLUETOOTH",94"BLUETOOTH_ADMIN",95"BLUETOOTH_PRIVILEGED",96"BRICK",97"BROADCAST_PACKAGE_REMOVED",98"BROADCAST_SMS",99"BROADCAST_STICKY",100"BROADCAST_WAP_PUSH",101"CALL_PHONE",102"CALL_PRIVILEGED",103"CAMERA",104"CAPTURE_AUDIO_OUTPUT",105"CAPTURE_SECURE_VIDEO_OUTPUT",106"CAPTURE_VIDEO_OUTPUT",107"CHANGE_COMPONENT_ENABLED_STATE",108"CHANGE_CONFIGURATION",109"CHANGE_NETWORK_STATE",110"CHANGE_WIFI_MULTICAST_STATE",111"CHANGE_WIFI_STATE",112"CLEAR_APP_CACHE",113"CLEAR_APP_USER_DATA",114"CONTROL_LOCATION_UPDATES",115"DELETE_CACHE_FILES",116"DELETE_PACKAGES",117"DEVICE_POWER",118"DIAGNOSTIC",119"DISABLE_KEYGUARD",120"DUMP",121"EXPAND_STATUS_BAR",122"FACTORY_TEST",123"FLASHLIGHT",124"FORCE_BACK",125"GET_ACCOUNTS",126"GET_PACKAGE_SIZE",127"GET_TASKS",128"GET_TOP_ACTIVITY_INFO",129"GLOBAL_SEARCH",130"HARDWARE_TEST",131"INJECT_EVENTS",132"INSTALL_LOCATION_PROVIDER",133"INSTALL_PACKAGES",134"INSTALL_SHORTCUT",135"INTERNAL_SYSTEM_WINDOW",136"INTERNET",137"KILL_BACKGROUND_PROCESSES",138"LOCATION_HARDWARE",139"MANAGE_ACCOUNTS",140"MANAGE_APP_TOKENS",141"MANAGE_DOCUMENTS",142"MANAGE_EXTERNAL_STORAGE",143"MANAGE_MEDIA",144"MASTER_CLEAR",145"MEDIA_CONTENT_CONTROL",146"MODIFY_AUDIO_SETTINGS",147"MODIFY_PHONE_STATE",148"MOUNT_FORMAT_FILESYSTEMS",149"MOUNT_UNMOUNT_FILESYSTEMS",150"NFC",151"PERSISTENT_ACTIVITY",152"POST_NOTIFICATIONS",153"PROCESS_OUTGOING_CALLS",154"READ_CALENDAR",155"READ_CALL_LOG",156"READ_CONTACTS",157"READ_EXTERNAL_STORAGE",158"READ_FRAME_BUFFER",159"READ_HISTORY_BOOKMARKS",160"READ_INPUT_STATE",161"READ_LOGS",162"READ_MEDIA_AUDIO",163"READ_MEDIA_IMAGES",164"READ_MEDIA_VIDEO",165"READ_MEDIA_VISUAL_USER_SELECTED",166"READ_PHONE_STATE",167"READ_PROFILE",168"READ_SMS",169"READ_SOCIAL_STREAM",170"READ_SYNC_SETTINGS",171"READ_SYNC_STATS",172"READ_USER_DICTIONARY",173"REBOOT",174"RECEIVE_BOOT_COMPLETED",175"RECEIVE_MMS",176"RECEIVE_SMS",177"RECEIVE_WAP_PUSH",178"RECORD_AUDIO",179"REORDER_TASKS",180"RESTART_PACKAGES",181"SEND_RESPOND_VIA_MESSAGE",182"SEND_SMS",183"SET_ACTIVITY_WATCHER",184"SET_ALARM",185"SET_ALWAYS_FINISH",186"SET_ANIMATION_SCALE",187"SET_DEBUG_APP",188"SET_ORIENTATION",189"SET_POINTER_SPEED",190"SET_PREFERRED_APPLICATIONS",191"SET_PROCESS_LIMIT",192"SET_TIME",193"SET_TIME_ZONE",194"SET_WALLPAPER",195"SET_WALLPAPER_HINTS",196"SIGNAL_PERSISTENT_PROCESSES",197"STATUS_BAR",198"SUBSCRIBED_FEEDS_READ",199"SUBSCRIBED_FEEDS_WRITE",200"SYSTEM_ALERT_WINDOW",201"TRANSMIT_IR",202"UNINSTALL_SHORTCUT",203"UPDATE_DEVICE_STATS",204"USE_CREDENTIALS",205"USE_SIP",206"VIBRATE",207"WAKE_LOCK",208"WRITE_APN_SETTINGS",209"WRITE_CALENDAR",210"WRITE_CALL_LOG",211"WRITE_CONTACTS",212"WRITE_EXTERNAL_STORAGE",213"WRITE_GSERVICES",214"WRITE_HISTORY_BOOKMARKS",215"WRITE_PROFILE",216"WRITE_SECURE_SETTINGS",217"WRITE_SETTINGS",218"WRITE_SMS",219"WRITE_SOCIAL_STREAM",220"WRITE_SYNC_SETTINGS",221"WRITE_USER_DICTIONARY",222nullptr223};224225static const char *MISMATCHED_VERSIONS_MESSAGE = "Android build version mismatch:\n| Template installed: %s\n| Requested version: %s\nPlease reinstall Android build template from 'Project' menu.";226227static const char *GDEXTENSION_LIBS_PATH = "libs/gdextensionlibs.json";228229// This template string must be in sync with the content of 'platform/android/java/lib/src/main/java/res/mipmap-anydpi-v26/icon.xml'.230static const String ICON_XML_TEMPLATE =231"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"232"<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n"233" <background android:drawable=\"@mipmap/icon_background\"/>\n"234" <foreground android:drawable=\"@mipmap/icon_foreground\"/>\n"235"%s" // Placeholder for the optional monochrome tag.236"</adaptive-icon>";237238static const String ICON_XML_PATH = "res/mipmap-anydpi-v26/icon.xml";239static const String THEMED_ICON_XML_PATH = "res/mipmap-anydpi-v26/themed_icon.xml";240241static const int ICON_DENSITIES_COUNT = 6;242static const char *LAUNCHER_ICON_OPTION = PNAME("launcher_icons/main_192x192");243static const char *LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION = PNAME("launcher_icons/adaptive_foreground_432x432");244static const char *LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION = PNAME("launcher_icons/adaptive_background_432x432");245static const char *LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION = PNAME("launcher_icons/adaptive_monochrome_432x432");246247static const LauncherIcon LAUNCHER_ICONS[ICON_DENSITIES_COUNT] = {248{ "res/mipmap-xxxhdpi-v4/icon.webp", 192 },249{ "res/mipmap-xxhdpi-v4/icon.webp", 144 },250{ "res/mipmap-xhdpi-v4/icon.webp", 96 },251{ "res/mipmap-hdpi-v4/icon.webp", 72 },252{ "res/mipmap-mdpi-v4/icon.webp", 48 },253{ "res/mipmap/icon.webp", 192 }254};255256static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[ICON_DENSITIES_COUNT] = {257{ "res/mipmap-xxxhdpi-v4/icon_foreground.webp", 432 },258{ "res/mipmap-xxhdpi-v4/icon_foreground.webp", 324 },259{ "res/mipmap-xhdpi-v4/icon_foreground.webp", 216 },260{ "res/mipmap-hdpi-v4/icon_foreground.webp", 162 },261{ "res/mipmap-mdpi-v4/icon_foreground.webp", 108 },262{ "res/mipmap/icon_foreground.webp", 432 }263};264265static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[ICON_DENSITIES_COUNT] = {266{ "res/mipmap-xxxhdpi-v4/icon_background.webp", 432 },267{ "res/mipmap-xxhdpi-v4/icon_background.webp", 324 },268{ "res/mipmap-xhdpi-v4/icon_background.webp", 216 },269{ "res/mipmap-hdpi-v4/icon_background.webp", 162 },270{ "res/mipmap-mdpi-v4/icon_background.webp", 108 },271{ "res/mipmap/icon_background.webp", 432 }272};273274static const LauncherIcon LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[ICON_DENSITIES_COUNT] = {275{ "res/mipmap-xxxhdpi-v4/icon_monochrome.webp", 432 },276{ "res/mipmap-xxhdpi-v4/icon_monochrome.webp", 324 },277{ "res/mipmap-xhdpi-v4/icon_monochrome.webp", 216 },278{ "res/mipmap-hdpi-v4/icon_monochrome.webp", 162 },279{ "res/mipmap-mdpi-v4/icon_monochrome.webp", 108 },280{ "res/mipmap/icon_monochrome.webp", 432 }281};282283static const int EXPORT_FORMAT_APK = 0;284static const int EXPORT_FORMAT_AAB = 1;285286static const char *APK_ASSETS_DIRECTORY = "src/main/assets";287static const char *AAB_ASSETS_DIRECTORY = "assetPackInstallTime/src/main/assets";288289static const int DEFAULT_MIN_SDK_VERSION = 24; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'290static const int DEFAULT_TARGET_SDK_VERSION = 36; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'291292#ifndef ANDROID_ENABLED293void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {294EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);295296while (!ea->quit_request.is_set()) {297#ifndef DISABLE_DEPRECATED298// Check for android plugins updates299{300// Nothing to do if we already know the plugins have changed.301if (!ea->android_plugins_changed.is_set()) {302Vector<PluginConfigAndroid> loaded_plugins = get_plugins();303304MutexLock lock(ea->android_plugins_lock);305306if (ea->android_plugins.size() != loaded_plugins.size()) {307ea->android_plugins_changed.set();308} else {309for (int i = 0; i < ea->android_plugins.size(); i++) {310if (ea->android_plugins[i].name != loaded_plugins[i].name) {311ea->android_plugins_changed.set();312break;313}314}315}316317if (ea->android_plugins_changed.is_set()) {318ea->android_plugins = loaded_plugins;319}320}321}322#endif // DISABLE_DEPRECATED323324// Check for devices updates325String adb = get_adb_path();326// adb.exe was locking the editor_doc_cache file on startup. Adding a check for is_editor_ready provides just enough time327// to regenerate the doc cache.328if (ea->has_runnable_preset.is_set() && FileAccess::exists(adb) && EditorNode::get_singleton()->is_editor_ready()) {329String devices;330List<String> args;331args.push_back("devices");332int ec;333OS::get_singleton()->execute(adb, args, &devices, &ec);334335Vector<String> ds = devices.split("\n");336Vector<String> ldevices;337for (int i = 1; i < ds.size(); i++) {338String d = ds[i];339int dpos = d.find("device");340if (dpos == -1) {341continue;342}343d = d.substr(0, dpos).strip_edges();344ldevices.push_back(d);345}346347MutexLock lock(ea->device_lock);348349bool different = false;350351if (ea->devices.size() != ldevices.size()) {352different = true;353} else {354for (int i = 0; i < ea->devices.size(); i++) {355if (ea->devices[i].id != ldevices[i]) {356different = true;357break;358}359}360}361362if (different) {363Vector<Device> ndevices;364365for (int i = 0; i < ldevices.size(); i++) {366Device d;367d.id = ldevices[i];368for (int j = 0; j < ea->devices.size(); j++) {369if (ea->devices[j].id == ldevices[i]) {370d.description = ea->devices[j].description;371d.name = ea->devices[j].name;372d.api_level = ea->devices[j].api_level;373}374}375376if (d.description.is_empty()) {377//in the oven, request!378args.clear();379args.push_back("-s");380args.push_back(d.id);381args.push_back("shell");382args.push_back("getprop");383int ec2;384String dp;385386OS::get_singleton()->execute(adb, args, &dp, &ec2);387388Vector<String> props = dp.split("\n");389String vendor;390String device;391d.description = "Device ID: " + d.id + "\n";392d.api_level = 0;393for (int j = 0; j < props.size(); j++) {394// got information by `shell cat /system/build.prop` before and its format is "property=value"395// it's now changed to use `shell getporp` because of permission issue with Android 8.0 and above396// its format is "[property]: [value]" so changed it as like build.prop397String p = props[j];398p = p.replace("]: ", "=");399p = p.remove_chars("[]");400401if (p.begins_with("ro.product.model=")) {402device = p.get_slicec('=', 1).strip_edges();403} else if (p.begins_with("ro.product.brand=")) {404vendor = p.get_slicec('=', 1).strip_edges().capitalize();405} else if (p.begins_with("ro.build.display.id=")) {406d.description += "Build: " + p.get_slicec('=', 1).strip_edges() + "\n";407} else if (p.begins_with("ro.build.version.release=")) {408d.description += "Release: " + p.get_slicec('=', 1).strip_edges() + "\n";409} else if (p.begins_with("ro.build.version.sdk=")) {410d.api_level = p.get_slicec('=', 1).to_int();411} else if (p.begins_with("ro.product.cpu.abi=")) {412d.architecture = p.get_slicec('=', 1).strip_edges();413d.description += "CPU: " + d.architecture + "\n";414} else if (p.begins_with("ro.product.manufacturer=")) {415d.description += "Manufacturer: " + p.get_slicec('=', 1).strip_edges() + "\n";416} else if (p.begins_with("ro.board.platform=")) {417d.description += "Chipset: " + p.get_slicec('=', 1).strip_edges() + "\n";418} else if (p.begins_with("ro.opengles.version=")) {419uint32_t opengl = p.get_slicec('=', 1).to_int();420d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n";421}422}423424d.name = vendor + " " + device;425if (device.is_empty()) {426continue;427}428}429430ndevices.push_back(d);431}432433ea->devices = ndevices;434ea->devices_changed.set();435}436}437438uint64_t sleep = 200;439uint64_t wait = 3000000;440uint64_t time = OS::get_singleton()->get_ticks_usec();441while (OS::get_singleton()->get_ticks_usec() - time < wait) {442OS::get_singleton()->delay_usec(1000 * sleep);443if (ea->quit_request.is_set()) {444break;445}446}447}448449if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) {450String adb = get_adb_path();451if (!FileAccess::exists(adb)) {452return; //adb not configured453}454455List<String> args;456args.push_back("kill-server");457OS::get_singleton()->execute(adb, args);458}459}460461void EditorExportPlatformAndroid::_update_preset_status() {462bool has_runnable = EditorExport::get_singleton()->get_runnable_preset_for_platform(this).is_valid();463if (has_runnable) {464has_runnable_preset.set();465} else {466has_runnable_preset.clear();467}468devices_changed.set();469}470#endif471472String EditorExportPlatformAndroid::get_project_name(const Ref<EditorExportPreset> &p_preset, const String &p_name) const {473String aname;474if (!p_name.is_empty()) {475aname = p_name;476} else {477aname = get_project_setting(p_preset, "application/config/name");478}479480if (aname.is_empty()) {481aname = GODOT_VERSION_NAME;482}483484return aname;485}486487String EditorExportPlatformAndroid::get_package_name(const Ref<EditorExportPreset> &p_preset, const String &p_package) const {488String pname = p_package;489String name = get_valid_basename(p_preset);490pname = pname.replace("$genname", name);491return pname;492}493494// Returns the project name without invalid characters495// or the "noname" string if all characters are invalid.496String EditorExportPlatformAndroid::get_valid_basename(const Ref<EditorExportPreset> &p_preset) const {497String basename = get_project_setting(p_preset, "application/config/name");498basename = basename.to_lower();499500String name;501bool first = true;502for (int i = 0; i < basename.length(); i++) {503char32_t c = basename[i];504if (is_digit(c) && first) {505continue;506}507if (is_ascii_identifier_char(c)) {508name += String::chr(c);509first = false;510}511}512513if (name.is_empty()) {514name = "noname";515}516517return name;518}519520String EditorExportPlatformAndroid::get_assets_directory(const Ref<EditorExportPreset> &p_preset, int p_export_format) const {521String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);522return gradle_build_directory.path_join(p_export_format == EXPORT_FORMAT_AAB ? AAB_ASSETS_DIRECTORY : APK_ASSETS_DIRECTORY);523}524525bool EditorExportPlatformAndroid::is_package_name_valid(const Ref<EditorExportPreset> &p_preset, const String &p_package, String *r_error) const {526String pname = get_package_name(p_preset, p_package);527528if (pname.length() == 0) {529if (r_error) {530*r_error = TTR("Package name is missing.");531}532return false;533}534535int segments = 0;536bool first = true;537for (int i = 0; i < pname.length(); i++) {538char32_t c = pname[i];539if (first && c == '.') {540if (r_error) {541*r_error = TTR("Package segments must be of non-zero length.");542}543return false;544}545if (c == '.') {546segments++;547first = true;548continue;549}550if (!is_ascii_identifier_char(c)) {551if (r_error) {552*r_error = vformat(TTR("The character '%s' is not allowed in Android application package names."), String::chr(c));553}554return false;555}556if (first && is_digit(c)) {557if (r_error) {558*r_error = TTR("A digit cannot be the first character in a package segment.");559}560return false;561}562if (first && is_underscore(c)) {563if (r_error) {564*r_error = vformat(TTR("The character '%s' cannot be the first character in a package segment."), String::chr(c));565}566return false;567}568first = false;569}570571if (segments == 0) {572if (r_error) {573*r_error = TTR("The package must have at least one '.' separator.");574}575return false;576}577578if (first) {579if (r_error) {580*r_error = TTR("Package segments must be of non-zero length.");581}582return false;583}584585return true;586}587588bool EditorExportPlatformAndroid::is_project_name_valid(const Ref<EditorExportPreset> &p_preset) const {589// Get the original project name and convert to lowercase.590String basename = get_project_setting(p_preset, "application/config/name");591basename = basename.to_lower();592// Check if there are invalid characters.593if (basename != get_valid_basename(p_preset)) {594return false;595}596return true;597}598599bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) {600/*601* By not compressing files with little or no benefit in doing so,602* a performance gain is expected at runtime. Moreover, if the APK is603* zip-aligned, assets stored as they are can be efficiently read by604* Android by memory-mapping them.605*/606607// -- Unconditional uncompress to mimic AAPT plus some other608609static const char *unconditional_compress_ext[] = {610// From https://github.com/android/platform_frameworks_base/blob/master/tools/aapt/Package.cpp611// These formats are already compressed, or don't compress well:612".jpg", ".jpeg", ".png", ".gif",613".wav", ".mp2", ".mp3", ".ogg", ".aac",614".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",615".rtttl", ".imy", ".xmf", ".mp4", ".m4a",616".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",617".amr", ".awb", ".wma", ".wmv",618// Godot-specific:619".webp", // Same reasoning as .png620".cfb", // Don't let small config files slow-down startup621".scn", // Binary scenes are usually already compressed622".ctex", // Streamable textures are usually already compressed623".pck", // Pack.624// Trailer for easier processing625nullptr626};627628for (const char **ext = unconditional_compress_ext; *ext; ++ext) {629if (p_path.to_lower().ends_with(String(*ext))) {630return false;631}632}633634// -- Compressed resource?635636if (p_data.size() >= 4 && p_data[0] == 'R' && p_data[1] == 'S' && p_data[2] == 'C' && p_data[3] == 'C') {637// Already compressed638return false;639}640641// --- TODO: Decide on texture resources according to their image compression setting642643return true;644}645646zip_fileinfo EditorExportPlatformAndroid::get_zip_fileinfo() {647OS::DateTime dt = OS::get_singleton()->get_datetime();648649zip_fileinfo zipfi;650zipfi.tmz_date.tm_year = dt.year;651zipfi.tmz_date.tm_mon = dt.month - 1; // tm_mon is zero indexed652zipfi.tmz_date.tm_mday = dt.day;653zipfi.tmz_date.tm_hour = dt.hour;654zipfi.tmz_date.tm_min = dt.minute;655zipfi.tmz_date.tm_sec = dt.second;656zipfi.dosDate = 0;657zipfi.external_fa = 0;658zipfi.internal_fa = 0;659660return zipfi;661}662663Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_abis() {664// Should have the same order and size as get_archs.665Vector<ABI> abis;666abis.push_back(ABI("armeabi-v7a", "arm32"));667abis.push_back(ABI("arm64-v8a", "arm64"));668abis.push_back(ABI("x86", "x86_32"));669abis.push_back(ABI("x86_64", "x86_64"));670return abis;671}672673#ifndef DISABLE_DEPRECATED674/// List the gdap files in the directory specified by the p_path parameter.675Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) {676Vector<String> dir_files;677Ref<DirAccess> da = DirAccess::open(p_path);678if (da.is_valid()) {679da->list_dir_begin();680while (true) {681String file = da->get_next();682if (file.is_empty()) {683break;684}685686if (da->current_is_dir() || da->current_is_hidden()) {687continue;688}689690if (file.ends_with(PluginConfigAndroid::PLUGIN_CONFIG_EXT)) {691dir_files.push_back(file);692}693}694da->list_dir_end();695}696697return dir_files;698}699700Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_plugins() {701Vector<PluginConfigAndroid> loaded_plugins;702703String plugins_dir = ProjectSettings::get_singleton()->get_resource_path().path_join("android/plugins");704705// Add the prebuilt plugins706loaded_plugins.append_array(PluginConfigAndroid::get_prebuilt_plugins(plugins_dir));707708if (DirAccess::exists(plugins_dir)) {709Vector<String> plugins_filenames = list_gdap_files(plugins_dir);710711if (!plugins_filenames.is_empty()) {712Ref<ConfigFile> config_file;713config_file.instantiate();714for (int i = 0; i < plugins_filenames.size(); i++) {715PluginConfigAndroid config = PluginConfigAndroid::load_plugin_config(config_file, plugins_dir.path_join(plugins_filenames[i]));716if (config.valid_config) {717loaded_plugins.push_back(config);718} else {719print_error("Invalid plugin config file " + plugins_filenames[i]);720}721}722}723}724725return loaded_plugins;726}727728Vector<PluginConfigAndroid> EditorExportPlatformAndroid::get_enabled_plugins(const Ref<EditorExportPreset> &p_presets) {729Vector<PluginConfigAndroid> enabled_plugins;730Vector<PluginConfigAndroid> all_plugins = get_plugins();731for (int i = 0; i < all_plugins.size(); i++) {732PluginConfigAndroid plugin = all_plugins[i];733bool enabled = p_presets->get("plugins/" + plugin.name);734if (enabled) {735enabled_plugins.push_back(plugin);736}737}738739return enabled_plugins;740}741#endif // DISABLE_DEPRECATED742743Error EditorExportPlatformAndroid::store_in_apk(APKExportData *ed, const String &p_path, const Vector<uint8_t> &p_data, int compression_method) {744zip_fileinfo zipfi = get_zip_fileinfo();745zipOpenNewFileInZip(ed->apk,746p_path.utf8().get_data(),747&zipfi,748nullptr,7490,750nullptr,7510,752nullptr,753compression_method,754Z_DEFAULT_COMPRESSION);755756zipWriteInFileInZip(ed->apk, p_data.ptr(), p_data.size());757zipCloseFileInZip(ed->apk);758759return OK;760}761762Error EditorExportPlatformAndroid::save_apk_so(const Ref<EditorExportPreset> &p_preset, void *p_userdata, const SharedObject &p_so) {763if (!p_so.path.get_file().begins_with("lib")) {764String err = "Android .so file names must start with \"lib\", but got: " + p_so.path;765ERR_PRINT(err);766return FAILED;767}768APKExportData *ed = static_cast<APKExportData *>(p_userdata);769Vector<ABI> abis = get_abis();770bool exported = false;771for (int i = 0; i < p_so.tags.size(); ++i) {772// shared objects can be fat (compatible with multiple ABIs)773int abi_index = -1;774for (int j = 0; j < abis.size(); ++j) {775if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {776abi_index = j;777break;778}779}780if (abi_index != -1) {781exported = true;782String abi = abis[abi_index].abi;783String dst_path = String("lib").path_join(abi).path_join(p_so.path.get_file());784Vector<uint8_t> array = FileAccess::get_file_as_bytes(p_so.path);785Error store_err = store_in_apk(ed, dst_path, array, Z_NO_COMPRESSION);786ERR_FAIL_COND_V_MSG(store_err, store_err, "Cannot store in apk file '" + dst_path + "'.");787}788}789if (!exported) {790ERR_PRINT("Cannot determine architecture for library \"" + p_so.path + "\". One of the supported architectures must be used as a tag: " + join_abis(abis, " ", true));791return FAILED;792}793return OK;794}795796Error EditorExportPlatformAndroid::save_apk_file(const Ref<EditorExportPreset> &p_preset, void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed, bool p_delta) {797APKExportData *ed = static_cast<APKExportData *>(p_userdata);798799const String simplified_path = simplify_path(p_path);800801Vector<uint8_t> enc_data;802EditorExportPlatform::SavedData sd;803Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, p_delta, enc_data, sd);804if (err != OK) {805return err;806}807808String dst_path;809if (ed->pd.salt.length() == 32) {810dst_path = String("assets/") + (simplified_path + ed->pd.salt).sha256_text();811} else {812dst_path = String("assets/") + simplified_path.trim_prefix("res://");813}814print_verbose("Saving project files from " + simplified_path + " into " + dst_path);815store_in_apk(ed, dst_path, enc_data, _should_compress_asset(simplified_path, enc_data) ? Z_DEFLATED : 0);816817ed->pd.file_ofs.push_back(sd);818819return OK;820}821822Error EditorExportPlatformAndroid::ignore_apk_file(const Ref<EditorExportPreset> &p_preset, void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed, bool p_delta) {823return OK;824}825826Error EditorExportPlatformAndroid::copy_gradle_so(const Ref<EditorExportPreset> &p_preset, void *p_userdata, const SharedObject &p_so) {827ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED,828"Android .so file names must start with \"lib\", but got: " + p_so.path);829Vector<ABI> abis = get_abis();830CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);831bool exported = false;832for (int i = 0; i < p_so.tags.size(); ++i) {833int abi_index = -1;834for (int j = 0; j < abis.size(); ++j) {835if (abis[j].abi == p_so.tags[i] || abis[j].arch == p_so.tags[i]) {836abi_index = j;837break;838}839}840if (abi_index != -1) {841exported = true;842String type = export_data->debug ? "debug" : "release";843String abi = abis[abi_index].abi;844String filename = p_so.path.get_file();845String dst_path = export_data->libs_directory.path_join(type).path_join(abi).path_join(filename);846Vector<uint8_t> data = FileAccess::get_file_as_bytes(p_so.path);847print_verbose("Copying .so file from " + p_so.path + " to " + dst_path);848Error err = store_file_at_path(dst_path, data);849ERR_FAIL_COND_V_MSG(err, err, "Failed to copy .so file from " + p_so.path + " to " + dst_path);850export_data->libs.push_back(dst_path);851}852}853ERR_FAIL_COND_V_MSG(!exported, FAILED,854"Cannot determine architecture for library \"" + p_so.path + "\". One of the supported architectures must be used as a tag:" + join_abis(abis, " ", true));855return OK;856}857858bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) {859return p_permissions.has("android.permission.READ_EXTERNAL_STORAGE") || p_permissions.has("android.permission.WRITE_EXTERNAL_STORAGE");860}861862bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) {863return p_permissions.has("android.permission.MANAGE_EXTERNAL_STORAGE");864}865866bool EditorExportPlatformAndroid::_uses_vulkan(const Ref<EditorExportPreset> &p_preset) const {867String rendering_method = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");868String rendering_driver = get_project_setting(p_preset, "rendering/rendering_device/driver.android");869return (rendering_method == "forward_plus" || rendering_method == "mobile") && rendering_driver == "vulkan";870}871872void EditorExportPlatformAndroid::_notification(int p_what) {873#ifndef ANDROID_ENABLED874switch (p_what) {875case NOTIFICATION_POSTINITIALIZE: {876if (EditorExport::get_singleton()) {877EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status));878}879} break;880881case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {882if (EditorSettings::get_singleton()->check_changed_settings_in_group("export/android")) {883_create_editor_debug_keystore_if_needed();884}885} break;886}887#endif888}889890void EditorExportPlatformAndroid::_create_editor_debug_keystore_if_needed() {891// Check if we have a valid keytool path.892String keytool_path = get_keytool_path();893if (!FileAccess::exists(keytool_path)) {894return;895}896897// Check if the current editor debug keystore exists.898String editor_debug_keystore = EDITOR_GET("export/android/debug_keystore");899if (FileAccess::exists(editor_debug_keystore)) {900return;901}902903// Generate the debug keystore.904String keystore_path = EditorPaths::get_singleton()->get_debug_keystore_path();905String keystores_dir = keystore_path.get_base_dir();906if (!DirAccess::exists(keystores_dir)) {907Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);908Error err = dir_access->make_dir_recursive(keystores_dir);909if (err != OK) {910WARN_PRINT(TTR("Error creating keystores directory:") + "\n" + keystores_dir);911return;912}913}914915if (!FileAccess::exists(keystore_path)) {916String output;917List<String> args;918args.push_back("-genkey");919args.push_back("-keystore");920args.push_back(keystore_path);921args.push_back("-storepass");922args.push_back("android");923args.push_back("-alias");924args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);925args.push_back("-keypass");926args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);927args.push_back("-keyalg");928args.push_back("RSA");929args.push_back("-keysize");930args.push_back("2048");931args.push_back("-validity");932args.push_back("10000");933args.push_back("-dname");934args.push_back("cn=Godot, ou=Godot Engine, o=Stichting Godot, c=NL");935Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);936print_verbose(output);937if (error != OK) {938WARN_PRINT("Error: Unable to create debug keystore");939return;940}941}942943// Update the editor settings.944EditorSettings::get_singleton()->set("export/android/debug_keystore", keystore_path);945EditorSettings::get_singleton()->set("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);946EditorSettings::get_singleton()->set("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);947print_verbose("Updated editor debug keystore to " + keystore_path);948}949950void EditorExportPlatformAndroid::_get_manifest_info(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions, Vector<FeatureInfo> &r_features, Vector<MetadataInfo> &r_metadata) {951const char **aperms = ANDROID_PERMS;952while (*aperms) {953bool enabled = p_preset->get("permissions/" + String(*aperms).to_lower());954if (enabled) {955r_permissions.push_back("android.permission." + String(*aperms));956}957aperms++;958}959PackedStringArray user_perms = p_preset->get("permissions/custom_permissions");960for (int i = 0; i < user_perms.size(); i++) {961String user_perm = user_perms[i].strip_edges();962if (!user_perm.is_empty()) {963r_permissions.push_back(user_perm);964}965}966if (p_give_internet) {967if (!r_permissions.has("android.permission.INTERNET")) {968r_permissions.push_back("android.permission.INTERNET");969}970}971972if (_uses_vulkan(p_preset)) {973// Require vulkan hardware level 1 support974FeatureInfo vulkan_level = {975"android.hardware.vulkan.level", // name976false, // required977"1" // version978};979r_features.append(vulkan_level);980981// Require vulkan version 1.0982FeatureInfo vulkan_version = {983"android.hardware.vulkan.version", // name984true, // required985"0x400003" // version - Encoded value for api version 1.0986};987r_features.append(vulkan_version);988}989990MetadataInfo rendering_method_metadata = {991"org.godotengine.rendering.method",992p_preset->get_project_setting("rendering/renderer/rendering_method.mobile")993};994r_metadata.append(rendering_method_metadata);995996MetadataInfo editor_version_metadata = {997"org.godotengine.editor.version",998String(GODOT_VERSION_FULL_CONFIG)999};1000r_metadata.append(editor_version_metadata);1001}10021003void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) {1004print_verbose("Building temporary manifest...");1005String manifest_text =1006"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"1007"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"1008" xmlns:tools=\"http://schemas.android.com/tools\">\n";10091010manifest_text += _get_screen_sizes_tag(p_preset);1011manifest_text += _get_gles_tag();10121013Vector<String> perms;1014Vector<FeatureInfo> features;1015Vector<MetadataInfo> manifest_metadata;1016_get_manifest_info(p_preset, p_give_internet, perms, features, manifest_metadata);1017for (int i = 0; i < perms.size(); i++) {1018String permission = perms.get(i);1019if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {1020manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);1021} else {1022manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission);1023}1024}10251026for (int i = 0; i < features.size(); i++) {1027manifest_text += vformat(" <uses-feature tools:node=\"replace\" android:name=\"%s\" android:required=\"%s\" android:version=\"%s\" />\n", features[i].name, features[i].required, features[i].version);1028}10291030Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();1031for (int i = 0; i < export_plugins.size(); i++) {1032if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {1033const String contents = export_plugins[i]->get_android_manifest_element_contents(Ref<EditorExportPlatform>(this), p_debug);1034if (!contents.is_empty()) {1035const String export_plugin_name = export_plugins[i]->get_name();1036manifest_text += "<!-- Start of manifest element contents from " + export_plugin_name + " -->\n";1037manifest_text += contents;1038manifest_text += "\n";1039manifest_text += "<!-- End of manifest element contents from " + export_plugin_name + " -->\n";1040}1041}1042}10431044manifest_text += _get_application_tag(Ref<EditorExportPlatform>(this), p_preset, _has_read_write_storage_permission(perms), p_debug, manifest_metadata);1045manifest_text += "</manifest>\n";1046String manifest_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join(vformat("src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")));10471048print_verbose("Storing manifest into " + manifest_path + ": " + "\n" + manifest_text);1049store_string_at_path(manifest_path, manifest_text);1050}10511052bool EditorExportPlatformAndroid::_is_transparency_allowed(const Ref<EditorExportPreset> &p_preset) const {1053return (bool)get_project_setting(p_preset, "display/window/per_pixel_transparency/allowed");1054}10551056void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset> &p_preset) {1057const String themes_xml_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join("res/values/themes.xml");10581059if (!FileAccess::exists(themes_xml_path)) {1060print_error("res/values/themes.xml does not exist.");1061return;1062}10631064bool transparency_allowed = _is_transparency_allowed(p_preset);10651066// Default/Reserved theme attributes.1067Dictionary main_theme_attributes;1068main_theme_attributes["android:windowSwipeToDismiss"] = bool_to_string(p_preset->get("gesture/swipe_to_dismiss"));1069main_theme_attributes["android:windowIsTranslucent"] = bool_to_string(transparency_allowed);1070if (transparency_allowed) {1071main_theme_attributes["android:windowBackground"] = "@android:color/transparent";1072} else {1073main_theme_attributes["android:windowBackground"] = "#" + p_preset->get("screen/background_color").operator Color().to_html(false);1074}10751076Dictionary splash_theme_attributes;1077splash_theme_attributes["android:windowSplashScreenBackground"] = "@mipmap/icon_background";1078splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@mipmap/icon_foreground";1079splash_theme_attributes["postSplashScreenTheme"] = "@style/GodotAppMainTheme";1080splash_theme_attributes["android:windowIsTranslucent"] = bool_to_string(transparency_allowed);10811082PackedStringArray reserved_splash_keys;1083reserved_splash_keys.append("postSplashScreenTheme");1084reserved_splash_keys.append("android:windowIsTranslucent");10851086Dictionary custom_theme_attributes = p_preset->get("gradle_build/custom_theme_attributes");10871088// Does not override default/reserved theme attributes; skips any duplicates from custom_theme_attributes.1089for (const Variant &k : custom_theme_attributes.keys()) {1090String key = k;1091String value = custom_theme_attributes[k];1092if (key.begins_with("[splash]")) {1093String splash_key = key.trim_prefix("[splash]");1094if (reserved_splash_keys.has(splash_key)) {1095WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", splash_key));1096} else {1097splash_theme_attributes[splash_key] = value;1098}1099} else {1100if (main_theme_attributes.has(key)) {1101WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", key));1102} else {1103main_theme_attributes[key] = value;1104}1105}1106}11071108Ref<FileAccess> file = FileAccess::open(themes_xml_path, FileAccess::READ);1109PackedStringArray lines = file->get_as_text().split("\n");1110file->close();11111112PackedStringArray new_lines;1113bool inside_main_theme = false;1114bool inside_splash_theme = false;11151116for (int i = 0; i < lines.size(); i++) {1117String line = lines[i];11181119if (line.contains("<style name=\"GodotAppMainTheme\"")) {1120inside_main_theme = true;1121new_lines.append(line);1122continue;1123}1124if (line.contains("<style name=\"GodotAppSplashTheme\"")) {1125inside_splash_theme = true;1126new_lines.append(line);1127continue;1128}11291130// Inject GodotAppMainTheme attributes.1131if (inside_main_theme && line.contains("</style>")) {1132for (const Variant &attribute : main_theme_attributes.keys()) {1133String value = main_theme_attributes[attribute];1134String item_line = vformat(" <item name=\"%s\">%s</item>", attribute, value);1135new_lines.append(item_line);1136}1137new_lines.append(line); // Add </style> in the end.1138inside_main_theme = false;1139continue;1140}11411142// Inject GodotAppSplashTheme attributes.1143if (inside_splash_theme && line.contains("</style>")) {1144for (const Variant &attribute : splash_theme_attributes.keys()) {1145String value = splash_theme_attributes[attribute];1146String item_line = vformat(" <item name=\"%s\">%s</item>", attribute, value);1147new_lines.append(item_line);1148}1149new_lines.append(line); // Add </style> in the end.1150inside_splash_theme = false;1151continue;1152}11531154// Add all other lines unchanged.1155if (!inside_main_theme && !inside_splash_theme) {1156new_lines.append(line);1157}1158}11591160// Reconstruct the XML content from the modified lines.1161String xml_content = String("\n").join(new_lines);1162store_string_at_path(themes_xml_path, xml_content);1163print_verbose("Successfully modified " + themes_xml_path + ": " + "\n" + xml_content);1164}11651166void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet) {1167// Leaving the unused types commented because looking these constants up1168// again later would be annoying1169// const int CHUNK_AXML_FILE = 0x00080003;1170// const int CHUNK_RESOURCEIDS = 0x00080180;1171const int CHUNK_STRINGS = 0x001C0001;1172// const int CHUNK_XML_END_NAMESPACE = 0x00100101;1173const int CHUNK_XML_END_TAG = 0x00100103;1174// const int CHUNK_XML_START_NAMESPACE = 0x00100100;1175const int CHUNK_XML_START_TAG = 0x00100102;1176// const int CHUNK_XML_TEXT = 0x00100104;1177const int UTF8_FLAG = 0x00000100;11781179Vector<String> string_table;11801181uint32_t ofs = 8;11821183uint32_t string_count = 0;1184uint32_t string_flags = 0;1185uint32_t string_data_offset = 0;11861187uint32_t string_table_begins = 0;1188uint32_t string_table_ends = 0;1189Vector<uint8_t> stable_extra;11901191String version_name = p_preset->get_version("version/name");1192int version_code = p_preset->get("version/code");1193String package_name = p_preset->get("package/unique_name");11941195const int screen_orientation =1196_get_android_orientation_value(DisplayServer::ScreenOrientation(int(get_project_setting(p_preset, "display/window/handheld/orientation"))));11971198bool screen_support_small = p_preset->get("screen/support_small");1199bool screen_support_normal = p_preset->get("screen/support_normal");1200bool screen_support_large = p_preset->get("screen/support_large");1201bool screen_support_xlarge = p_preset->get("screen/support_xlarge");12021203bool backup_allowed = p_preset->get("user_data_backup/allow");1204int app_category = p_preset->get("package/app_category");1205bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall");1206bool exclude_from_recents = p_preset->get("package/exclude_from_recents");1207bool is_resizeable = bool(get_project_setting(p_preset, "display/window/size/resizable"));12081209Vector<String> perms;1210Vector<FeatureInfo> features;1211Vector<MetadataInfo> manifest_metadata;1212_get_manifest_info(p_preset, p_give_internet, perms, features, manifest_metadata);1213bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);12141215while (ofs < (uint32_t)p_manifest.size()) {1216uint32_t chunk = decode_uint32(&p_manifest[ofs]);1217uint32_t size = decode_uint32(&p_manifest[ofs + 4]);12181219switch (chunk) {1220case CHUNK_STRINGS: {1221int iofs = ofs + 8;12221223string_count = decode_uint32(&p_manifest[iofs]);1224string_flags = decode_uint32(&p_manifest[iofs + 8]);1225string_data_offset = decode_uint32(&p_manifest[iofs + 12]);12261227uint32_t st_offset = iofs + 20;1228string_table.resize(string_count);1229uint32_t string_end = 0;12301231string_table_begins = st_offset;12321233for (uint32_t i = 0; i < string_count; i++) {1234uint32_t string_at = decode_uint32(&p_manifest[st_offset + i * 4]);1235string_at += st_offset + string_count * 4;12361237ERR_FAIL_COND_MSG(string_flags & UTF8_FLAG, "Unimplemented, can't read UTF-8 string table.");12381239if (string_flags & UTF8_FLAG) {1240} else {1241uint32_t len = decode_uint16(&p_manifest[string_at]);1242Vector<char32_t> ucstring;1243ucstring.resize(len + 1);1244for (uint32_t j = 0; j < len; j++) {1245uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]);1246ucstring.write[j] = c;1247}1248string_end = MAX(string_at + 2 + 2 * len, string_end);1249ucstring.write[len] = 0;1250string_table.write[i] = ucstring.ptr();1251}1252}12531254for (uint32_t i = string_end; i < (ofs + size); i++) {1255stable_extra.push_back(p_manifest[i]);1256}12571258string_table_ends = ofs + size;12591260} break;1261case CHUNK_XML_START_TAG: {1262int iofs = ofs + 8;1263uint32_t name = decode_uint32(&p_manifest[iofs + 12]);12641265String tname = string_table[name];1266uint32_t attrcount = decode_uint32(&p_manifest[iofs + 20]);1267iofs += 28;12681269for (uint32_t i = 0; i < attrcount; i++) {1270uint32_t attr_nspace = decode_uint32(&p_manifest[iofs]);1271uint32_t attr_name = decode_uint32(&p_manifest[iofs + 4]);1272uint32_t attr_value = decode_uint32(&p_manifest[iofs + 8]);1273uint32_t attr_resid = decode_uint32(&p_manifest[iofs + 16]);12741275const String value = (attr_value != 0xFFFFFFFF) ? string_table[attr_value] : "Res #" + itos(attr_resid);1276String attrname = string_table[attr_name];1277const String nspace = (attr_nspace != 0xFFFFFFFF) ? string_table[attr_nspace] : "";12781279//replace project information1280if (tname == "manifest" && attrname == "package") {1281string_table.write[attr_value] = get_package_name(p_preset, package_name);1282}12831284if (tname == "manifest" && attrname == "versionCode") {1285encode_uint32(version_code, &p_manifest.write[iofs + 16]);1286}12871288if (tname == "manifest" && attrname == "versionName") {1289if (attr_value == 0xFFFFFFFF) {1290WARN_PRINT("Version name in a resource, should be plain text");1291} else {1292string_table.write[attr_value] = version_name;1293}1294}12951296if (tname == "application" && attrname == "requestLegacyExternalStorage") {1297encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);1298}12991300if (tname == "application" && attrname == "allowBackup") {1301encode_uint32(backup_allowed, &p_manifest.write[iofs + 16]);1302}13031304if (tname == "application" && attrname == "appCategory") {1305encode_uint32(_get_app_category_value(app_category), &p_manifest.write[iofs + 16]);1306}13071308if (tname == "application" && attrname == "isGame") {1309encode_uint32(app_category == APP_CATEGORY_GAME, &p_manifest.write[iofs + 16]);1310}13111312if (tname == "application" && attrname == "hasFragileUserData") {1313encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]);1314}13151316if (tname == "activity" && attrname == "screenOrientation") {1317encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]);1318}13191320if (tname == "activity" && attrname == "excludeFromRecents") {1321encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]);1322}13231324if (tname == "activity" && attrname == "resizeableActivity") {1325encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]);1326}13271328if (tname == "provider" && attrname == "authorities") {1329string_table.write[attr_value] = get_package_name(p_preset, package_name) + String(".fileprovider");1330}13311332if (tname == "supports-screens") {1333if (attrname == "smallScreens") {1334encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);13351336} else if (attrname == "normalScreens") {1337encode_uint32(screen_support_normal ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);13381339} else if (attrname == "largeScreens") {1340encode_uint32(screen_support_large ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);13411342} else if (attrname == "xlargeScreens") {1343encode_uint32(screen_support_xlarge ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);1344}1345}13461347iofs += 20;1348}13491350} break;1351case CHUNK_XML_END_TAG: {1352int iofs = ofs + 8;1353uint32_t name = decode_uint32(&p_manifest[iofs + 12]);1354String tname = string_table[name];13551356if (tname == "manifest" || tname == "application") {1357// save manifest ending so we can restore it1358Vector<uint8_t> manifest_end;1359uint32_t manifest_cur_size = p_manifest.size();13601361manifest_end.resize(p_manifest.size() - ofs);1362memcpy(manifest_end.ptrw(), &p_manifest[ofs], manifest_end.size());13631364int32_t attr_name_string = string_table.find("name");1365ERR_FAIL_COND_MSG(attr_name_string == -1, "Template does not have 'name' attribute.");13661367int32_t ns_android_string = string_table.find("http://schemas.android.com/apk/res/android");1368if (ns_android_string == -1) {1369string_table.push_back("http://schemas.android.com/apk/res/android");1370ns_android_string = string_table.size() - 1;1371}13721373if (tname == "manifest") {1374// Updating manifest features1375int32_t attr_uses_feature_string = string_table.find("uses-feature");1376if (attr_uses_feature_string == -1) {1377string_table.push_back("uses-feature");1378attr_uses_feature_string = string_table.size() - 1;1379}13801381int32_t attr_required_string = string_table.find("required");1382if (attr_required_string == -1) {1383string_table.push_back("required");1384attr_required_string = string_table.size() - 1;1385}13861387for (int i = 0; i < features.size(); i++) {1388const String &feature_name = features[i].name;1389bool feature_required = features[i].required;1390String feature_version = features[i].version;1391bool has_version_attribute = !feature_version.is_empty();13921393print_line("Adding feature " + feature_name);13941395int32_t feature_string = string_table.find(feature_name);1396if (feature_string == -1) {1397string_table.push_back(feature_name);1398feature_string = string_table.size() - 1;1399}14001401String required_value_string = feature_required ? "true" : "false";1402int32_t required_value = string_table.find(required_value_string);1403if (required_value == -1) {1404string_table.push_back(required_value_string);1405required_value = string_table.size() - 1;1406}14071408int32_t attr_version_string = -1;1409int32_t version_value = -1;1410int tag_size;1411int attr_count;1412if (has_version_attribute) {1413attr_version_string = string_table.find("version");1414if (attr_version_string == -1) {1415string_table.push_back("version");1416attr_version_string = string_table.size() - 1;1417}14181419version_value = string_table.find(feature_version);1420if (version_value == -1) {1421string_table.push_back(feature_version);1422version_value = string_table.size() - 1;1423}14241425tag_size = 96; // node and three attrs + end node1426attr_count = 3;1427} else {1428tag_size = 76; // node and two attrs + end node1429attr_count = 2;1430}1431manifest_cur_size += tag_size + 24;1432p_manifest.resize(manifest_cur_size);14331434// start tag1435encode_uint16(0x102, &p_manifest.write[ofs]); // type1436encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1437encode_uint32(tag_size, &p_manifest.write[ofs + 4]); // size1438encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1439encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1440encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1441encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name1442encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start1443encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size1444encode_uint16(attr_count, &p_manifest.write[ofs + 28]); // num_attrs1445encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index1446encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index1447encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index14481449// android:name attribute1450encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns1451encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'1452encode_uint32(feature_string, &p_manifest.write[ofs + 44]); // raw_value1453encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size1454p_manifest.write[ofs + 50] = 0; // typedvalue_always01455p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)1456encode_uint32(feature_string, &p_manifest.write[ofs + 52]); // typedvalue reference14571458// android:required attribute1459encode_uint32(ns_android_string, &p_manifest.write[ofs + 56]); // ns1460encode_uint32(attr_required_string, &p_manifest.write[ofs + 60]); // 'name'1461encode_uint32(required_value, &p_manifest.write[ofs + 64]); // raw_value1462encode_uint16(8, &p_manifest.write[ofs + 68]); // typedvalue_size1463p_manifest.write[ofs + 70] = 0; // typedvalue_always01464p_manifest.write[ofs + 71] = 0x03; // typedvalue_type (string)1465encode_uint32(required_value, &p_manifest.write[ofs + 72]); // typedvalue reference14661467ofs += 76;14681469if (has_version_attribute) {1470// android:version attribute1471encode_uint32(ns_android_string, &p_manifest.write[ofs]); // ns1472encode_uint32(attr_version_string, &p_manifest.write[ofs + 4]); // 'name'1473encode_uint32(version_value, &p_manifest.write[ofs + 8]); // raw_value1474encode_uint16(8, &p_manifest.write[ofs + 12]); // typedvalue_size1475p_manifest.write[ofs + 14] = 0; // typedvalue_always01476p_manifest.write[ofs + 15] = 0x03; // typedvalue_type (string)1477encode_uint32(version_value, &p_manifest.write[ofs + 16]); // typedvalue reference14781479ofs += 20;1480}14811482// end tag1483encode_uint16(0x103, &p_manifest.write[ofs]); // type1484encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1485encode_uint32(24, &p_manifest.write[ofs + 4]); // size1486encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1487encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1488encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1489encode_uint32(attr_uses_feature_string, &p_manifest.write[ofs + 20]); // name14901491ofs += 24;1492}14931494// Updating manifest permissions1495int32_t attr_uses_permission_string = string_table.find("uses-permission");1496if (attr_uses_permission_string == -1) {1497string_table.push_back("uses-permission");1498attr_uses_permission_string = string_table.size() - 1;1499}15001501for (int i = 0; i < perms.size(); ++i) {1502print_line("Adding permission " + perms[i]);15031504manifest_cur_size += 56 + 24; // node + end node1505p_manifest.resize(manifest_cur_size);15061507// Add permission to the string pool1508int32_t perm_string = string_table.find(perms[i]);1509if (perm_string == -1) {1510string_table.push_back(perms[i]);1511perm_string = string_table.size() - 1;1512}15131514// start tag1515encode_uint16(0x102, &p_manifest.write[ofs]); // type1516encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1517encode_uint32(56, &p_manifest.write[ofs + 4]); // size1518encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1519encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1520encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1521encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name1522encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start1523encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size1524encode_uint16(1, &p_manifest.write[ofs + 28]); // num_attrs1525encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index1526encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index1527encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index15281529// attribute1530encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns1531encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'1532encode_uint32(perm_string, &p_manifest.write[ofs + 44]); // raw_value1533encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size1534p_manifest.write[ofs + 50] = 0; // typedvalue_always01535p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)1536encode_uint32(perm_string, &p_manifest.write[ofs + 52]); // typedvalue reference15371538ofs += 56;15391540// end tag1541encode_uint16(0x103, &p_manifest.write[ofs]); // type1542encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1543encode_uint32(24, &p_manifest.write[ofs + 4]); // size1544encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1545encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1546encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1547encode_uint32(attr_uses_permission_string, &p_manifest.write[ofs + 20]); // name15481549ofs += 24;1550}1551}15521553if (tname == "application") {1554// Updating application meta-data1555int32_t attr_meta_data_string = string_table.find("meta-data");1556if (attr_meta_data_string == -1) {1557string_table.push_back("meta-data");1558attr_meta_data_string = string_table.size() - 1;1559}15601561int32_t attr_value_string = string_table.find("value");1562if (attr_value_string == -1) {1563string_table.push_back("value");1564attr_value_string = string_table.size() - 1;1565}15661567for (int i = 0; i < manifest_metadata.size(); i++) {1568String meta_data_name = manifest_metadata[i].name;1569String meta_data_value = manifest_metadata[i].value;15701571print_line("Adding application metadata " + meta_data_name);15721573int32_t meta_data_name_string = string_table.find(meta_data_name);1574if (meta_data_name_string == -1) {1575string_table.push_back(meta_data_name);1576meta_data_name_string = string_table.size() - 1;1577}15781579int32_t meta_data_value_string = string_table.find(meta_data_value);1580if (meta_data_value_string == -1) {1581string_table.push_back(meta_data_value);1582meta_data_value_string = string_table.size() - 1;1583}15841585int tag_size = 76; // node and two attrs + end node1586int attr_count = 2;1587manifest_cur_size += tag_size + 24;1588p_manifest.resize(manifest_cur_size);15891590// start tag1591encode_uint16(0x102, &p_manifest.write[ofs]); // type1592encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1593encode_uint32(tag_size, &p_manifest.write[ofs + 4]); // size1594encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1595encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1596encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1597encode_uint32(attr_meta_data_string, &p_manifest.write[ofs + 20]); // name1598encode_uint16(20, &p_manifest.write[ofs + 24]); // attr_start1599encode_uint16(20, &p_manifest.write[ofs + 26]); // attr_size1600encode_uint16(attr_count, &p_manifest.write[ofs + 28]); // num_attrs1601encode_uint16(0, &p_manifest.write[ofs + 30]); // id_index1602encode_uint16(0, &p_manifest.write[ofs + 32]); // class_index1603encode_uint16(0, &p_manifest.write[ofs + 34]); // style_index16041605// android:name attribute1606encode_uint32(ns_android_string, &p_manifest.write[ofs + 36]); // ns1607encode_uint32(attr_name_string, &p_manifest.write[ofs + 40]); // 'name'1608encode_uint32(meta_data_name_string, &p_manifest.write[ofs + 44]); // raw_value1609encode_uint16(8, &p_manifest.write[ofs + 48]); // typedvalue_size1610p_manifest.write[ofs + 50] = 0; // typedvalue_always01611p_manifest.write[ofs + 51] = 0x03; // typedvalue_type (string)1612encode_uint32(meta_data_name_string, &p_manifest.write[ofs + 52]); // typedvalue reference16131614// android:value attribute1615encode_uint32(ns_android_string, &p_manifest.write[ofs + 56]); // ns1616encode_uint32(attr_value_string, &p_manifest.write[ofs + 60]); // 'value'1617encode_uint32(meta_data_value_string, &p_manifest.write[ofs + 64]); // raw_value1618encode_uint16(8, &p_manifest.write[ofs + 68]); // typedvalue_size1619p_manifest.write[ofs + 70] = 0; // typedvalue_always01620p_manifest.write[ofs + 71] = 0x03; // typedvalue_type (string)1621encode_uint32(meta_data_value_string, &p_manifest.write[ofs + 72]); // typedvalue reference16221623ofs += 76;16241625// end tag1626encode_uint16(0x103, &p_manifest.write[ofs]); // type1627encode_uint16(16, &p_manifest.write[ofs + 2]); // headersize1628encode_uint32(24, &p_manifest.write[ofs + 4]); // size1629encode_uint32(0, &p_manifest.write[ofs + 8]); // lineno1630encode_uint32(-1, &p_manifest.write[ofs + 12]); // comment1631encode_uint32(-1, &p_manifest.write[ofs + 16]); // ns1632encode_uint32(attr_meta_data_string, &p_manifest.write[ofs + 20]); // name16331634ofs += 24;1635}1636}16371638// copy footer back in1639memcpy(&p_manifest.write[ofs], manifest_end.ptr(), manifest_end.size());1640}1641} break;1642}16431644ofs += size;1645}16461647// Create new android manifest binary.16481649Vector<uint8_t> ret;1650ret.resize(string_table_begins + string_table.size() * 4);16511652for (uint32_t i = 0; i < string_table_begins; i++) {1653ret.write[i] = p_manifest[i];1654}16551656ofs = 0;1657for (int i = 0; i < string_table.size(); i++) {1658encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);1659ofs += string_table[i].length() * 2 + 2 + 2;1660}16611662ret.resize(ret.size() + ofs);1663string_data_offset = ret.size() - ofs;1664uint8_t *chars = &ret.write[string_data_offset];1665for (int i = 0; i < string_table.size(); i++) {1666String s = string_table[i];1667encode_uint16(s.length(), chars);1668chars += 2;1669for (int j = 0; j < s.length(); j++) {1670encode_uint16(s[j], chars);1671chars += 2;1672}1673encode_uint16(0, chars);1674chars += 2;1675}16761677for (int i = 0; i < stable_extra.size(); i++) {1678ret.push_back(stable_extra[i]);1679}16801681//pad1682while (ret.size() % 4) {1683ret.push_back(0);1684}16851686uint32_t new_stable_end = ret.size();16871688uint32_t extra = (p_manifest.size() - string_table_ends);1689ret.resize(new_stable_end + extra);1690for (uint32_t i = 0; i < extra; i++) {1691ret.write[new_stable_end + i] = p_manifest[string_table_ends + i];1692}16931694while (ret.size() % 4) {1695ret.push_back(0);1696}1697encode_uint32(ret.size(), &ret.write[4]); //update new file size16981699encode_uint32(new_stable_end - 8, &ret.write[12]); //update new string table size1700encode_uint32(string_table.size(), &ret.write[16]); //update new number of strings1701encode_uint32(string_data_offset - 8, &ret.write[28]); //update new string data offset17021703p_manifest = ret;1704}17051706String EditorExportPlatformAndroid::_get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug) {1707String keystore_preference = p_debug ? "keystore/debug" : "keystore/release";1708String keystore_env_variable = p_debug ? ENV_ANDROID_KEYSTORE_DEBUG_PATH : ENV_ANDROID_KEYSTORE_RELEASE_PATH;1709String keystore_path = p_preset->get_or_env(keystore_preference, keystore_env_variable);17101711return ProjectSettings::get_singleton()->globalize_path(keystore_path).simplify_path();1712}17131714String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) {1715uint32_t offset = 0;1716uint32_t len = 0;17171718if (p_utf8) {1719uint8_t byte = p_bytes[offset];1720if (byte & 0x80) {1721offset += 2;1722} else {1723offset += 1;1724}1725byte = p_bytes[offset];1726offset++;1727if (byte & 0x80) {1728len = byte & 0x7F;1729len = (len << 8) + p_bytes[offset];1730offset++;1731} else {1732len = byte;1733}1734} else {1735len = decode_uint16(&p_bytes[offset]);1736offset += 2;1737if (len & 0x8000) {1738len &= 0x7FFF;1739len = (len << 16) + decode_uint16(&p_bytes[offset]);1740offset += 2;1741}1742}17431744if (p_utf8) {1745Vector<uint8_t> str8;1746str8.resize(len + 1);1747for (uint32_t i = 0; i < len; i++) {1748str8.write[i] = p_bytes[offset + i];1749}1750str8.write[len] = 0;1751return String::utf8((const char *)str8.ptr(), len);1752} else {1753String str;1754for (uint32_t i = 0; i < len; i++) {1755char32_t c = decode_uint16(&p_bytes[offset + i * 2]);1756if (c == 0) {1757break;1758}1759str += String::chr(c);1760}1761return str;1762}1763}17641765void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest) {1766const int UTF8_FLAG = 0x00000100;17671768uint32_t string_block_len = decode_uint32(&r_manifest[16]);1769uint32_t string_count = decode_uint32(&r_manifest[20]);1770uint32_t string_flags = decode_uint32(&r_manifest[28]);1771const uint32_t string_table_begins = 40;17721773Vector<String> string_table;17741775const String project_name = get_project_name(p_preset, p_preset->get("package/name"));1776const Dictionary appnames = get_project_setting(p_preset, "application/config/name_localized");1777const StringName domain_name = "godot.project_name_localization";1778Ref<TranslationDomain> domain = TranslationServer::get_singleton()->get_or_add_domain(domain_name);1779TranslationServer::get_singleton()->load_project_translations(domain);17801781for (uint32_t i = 0; i < string_count; i++) {1782uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]);1783offset += string_table_begins + string_count * 4;17841785String str = _parse_string(&r_manifest[offset], string_flags & UTF8_FLAG);17861787if (str == "godot-project-name") {1788str = project_name;1789} else if (str.begins_with("godot-project-name")) {1790String lang = str.substr(str.rfind_char('-') + 1).replace_char('-', '_');17911792if (appnames.is_empty()) {1793domain->set_locale_override(lang);1794str = domain->translate(project_name, String());1795} else {1796str = appnames.get(lang, project_name);1797}1798}17991800string_table.push_back(str);1801}18021803TranslationServer::get_singleton()->remove_domain(domain_name);18041805//write a new string table, but use 16 bits1806Vector<uint8_t> ret;1807ret.resize(string_table_begins + string_table.size() * 4);18081809for (uint32_t i = 0; i < string_table_begins; i++) {1810ret.write[i] = r_manifest[i];1811}18121813int ofs = 0;1814for (int i = 0; i < string_table.size(); i++) {1815encode_uint32(ofs, &ret.write[string_table_begins + i * 4]);1816ofs += string_table[i].length() * 2 + 2 + 2;1817}18181819ret.resize(ret.size() + ofs);1820uint8_t *chars = &ret.write[ret.size() - ofs];1821for (int i = 0; i < string_table.size(); i++) {1822String s = string_table[i];1823encode_uint16(s.length(), chars);1824chars += 2;1825for (int j = 0; j < s.length(); j++) {1826encode_uint16(s[j], chars);1827chars += 2;1828}1829encode_uint16(0, chars);1830chars += 2;1831}18321833//pad1834while (ret.size() % 4) {1835ret.push_back(0);1836}18371838//change flags to not use utf81839encode_uint32(string_flags & ~0x100, &ret.write[28]);1840//change length1841encode_uint32(ret.size() - 12, &ret.write[16]);1842//append the rest...1843int rest_from = 12 + string_block_len;1844int rest_to = ret.size();1845int rest_len = (r_manifest.size() - rest_from);1846ret.resize(ret.size() + (r_manifest.size() - rest_from));1847for (int i = 0; i < rest_len; i++) {1848ret.write[rest_to + i] = r_manifest[rest_from + i];1849}1850//finally update the size1851encode_uint32(ret.size(), &ret.write[4]);18521853r_manifest = ret;1854//printf("end\n");1855}18561857void EditorExportPlatformAndroid::_process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) {1858Ref<Image> working_image = p_source_image;18591860if (p_source_image->get_width() != dimension || p_source_image->get_height() != dimension) {1861working_image = p_source_image->duplicate();1862working_image->resize(dimension, dimension, Image::Interpolation::INTERPOLATE_LANCZOS);1863}18641865Vector<uint8_t> buffer = working_image->save_webp_to_buffer();1866p_data.resize(buffer.size());1867memcpy(p_data.ptrw(), buffer.ptr(), p_data.size());1868}18691870void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background, Ref<Image> &monochrome) {1871String project_icon_path = get_project_setting(p_preset, "application/config/icon");18721873Error err = OK;18741875// Regular icon: user selection -> project icon -> default.1876String path = static_cast<String>(p_preset->get(LAUNCHER_ICON_OPTION)).strip_edges();1877print_verbose("Loading regular icon from " + path);1878if (!path.is_empty()) {1879icon = _load_icon_or_splash_image(path, &err);1880}1881if (path.is_empty() || err != OK || icon.is_null() || icon->is_empty()) {1882print_verbose("- falling back to project icon: " + project_icon_path);1883if (!project_icon_path.is_empty()) {1884icon = _load_icon_or_splash_image(project_icon_path, &err);1885} else {1886ERR_PRINT("No project icon specified. Please specify one in the Project Settings under Application -> Config -> Icon");1887}1888}18891890// Adaptive foreground: user selection -> regular icon (user selection -> project icon -> default).1891path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION)).strip_edges();1892print_verbose("Loading adaptive foreground icon from " + path);1893if (!path.is_empty()) {1894foreground = _load_icon_or_splash_image(path, &err);1895}1896if (path.is_empty() || err != OK || foreground.is_null() || foreground->is_empty()) {1897print_verbose("- falling back to using the regular icon");1898foreground = icon;1899}19001901// Adaptive background: user selection -> default.1902path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION)).strip_edges();1903if (!path.is_empty()) {1904print_verbose("Loading adaptive background icon from " + path);1905background = _load_icon_or_splash_image(path, &err);1906}19071908// Adaptive monochrome: user selection -> default.1909path = static_cast<String>(p_preset->get(LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION)).strip_edges();1910if (!path.is_empty()) {1911print_verbose("Loading adaptive monochrome icon from " + path);1912monochrome = _load_icon_or_splash_image(path, &err);1913}1914}19151916void EditorExportPlatformAndroid::_copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset,1917const Ref<Image> &p_main_image,1918const Ref<Image> &p_foreground,1919const Ref<Image> &p_background,1920const Ref<Image> &p_monochrome) {1921String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);19221923String monochrome_tag = "";19241925// Prepare images to be resized for the icons. If some image ends up being uninitialized,1926// the default image from the export template will be used.19271928for (int i = 0; i < ICON_DENSITIES_COUNT; ++i) {1929if (p_main_image.is_valid() && !p_main_image->is_empty()) {1930print_verbose("Processing launcher icon for dimension " + itos(LAUNCHER_ICONS[i].dimensions) + " into " + LAUNCHER_ICONS[i].export_path);1931Vector<uint8_t> data;1932_process_launcher_icons(LAUNCHER_ICONS[i].export_path, p_main_image, LAUNCHER_ICONS[i].dimensions, data);1933store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ICONS[i].export_path), data);1934}19351936if (p_foreground.is_valid() && !p_foreground->is_empty()) {1937print_verbose("Processing launcher adaptive icon p_foreground for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path);1938Vector<uint8_t> data;1939_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path, p_foreground,1940LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data);1941store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path), data);1942}19431944if (p_background.is_valid() && !p_background->is_empty()) {1945print_verbose("Processing launcher adaptive icon p_background for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path);1946Vector<uint8_t> data;1947_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path, p_background,1948LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data);1949store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path), data);1950}19511952if (p_monochrome.is_valid() && !p_monochrome->is_empty()) {1953print_verbose("Processing launcher adaptive icon p_monochrome for dimension " + itos(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions) + " into " + LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path);1954Vector<uint8_t> data;1955_process_launcher_icons(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path, p_monochrome,1956LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data);1957store_file_at_path(gradle_build_dir.path_join(LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path), data);1958monochrome_tag = " <monochrome android:drawable=\"@mipmap/icon_monochrome\"/>\n";1959}1960}19611962// Finalize the icon.xml by formatting the template with the optional monochrome tag.1963store_string_at_path(gradle_build_dir.path_join(ICON_XML_PATH), vformat(ICON_XML_TEMPLATE, monochrome_tag));1964}19651966Vector<EditorExportPlatformAndroid::ABI> EditorExportPlatformAndroid::get_enabled_abis(const Ref<EditorExportPreset> &p_preset) {1967Vector<ABI> abis = get_abis();1968Vector<ABI> enabled_abis;1969for (int i = 0; i < abis.size(); ++i) {1970bool is_enabled = p_preset->get("architectures/" + abis[i].abi);1971if (is_enabled) {1972enabled_abis.push_back(abis[i]);1973}1974}1975return enabled_abis;1976}19771978void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {1979r_features->push_back("etc2");1980r_features->push_back("astc");19811982if (!p_preset->is_dedicated_server() && p_preset->get("shader_baker/enabled")) {1983// Don't use the shader baker if exporting as a dedicated server, as no rendering is performed.1984r_features->push_back("shader_baker");1985}19861987Vector<ABI> abis = get_enabled_abis(p_preset);1988for (int i = 0; i < abis.size(); ++i) {1989r_features->push_back(abis[i].arch);1990}1991}19921993String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const {1994if (p_preset) {1995if (p_name == ("apk_expansion/public_key")) {1996bool apk_expansion = p_preset->get("apk_expansion/enable");1997String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");1998if (apk_expansion && apk_expansion_pkey.is_empty()) {1999return TTR("Invalid public key for APK expansion.");2000}2001} else if (p_name == "package/unique_name") {2002String pn = p_preset->get("package/unique_name");2003String pn_err;20042005if (!is_package_name_valid(Ref<EditorExportPreset>(p_preset), pn, &pn_err)) {2006return TTR("Invalid package name:") + " " + pn_err;2007}2008} else if (p_name == "gesture/swipe_to_dismiss") {2009bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2010if (bool(p_preset->get("gesture/swipe_to_dismiss")) && !gradle_build_enabled) {2011return TTR("\"Use Gradle Build\" is required to enable \"Swipe to dismiss\".");2012}2013} else if (p_name == "gradle_build/use_gradle_build") {2014bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2015String enabled_deprecated_plugins_names = _get_deprecated_plugins_names(Ref<EditorExportPreset>(p_preset));2016if (!enabled_deprecated_plugins_names.is_empty() && !gradle_build_enabled) {2017return TTR("\"Use Gradle Build\" must be enabled to use the plugins.");2018}2019#ifdef ANDROID_ENABLED2020if (gradle_build_enabled) {2021return TTR("Support for \"Use Gradle Build\" on Android is currently experimental.");2022}2023#endif // ANDROID_ENABLED2024} else if (p_name == "gradle_build/compress_native_libraries") {2025bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2026if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) {2027return TTR("\"Compress Native Libraries\" is only valid when \"Use Gradle Build\" is enabled.");2028}2029} else if (p_name == "gradle_build/export_format") {2030bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2031if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) {2032return TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled.");2033}2034} else if (p_name == "gradle_build/min_sdk") {2035String min_sdk_str = p_preset->get("gradle_build/min_sdk");2036bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2037if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do.2038if (!gradle_build_enabled) {2039return TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");2040}2041if (!min_sdk_str.is_valid_int()) {2042return vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str);2043} else {2044int min_sdk_int = min_sdk_str.to_int();2045if (min_sdk_int < DEFAULT_MIN_SDK_VERSION) {2046return vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), DEFAULT_MIN_SDK_VERSION);2047}2048}2049}2050} else if (p_name == "gradle_build/target_sdk") {2051String target_sdk_str = p_preset->get("gradle_build/target_sdk");2052int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;20532054String min_sdk_str = p_preset->get("gradle_build/min_sdk");2055int min_sdk_int = DEFAULT_MIN_SDK_VERSION;2056if (min_sdk_str.is_valid_int()) {2057min_sdk_int = min_sdk_str.to_int();2058}2059bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2060if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.2061if (!gradle_build_enabled) {2062return TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled.");2063}2064if (!target_sdk_str.is_valid_int()) {2065return vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str);2066} else {2067target_sdk_int = target_sdk_str.to_int();2068if (target_sdk_int < min_sdk_int) {2069return TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version.");2070}2071}2072}2073} else if (p_name == "gradle_build/custom_theme_attributes") {2074bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2075if (bool(p_preset->get("gradle_build/custom_theme_attributes")) && !gradle_build_enabled) {2076return TTR("\"Use Gradle Build\" is required to add custom theme attributes.");2077}2078} else if (p_name == "package/show_in_android_tv") {2079bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2080if (bool(p_preset->get("package/show_in_android_tv")) && !gradle_build_enabled) {2081return TTR("\"Use Gradle Build\" must be enabled to enable \"Show In Android Tv\".");2082}2083} else if (p_name == "package/show_as_launcher_app") {2084bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2085if (bool(p_preset->get("package/show_as_launcher_app")) && !gradle_build_enabled) {2086return TTR("\"Use Gradle Build\" must be enabled to enable \"Show As Launcher App\".");2087}2088} else if (p_name == "package/show_in_app_library") {2089bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");2090if (!bool(p_preset->get("package/show_in_app_library")) && !gradle_build_enabled) {2091return TTR("\"Use Gradle Build\" must be enabled to disable \"Show In App Library\".");2092}2093} else if (p_name == "shader_baker/enabled" && bool(p_preset->get("shader_baker/enabled"))) {2094String export_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile");2095if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") {2096return TTR("\"Shader Baker\" is not supported when using the Compatibility renderer.");2097} else if (OS::get_singleton()->get_current_rendering_method() != export_renderer) {2098return vformat(TTR("The editor is currently using a different renderer than what the target platform will use. \"Shader Baker\" won't be able to include core shaders. Switch to the \"%s\" renderer temporarily to fix this."), export_renderer);2099}2100}2101}2102return String();2103}21042105void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) const {2106r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));2107r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));21082109r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, false));2110r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false));2111r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));2112r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/compress_native_libraries"), false, false, true));2113r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true));2114// Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465).2115// This implies doing validation that the string is a proper int.2116r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), "", false, true));2117r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true));21182119r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "gradle_build/custom_theme_attributes", PROPERTY_HINT_DICTIONARY_TYPE, "String;String"), Dictionary()));21202121#ifndef DISABLE_DEPRECATED2122Vector<PluginConfigAndroid> plugins_configs = get_plugins();2123for (int i = 0; i < plugins_configs.size(); i++) {2124print_verbose("Found Android plugin " + plugins_configs[i].name);2125r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false));2126}2127android_plugins_changed.clear();2128#endif // DISABLE_DEPRECATED21292130// Android supports multiple architectures in an app bundle, so2131// we expose each option as a checkbox in the export dialog.2132const Vector<ABI> abis = get_abis();2133for (int i = 0; i < abis.size(); ++i) {2134const String abi = abis[i].abi;2135// All Android devices supporting Vulkan run 64-bit Android,2136// so there is usually no point in exporting for 32-bit Android.2137const bool is_default = abi == "arm64-v8a";2138r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default));2139}21402141r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));2142r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));2143r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));2144r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));2145r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));2146r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), ""));21472148r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1));2149r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));21502151r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "com.example.$genname", false, true));2152r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), ""));2153r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true));2154r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video,Undefined"), APP_CATEGORY_GAME));2155r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/retain_data_on_uninstall"), false));2156r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/exclude_from_recents"), false));2157r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_android_tv"), false));2158r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_in_app_library"), true));2159r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/show_as_launcher_app"), false));21602161r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ICON_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));2162r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_FOREGROUND_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));2163r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_BACKGROUND_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));2164r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, LAUNCHER_ADAPTIVE_ICON_MONOCHROME_OPTION, PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));21652166r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false));21672168r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "shader_baker/enabled"), false));21692170#ifndef XR_DISABLED2171r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true));2172#endif // XR_DISABLED21732174r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gesture/swipe_to_dismiss"), false));21752176r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true));2177r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/edge_to_edge"), false));2178r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true));2179r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true));2180r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true));2181r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));2182r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "screen/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));21832184r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));21852186r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args", PROPERTY_HINT_NONE, "monospace"), ""));21872188r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true));2189r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), ""));2190r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT, "monospace,no_wrap"), "", false, true));21912192r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray()));21932194const char **perms = ANDROID_PERMS;2195while (*perms) {2196r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false));2197perms++;2198}2199}22002201bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {2202if (p_preset == nullptr) {2203return true;2204}22052206bool advanced_options_enabled = p_preset->are_advanced_options_enabled();2207if (p_option == "graphics/opengl_debug" ||2208p_option == "gradle_build/custom_theme_attributes" ||2209p_option == "command_line/extra_args" ||2210p_option == "permissions/custom_permissions" ||2211p_option == "keystore/debug" ||2212p_option == "keystore/debug_user" ||2213p_option == "keystore/debug_password" ||2214p_option == "package/retain_data_on_uninstall" ||2215p_option == "package/exclude_from_recents" ||2216p_option == "package/show_in_app_library" ||2217p_option == "package/show_as_launcher_app" ||2218p_option == "gesture/swipe_to_dismiss" ||2219p_option == "apk_expansion/enable" ||2220p_option == "apk_expansion/SALT" ||2221p_option == "apk_expansion/public_key") {2222return advanced_options_enabled;2223}2224if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") {2225return advanced_options_enabled && bool(p_preset->get("gradle_build/use_gradle_build"));2226}2227if (p_option == "custom_template/debug" || p_option == "custom_template/release") {2228// The APK templates are ignored if Gradle build is enabled.2229return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build"));2230}22312232// Hide .NET embedding option (always enabled).2233if (p_option == "dotnet/embed_build_outputs") {2234return false;2235}22362237if (p_option == "dotnet/android_use_linux_bionic") {2238return advanced_options_enabled;2239}2240return true;2241}22422243String EditorExportPlatformAndroid::get_name() const {2244return "Android";2245}22462247String EditorExportPlatformAndroid::get_os_name() const {2248return "Android";2249}22502251Ref<Texture2D> EditorExportPlatformAndroid::get_logo() const {2252return logo;2253}22542255bool EditorExportPlatformAndroid::should_update_export_options() {2256#ifndef DISABLE_DEPRECATED2257if (android_plugins_changed.is_set()) {2258// don't clear unless we're reporting true, to avoid race2259android_plugins_changed.clear();2260return true;2261}2262#endif // DISABLE_DEPRECATED2263return false;2264}22652266#ifndef ANDROID_ENABLED2267bool EditorExportPlatformAndroid::poll_export() {2268bool dc = devices_changed.is_set();2269if (dc) {2270// don't clear unless we're reporting true, to avoid race2271devices_changed.clear();2272}2273return dc;2274}22752276int EditorExportPlatformAndroid::get_options_count() const {2277MutexLock lock(device_lock);2278return devices.size() + 1;2279}22802281Ref<Texture2D> EditorExportPlatformAndroid::get_option_icon(int p_index) const {2282if (p_index == 0) {2283Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();2284ERR_FAIL_COND_V(theme.is_null(), Ref<ImageTexture>());2285return theme->get_icon(use_scrcpy ? SNAME("GuiChecked") : SNAME("GuiUnchecked"), EditorStringName(EditorIcons));2286}2287return EditorExportPlatform::get_option_icon(p_index - 1);2288}22892290String EditorExportPlatformAndroid::get_options_tooltip() const {2291return TTR("Select device from the list");2292}22932294String EditorExportPlatformAndroid::get_option_label(int p_index) const {2295ERR_FAIL_INDEX_V(p_index, devices.size() + 1, "");2296if (p_index == 0) {2297return TTR("Mirror Android devices");2298}2299MutexLock lock(device_lock);2300return devices[p_index - 1].name;2301}23022303String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const {2304ERR_FAIL_INDEX_V(p_index, devices.size() + 1, "");2305if (p_index == 0) {2306return TTR("If enabled, \"scrcpy\" is used to start the project and automatically stream device display (or virtual display) content.");2307}2308MutexLock lock(device_lock);2309String s = devices[p_index - 1].description;2310if (devices.size() == 1) {2311// Tooltip will be:2312// Name2313// Description2314s = devices[p_index - 1].name + "\n\n" + s;2315}2316return s;2317}23182319String EditorExportPlatformAndroid::get_device_architecture(int p_index) const {2320ERR_FAIL_INDEX_V(p_index, devices.size() + 1, "");2321if (p_index == 0) {2322return String();2323}2324MutexLock lock(device_lock);2325return devices[p_index - 1].architecture;2326}23272328Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {2329ERR_FAIL_INDEX_V(p_device, devices.size() + 1, ERR_INVALID_PARAMETER);2330if (p_device == 0) {2331use_scrcpy = !use_scrcpy;2332EditorSettings::get_singleton()->set_project_metadata("android", "use_scrcpy", use_scrcpy);2333devices_changed.set();2334return ERR_SKIP;2335}23362337String can_export_error;2338bool can_export_missing_templates;2339if (!can_export(p_preset, can_export_error, can_export_missing_templates)) {2340add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error);2341return ERR_UNCONFIGURED;2342}23432344MutexLock lock(device_lock);23452346EditorProgress ep("run", vformat(TTR("Running on %s"), devices[p_device - 1].name), 3);23472348String adb = get_adb_path();23492350// Export_temp APK.2351if (ep.step(TTR("Exporting APK..."), 0)) {2352return ERR_SKIP;2353}23542355const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug");2356const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT);2357const bool use_reverse = !use_wifi_for_remote_debug;23582359if (use_reverse) {2360p_debug_flags.set_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST);2361}23622363String tmp_export_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");23642365#define CLEANUP_AND_RETURN(m_err) \2366{ \2367DirAccess::remove_file_or_error(tmp_export_path); \2368if (FileAccess::exists(tmp_export_path + ".idsig")) { \2369DirAccess::remove_file_or_error(tmp_export_path + ".idsig"); \2370} \2371return m_err; \2372} \2373((void)0)23742375// Export to temporary APK before sending to device.2376Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags);23772378if (err != OK) {2379CLEANUP_AND_RETURN(err);2380}23812382List<String> args;2383int rv;2384String output;23852386bool remove_prev = EDITOR_GET("export/android/one_click_deploy_clear_previous_install");2387String version_name = p_preset->get_version("version/name");2388String package_name = p_preset->get("package/unique_name");23892390if (remove_prev) {2391if (ep.step(TTR("Uninstalling..."), 1)) {2392CLEANUP_AND_RETURN(ERR_SKIP);2393}23942395print_line("Uninstalling previous version: " + devices[p_device - 1].name);23962397args.push_back("-s");2398args.push_back(devices[p_device - 1].id);2399args.push_back("uninstall");2400if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device - 1].api_level >= 17) {2401args.push_back("--user");2402args.push_back("0");2403}2404args.push_back(get_package_name(p_preset, package_name));24052406output.clear();2407err = OS::get_singleton()->execute(adb, args, &output, &rv, true);2408print_verbose(output);2409}24102411print_line("Installing to device (please wait...): " + devices[p_device - 1].name);2412if (ep.step(TTR("Installing to device, please wait..."), 2)) {2413CLEANUP_AND_RETURN(ERR_SKIP);2414}24152416args.clear();2417args.push_back("-s");2418args.push_back(devices[p_device - 1].id);2419args.push_back("install");2420if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device - 1].api_level >= 17) {2421args.push_back("--user");2422args.push_back("0");2423}2424args.push_back("-r");2425args.push_back(tmp_export_path);24262427output.clear();2428err = OS::get_singleton()->execute(adb, args, &output, &rv, true);2429print_verbose(output);2430if (err || rv != 0) {2431add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output));2432CLEANUP_AND_RETURN(ERR_CANT_CREATE);2433}24342435if (use_remote) {2436if (use_reverse) {2437static const char *const msg = "--- Debugging over USB ---";2438EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);2439print_line(String(msg).to_upper());24402441args.clear();2442args.push_back("-s");2443args.push_back(devices[p_device - 1].id);2444args.push_back("reverse");2445args.push_back("--remove-all");2446output.clear();2447OS::get_singleton()->execute(adb, args, &output, &rv, true);2448print_verbose(output);24492450if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) {2451int dbg_port = EDITOR_GET("network/debug/remote_port");2452args.clear();2453args.push_back("-s");2454args.push_back(devices[p_device - 1].id);2455args.push_back("reverse");2456args.push_back("tcp:" + itos(dbg_port));2457args.push_back("tcp:" + itos(dbg_port));24582459output.clear();2460OS::get_singleton()->execute(adb, args, &output, &rv, true);2461print_verbose(output);2462print_line("Reverse result: " + itos(rv));2463}24642465if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {2466int fs_port = EDITOR_GET("filesystem/file_server/port");24672468args.clear();2469args.push_back("-s");2470args.push_back(devices[p_device - 1].id);2471args.push_back("reverse");2472args.push_back("tcp:" + itos(fs_port));2473args.push_back("tcp:" + itos(fs_port));24742475output.clear();2476err = OS::get_singleton()->execute(adb, args, &output, &rv, true);2477print_verbose(output);2478print_line("Reverse result2: " + itos(rv));2479}2480} else {2481static const char *const api_version_msg = "--- Debugging over Wi-Fi ---";2482static const char *const manual_override_msg = "--- Wi-Fi remote debug enabled in project settings; debugging over Wi-Fi ---";24832484const char *const msg = use_wifi_for_remote_debug ? manual_override_msg : api_version_msg;2485EditorNode::get_singleton()->get_log()->add_message(msg, EditorLog::MSG_TYPE_EDITOR);2486print_line(String(msg).to_upper());2487}2488}24892490if (ep.step(TTR("Running on device..."), 3)) {2491CLEANUP_AND_RETURN(ERR_SKIP);2492}24932494String scrcpy = "scrcpy";2495if (!EDITOR_GET("export/android/scrcpy/path").operator String().is_empty()) {2496scrcpy = EDITOR_GET("export/android/scrcpy/path").operator String();2497}24982499args.clear();2500if (use_scrcpy) {2501args.push_back("-s");2502args.push_back(devices[p_device - 1].id);2503if (EDITOR_GET("export/android/scrcpy/virtual_display").operator bool()) {2504args.push_back("--new-display=" + EDITOR_GET("export/android/scrcpy/screen_size").operator String());2505if (EDITOR_GET("export/android/scrcpy/local_ime").operator bool()) {2506args.push_back("--display-ime-policy=local");2507}2508if (EDITOR_GET("export/android/scrcpy/no_decorations").operator bool()) {2509args.push_back("--no-vd-system-decorations");2510}2511}2512args.push_back("--start-app=+" + get_package_name(p_preset, package_name));25132514Dictionary data = OS::get_singleton()->execute_with_pipe(scrcpy, args, false);2515if (!data.has("pid") || data["pid"].operator int() <= 0) {2516add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not start scrcpy executable. Configure scrcpy path in the Editor Settings (Export > Android > scrcpy > Path)."));2517CLEANUP_AND_RETURN(ERR_CANT_CREATE);2518}2519bool connected = false;2520uint64_t wait = 3000000;2521uint64_t time = OS::get_singleton()->get_ticks_usec();2522output.clear();2523String err_output;2524Ref<FileAccess> fa_out = data["stdio"];2525Ref<FileAccess> fa_err = data["stderr"];2526while (fa_out->is_open() && fa_err->is_open() && OS::get_singleton()->get_ticks_usec() - time < wait) {2527PackedByteArray buf;25282529buf.resize(fa_out->get_length());2530uint64_t size = fa_out->get_buffer(buf.ptrw(), buf.size());2531output.append_utf8((const char *)buf.ptr(), size);25322533buf.resize(fa_err->get_length());2534size = fa_err->get_buffer(buf.ptrw(), buf.size());2535err_output.append_utf8((const char *)buf.ptr(), size);25362537if (output.contains("[server] INFO: Device:")) {2538connected = true;2539break;2540}2541}2542print_verbose(output);2543print_verbose(err_output);2544if (!connected) {2545OS::get_singleton()->kill(data["pid"].operator int());2546add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device, scrcpy failed with the following error:\n" + err_output));2547CLEANUP_AND_RETURN(ERR_CANT_CREATE);2548}2549} else {2550args.push_back("-s");2551args.push_back(devices[p_device - 1].id);2552args.push_back("shell");2553args.push_back("am");2554args.push_back("start");2555if ((bool)EDITOR_GET("export/android/force_system_user") && devices[p_device - 1].api_level >= 17) {2556args.push_back("--user");2557args.push_back("0");2558}2559args.push_back("-a");2560args.push_back("android.intent.action.MAIN");25612562// Going with implicit launch first based on the LAUNCHER category and the app's package.2563args.push_back("-c");2564args.push_back("android.intent.category.LAUNCHER");2565args.push_back(get_package_name(p_preset, package_name));25662567output.clear();2568err = OS::get_singleton()->execute(adb, args, &output, &rv, true);2569print_verbose(output);2570if (err || rv != 0 || output.contains("Error: Activity not started")) {2571// The implicit launch failed, let's try an explicit launch by specifying the component name before giving up.2572const String component_name = get_package_name(p_preset, package_name) + "/com.godot.game.GodotAppLauncher";2573print_line("Implicit launch failed... Trying explicit launch using", component_name);2574args.erase(get_package_name(p_preset, package_name));2575args.push_back("-n");2576args.push_back(component_name);25772578output.clear();2579err = OS::get_singleton()->execute(adb, args, &output, &rv, true);2580print_verbose(output);25812582if (err || rv != 0 || output.begins_with("Error: Activity not started")) {2583add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device."));2584CLEANUP_AND_RETURN(ERR_CANT_CREATE);2585}2586}2587}25882589CLEANUP_AND_RETURN(OK);2590#undef CLEANUP_AND_RETURN2591}2592#endif // ANDROID_ENABLED25932594Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {2595return run_icon;2596}25972598String EditorExportPlatformAndroid::get_java_path() {2599String exe_ext;2600if (OS::get_singleton()->get_name() == "Windows") {2601exe_ext = ".exe";2602}2603String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");2604return java_sdk_path.path_join("bin/java" + exe_ext);2605}26062607String EditorExportPlatformAndroid::get_keytool_path() {2608String exe_ext;2609if (OS::get_singleton()->get_name() == "Windows") {2610exe_ext = ".exe";2611}2612String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");2613return java_sdk_path.path_join("bin/keytool" + exe_ext);2614}26152616String EditorExportPlatformAndroid::get_adb_path() {2617String exe_ext;2618if (OS::get_singleton()->get_name() == "Windows") {2619exe_ext = ".exe";2620}2621String sdk_path = EDITOR_GET("export/android/android_sdk_path");2622return sdk_path.path_join("platform-tools/adb" + exe_ext);2623}26242625String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_check_executes) {2626if (p_target_sdk == -1) {2627p_target_sdk = DEFAULT_TARGET_SDK_VERSION;2628}2629String exe_ext;2630if (OS::get_singleton()->get_name() == "Windows") {2631exe_ext = ".bat";2632}2633String apksigner_command_name = "apksigner" + exe_ext;2634String sdk_path = EDITOR_GET("export/android/android_sdk_path");2635String apksigner_path;26362637Error errn;2638String build_tools_dir = sdk_path.path_join("build-tools");2639Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn);2640if (errn != OK) {2641print_error("Unable to open Android 'build-tools' directory.");2642return apksigner_path;2643}26442645// There are additional versions directories we need to go through.2646Vector<String> dir_list = da->get_directories();26472648// We need to use the version of build_tools that matches the Target SDK2649// If somehow we can't find that, we see if a version between 28 and the default target SDK exists.2650// We need to avoid versions <= 27 because they fail on Java versions >92651// If we can't find that, we just use the first valid version.2652Vector<String> ideal_versions;2653Vector<String> other_versions;2654Vector<String> versions;2655bool found_target_sdk = false;2656// We only allow for versions <= 27 if specifically set2657int min_version = p_target_sdk <= 27 ? p_target_sdk : 28;2658for (String sub_dir : dir_list) {2659if (!sub_dir.begins_with(".")) {2660Vector<String> ver_numbers = sub_dir.split(".");2661// Dir not a version number, will use as last resort2662if (!ver_numbers.size() || !ver_numbers[0].is_valid_int()) {2663other_versions.push_back(sub_dir);2664continue;2665}2666int ver_number = ver_numbers[0].to_int();2667if (ver_number == p_target_sdk) {2668found_target_sdk = true;2669//ensure this is in front of the ones we check2670versions.push_back(sub_dir);2671} else {2672if (ver_number >= min_version && ver_number <= DEFAULT_TARGET_SDK_VERSION) {2673ideal_versions.push_back(sub_dir);2674} else {2675other_versions.push_back(sub_dir);2676}2677}2678}2679}2680// we will check ideal versions first, then other versions.2681versions.append_array(ideal_versions);2682versions.append_array(other_versions);26832684if (!versions.size()) {2685print_error("Unable to find the 'apksigner' tool.");2686return apksigner_path;2687}26882689int i;2690bool failed = false;2691String version_to_use;26922693String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");2694if (!java_sdk_path.is_empty()) {2695OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);26962697#ifdef UNIX_ENABLED2698String env_path = OS::get_singleton()->get_environment("PATH");2699if (!env_path.contains(java_sdk_path)) {2700OS::get_singleton()->set_environment("PATH", java_sdk_path + "/bin:" + env_path);2701}2702#endif2703}27042705List<String> args;2706args.push_back("--version");2707String output;2708int retval;2709Error err;2710for (i = 0; i < versions.size(); i++) {2711// Check if the tool is here.2712apksigner_path = build_tools_dir.path_join(versions[i]).path_join(apksigner_command_name);2713if (FileAccess::exists(apksigner_path)) {2714version_to_use = versions[i];2715// If we aren't exporting, just break here.2716if (!p_check_executes) {2717break;2718}2719// we only check to see if it executes on export because it is slow to load2720err = OS::get_singleton()->execute(apksigner_path, args, &output, &retval, false);2721if (err || retval) {2722failed = true;2723} else {2724break;2725}2726}2727}2728if (i == versions.size()) {2729if (failed) {2730print_error("All located 'apksigner' tools in " + build_tools_dir + " failed to execute");2731return "<FAILED>";2732} else {2733print_error("Unable to find the 'apksigner' tool.");2734return "";2735}2736}2737if (!found_target_sdk) {2738print_line("Could not find version of build tools that matches Target SDK, using " + version_to_use);2739} else if (failed && found_target_sdk) {2740print_line("Version of build tools that matches Target SDK failed to execute, using " + version_to_use);2741}27422743return apksigner_path;2744}27452746static bool has_valid_keystore_credentials(String &r_error_str, const String &p_keystore, const String &p_username, const String &p_password, const String &p_type) {2747String output;2748List<String> args;2749args.push_back("-list");2750args.push_back("-keystore");2751args.push_back(p_keystore);2752args.push_back("-storepass");2753args.push_back(p_password);2754args.push_back("-alias");2755args.push_back(p_username);2756String keytool_path = EditorExportPlatformAndroid::get_keytool_path();2757Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);2758String keytool_error = "keytool error:";2759bool valid = output.substr(0, keytool_error.length()) != keytool_error;27602761if (error != OK) {2762r_error_str = TTR("Error: There was a problem validating the keystore username and password");2763return false;2764}2765if (!valid) {2766r_error_str = TTR(p_type + " Username and/or Password is invalid for the given " + p_type + " Keystore");2767return false;2768}2769r_error_str = "";2770return true;2771}27722773bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) {2774String dk = _get_keystore_path(p_preset, true);2775String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);2776String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);2777String rk = _get_keystore_path(p_preset, false);2778String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);2779String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);27802781bool valid = true;2782if (!dk.is_empty() && !dk_user.is_empty() && !dk_password.is_empty()) {2783String err = "";2784valid = has_valid_keystore_credentials(err, dk, dk_user, dk_password, "Debug");2785r_error += err;2786}2787if (!rk.is_empty() && !rk_user.is_empty() && !rk_password.is_empty()) {2788String err = "";2789valid = has_valid_keystore_credentials(err, rk, rk_user, rk_password, "Release");2790r_error += err;2791}2792return valid;2793}27942795#ifdef MODULE_MONO_ENABLED2796static uint64_t _last_validate_tfm_time = 0;2797static String _last_validate_tfm = "";27982799bool _validate_dotnet_tfm(const String &required_tfm, String &r_error) {2800String assembly_name = Path::get_csharp_project_name();2801String project_path = ProjectSettings::get_singleton()->globalize_path("res://" + assembly_name + ".csproj");28022803if (!FileAccess::exists(project_path)) {2804return true;2805}28062807uint64_t modified_time = FileAccess::get_modified_time(project_path);2808String tfm;28092810if (modified_time == _last_validate_tfm_time) {2811tfm = _last_validate_tfm;2812} else {2813String pipe;2814List<String> args;2815args.push_back("build");2816args.push_back(project_path);2817args.push_back("/p:GodotTargetPlatform=android");2818args.push_back("--getProperty:TargetFramework");28192820int exitcode;2821Error err = OS::get_singleton()->execute("dotnet", args, &pipe, &exitcode, true);2822if (err != OK || exitcode != 0) {2823if (err != OK) {2824WARN_PRINT("Failed to execute dotnet command. Error " + String(error_names[err]));2825} else if (exitcode != 0) {2826print_line(pipe);2827WARN_PRINT("dotnet command exited with code " + itos(exitcode) + ". See output above for more details.");2828}2829r_error += vformat(TTR("Unable to determine the C# project's TFM, it may be incompatible. The export template only supports '%s'. Make sure the project targets '%s' or consider using gradle builds instead."), required_tfm, required_tfm) + "\n";2830return true;2831} else {2832tfm = pipe.strip_edges();2833_last_validate_tfm_time = modified_time;2834_last_validate_tfm = tfm;2835}2836}28372838if (tfm != required_tfm) {2839r_error += vformat(TTR("C# project targets '%s' but the export template only supports '%s'. Consider using gradle builds instead."), tfm, required_tfm) + "\n";2840return false;2841}28422843return true;2844}2845#endif28462847bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {2848String err;2849bool valid = false;2850const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");28512852#ifdef MODULE_MONO_ENABLED2853// Android export is still a work in progress, keep a message as a warning.2854err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n";28552856if (!gradle_build_enabled) {2857// For template exports we only support .NET 9 because the template2858// includes .jar dependencies that may only be compatible with .NET 9.2859if (!_validate_dotnet_tfm("net9.0", err)) {2860r_error = err;2861return false;2862}2863}2864#endif28652866// Look for export templates (first official, and if defined custom templates).28672868if (!gradle_build_enabled) {2869String template_err;2870bool dvalid = false;2871bool rvalid = false;2872bool has_export_templates = false;28732874if (p_preset->get("custom_template/debug") != "") {2875dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));2876if (!dvalid) {2877template_err += TTR("Custom debug template not found.") + "\n";2878}2879has_export_templates |= dvalid;2880} else {2881has_export_templates |= exists_export_template("android_debug.apk", &template_err);2882}28832884if (p_preset->get("custom_template/release") != "") {2885rvalid = FileAccess::exists(p_preset->get("custom_template/release"));2886if (!rvalid) {2887template_err += TTR("Custom release template not found.") + "\n";2888}2889has_export_templates |= rvalid;2890} else {2891has_export_templates |= exists_export_template("android_release.apk", &template_err);2892}28932894r_missing_templates = !has_export_templates;2895valid = dvalid || rvalid || has_export_templates;2896if (!valid) {2897err += template_err;2898}2899} else {2900// Validate the custom gradle android source template.2901bool android_source_template_valid = false;2902const String android_source_template = p_preset->get("gradle_build/android_source_template");2903if (!android_source_template.is_empty()) {2904android_source_template_valid = FileAccess::exists(android_source_template);2905if (!android_source_template_valid) {2906err += TTR("Custom Android source template not found.") + "\n";2907}2908}29092910// Validate the installed build template.2911bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle"));2912if (!installed_android_build_template) {2913if (!android_source_template_valid) {2914r_missing_templates = !exists_export_template("android_source.zip", &err);2915}2916err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n";2917} else {2918r_missing_templates = false;2919}29202921valid = installed_android_build_template && !r_missing_templates;2922}29232924// Validate the rest of the export configuration.29252926if (p_debug) {2927String dk = _get_keystore_path(p_preset, true);2928String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);2929String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);29302931if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) {2932valid = false;2933err += TTR("Either Debug Keystore, Debug User AND Debug Password settings must be configured OR none of them.") + "\n";2934}29352936// Use OR to make the export UI able to show this error.2937if (!dk.is_empty() && !FileAccess::exists(dk)) {2938dk = EDITOR_GET("export/android/debug_keystore");2939if (!FileAccess::exists(dk)) {2940valid = false;2941err += TTR("Debug keystore not configured in the Editor Settings nor in the preset.") + "\n";2942}2943}2944} else {2945String rk = _get_keystore_path(p_preset, false);2946String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);2947String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);29482949if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) {2950valid = false;2951err += TTR("Either Release Keystore, Release User AND Release Password settings must be configured OR none of them.") + "\n";2952}29532954if (!rk.is_empty() && !FileAccess::exists(rk)) {2955valid = false;2956err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";2957}2958}29592960#ifndef ANDROID_ENABLED2961String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");2962if (java_sdk_path.is_empty()) {2963err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";2964valid = false;2965} else {2966// Validate the given path by checking that `java` is present under the `bin` directory.2967Error errn;2968// Check for the bin directory.2969Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);2970if (errn != OK) {2971err += TTR("Invalid Java SDK path in Editor Settings.") + " ";2972err += TTR("Missing 'bin' directory!");2973err += "\n";2974valid = false;2975} else {2976// Check for the `java` command.2977String java_path = get_java_path();2978if (!FileAccess::exists(java_path)) {2979err += TTR("Unable to find 'java' command using the Java SDK path.") + " ";2980err += TTR("Please check the Java SDK directory specified in Editor Settings.");2981err += "\n";2982valid = false;2983}2984}2985}29862987String sdk_path = EDITOR_GET("export/android/android_sdk_path");2988if (sdk_path.is_empty()) {2989err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";2990valid = false;2991} else {2992Error errn;2993// Check for the platform-tools directory.2994Ref<DirAccess> da = DirAccess::open(sdk_path.path_join("platform-tools"), &errn);2995if (errn != OK) {2996err += TTR("Invalid Android SDK path in Editor Settings.") + " ";2997err += TTR("Missing 'platform-tools' directory!");2998err += "\n";2999valid = false;3000}30013002// Validate that adb is available.3003String adb_path = get_adb_path();3004if (!FileAccess::exists(adb_path)) {3005err += TTR("Unable to find Android SDK platform-tools' adb command.") + " ";3006err += TTR("Please check in the Android SDK directory specified in Editor Settings.");3007err += "\n";3008valid = false;3009}30103011// Check for the build-tools directory.3012Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.path_join("build-tools"), &errn);3013if (errn != OK) {3014err += TTR("Invalid Android SDK path in Editor Settings.") + " ";3015err += TTR("Missing 'build-tools' directory!");3016err += "\n";3017valid = false;3018}30193020String target_sdk_version = p_preset->get("gradle_build/target_sdk");3021if (!target_sdk_version.is_valid_int()) {3022target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);3023}3024// Validate that apksigner is available.3025String apksigner_path = get_apksigner_path(target_sdk_version.to_int());3026if (!FileAccess::exists(apksigner_path)) {3027err += TTR("Unable to find Android SDK build-tools' apksigner command.") + " ";3028err += TTR("Please check in the Android SDK directory specified in Editor Settings.");3029err += "\n";3030valid = false;3031}3032}3033#endif30343035if (!err.is_empty()) {3036r_error = err;3037}30383039return valid;3040}30413042bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {3043String err;3044bool valid = true;30453046List<ExportOption> options;3047get_export_options(&options);3048for (const EditorExportPlatform::ExportOption &E : options) {3049if (get_export_option_visibility(p_preset.ptr(), E.option.name)) {3050String warn = get_export_option_warning(p_preset.ptr(), E.option.name);3051if (!warn.is_empty()) {3052err += warn + "\n";3053if (E.required) {3054valid = false;3055}3056}3057}3058}30593060if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {3061valid = false;3062}30633064bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");3065if (gradle_build_enabled) {3066String build_version_path = ExportTemplateManager::get_android_build_directory(p_preset).get_base_dir().path_join(".build_version");3067Ref<FileAccess> f = FileAccess::open(build_version_path, FileAccess::READ);3068if (f.is_valid()) {3069String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);3070String installed_version = f->get_line().strip_edges();3071if (current_version != installed_version) {3072err += vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version);3073err += "\n";3074}3075}3076} else {3077if (_is_transparency_allowed(p_preset)) {3078// Warning only, so don't override `valid`.3079err += vformat(TTR("\"Use Gradle Build\" is required for transparent background on Android"));3080err += "\n";3081}3082}30833084String target_sdk_str = p_preset->get("gradle_build/target_sdk");3085int target_sdk_int = DEFAULT_TARGET_SDK_VERSION;3086if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do.3087if (target_sdk_str.is_valid_int()) {3088target_sdk_int = target_sdk_str.to_int();3089if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) {3090// Warning only, so don't override `valid`.3091err += 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);3092err += "\n";3093}3094}3095}30963097String current_renderer = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");3098if (current_renderer == "forward_plus") {3099// Warning only, so don't override `valid`.3100err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer);3101err += "\n";3102}31033104String package_name = p_preset->get("package/unique_name");3105if (package_name.contains("$genname") && !is_project_name_valid(p_preset)) {3106// Warning only, so don't override `valid`.3107err += vformat(TTR("The project name does not meet the requirement for the package name format and will be updated to \"%s\". Please explicitly specify the package name if needed."), get_valid_basename(p_preset));3108err += "\n";3109}31103111r_error = err;3112return valid;3113}31143115List<String> EditorExportPlatformAndroid::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {3116List<String> list;3117int export_format = int(p_preset->get("gradle_build/export_format"));3118if (export_format == EXPORT_FORMAT_AAB) {3119list.push_back("aab");3120} else {3121list.push_back("apk");3122}3123return list;3124}31253126String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) {3127int version_code = p_preset->get("version/code");3128String package_name = p_preset->get("package/unique_name");3129String apk_file_name = "main." + itos(version_code) + "." + get_package_name(p_preset, package_name) + ".obb";3130String fullpath = p_path.get_base_dir().path_join(apk_file_name);3131return fullpath;3132}31333134Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) {3135String fullpath = get_apk_expansion_fullpath(p_preset, p_path);3136Error err = save_pack(p_preset, p_debug, fullpath);3137return err;3138}31393140void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags) {3141String cmdline = p_preset->get("command_line/extra_args");3142Vector<String> command_line_strings = cmdline.strip_edges().split(" ");3143for (int i = 0; i < command_line_strings.size(); i++) {3144if (command_line_strings[i].strip_edges().length() == 0) {3145command_line_strings.remove_at(i);3146i--;3147}3148}31493150command_line_strings.append_array(gen_export_flags(p_flags));31513152bool apk_expansion = p_preset->get("apk_expansion/enable");3153if (apk_expansion) {3154String fullpath = get_apk_expansion_fullpath(p_preset, p_path);3155String apk_expansion_public_key = p_preset->get("apk_expansion/public_key");31563157command_line_strings.push_back("--use_apk_expansion");3158command_line_strings.push_back("--apk_expansion_md5");3159command_line_strings.push_back(FileAccess::get_md5(fullpath));3160command_line_strings.push_back("--apk_expansion_key");3161command_line_strings.push_back(apk_expansion_public_key.strip_edges());3162}31633164#ifndef XR_DISABLED3165int xr_mode_index = p_preset->get("xr_features/xr_mode");3166if (xr_mode_index == XR_MODE_OPENXR) {3167command_line_strings.push_back("--xr_mode_openxr");3168} else { // XRMode.REGULAR is the default.3169command_line_strings.push_back("--xr_mode_regular");31703171// Also override the 'xr/openxr/enabled' project setting.3172// This is useful for multi-platforms projects supporting both XR and non-XR devices. The project would need3173// to enable openxr for development, and would create multiple XR and non-XR export presets.3174// These command line args ensure that the non-XR export presets will have openxr disabled.3175command_line_strings.push_back("--xr-mode");3176command_line_strings.push_back("off");3177}3178#endif // XR_DISABLED31793180bool immersive = p_preset->get("screen/immersive_mode");3181if (immersive) {3182command_line_strings.push_back("--fullscreen");3183}31843185bool edge_to_edge = p_preset->get("screen/edge_to_edge");3186if (edge_to_edge) {3187command_line_strings.push_back("--edge_to_edge");3188}31893190String background_color = "#" + p_preset->get("screen/background_color").operator Color().to_html(false);31913192// For Gradle build, _fix_themes_xml() sets background to transparent if _is_transparency_allowed().3193// Overriding to transparent here too as it's used as fallback for system bar appearance.3194if (_is_transparency_allowed(p_preset) && p_preset->get("gradle_build/use_gradle_build")) {3195background_color = "#00000000";3196}3197command_line_strings.push_back("--background_color");3198command_line_strings.push_back(background_color);31993200bool debug_opengl = p_preset->get("graphics/opengl_debug");3201if (debug_opengl) {3202command_line_strings.push_back("--debug_opengl");3203}32043205if (command_line_strings.size()) {3206r_command_line_flags.resize(4);3207encode_uint32(command_line_strings.size(), &r_command_line_flags.write[0]);3208for (int i = 0; i < command_line_strings.size(); i++) {3209print_line(itos(i) + " param: " + command_line_strings[i]);3210CharString command_line_argument = command_line_strings[i].utf8();3211int base = r_command_line_flags.size();3212int length = command_line_argument.length();3213if (length == 0) {3214continue;3215}3216r_command_line_flags.resize(base + 4 + length);3217encode_uint32(length, &r_command_line_flags.write[base]);3218memcpy(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length);3219}3220}3221}32223223Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {3224int export_format = int(p_preset->get("gradle_build/export_format"));3225if (export_format == EXPORT_FORMAT_AAB) {3226add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));3227return FAILED;3228}32293230String keystore;3231String password;3232String user;3233if (p_debug) {3234keystore = _get_keystore_path(p_preset, true);3235password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);3236user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);32373238if (keystore.is_empty()) {3239keystore = EDITOR_GET("export/android/debug_keystore");3240password = EDITOR_GET("export/android/debug_keystore_pass");3241user = EDITOR_GET("export/android/debug_keystore_user");3242}32433244if (ep.step(TTR("Signing debug APK..."), 104)) {3245return ERR_SKIP;3246}3247} else {3248keystore = _get_keystore_path(p_preset, false);3249password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);3250user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);32513252if (ep.step(TTR("Signing release APK..."), 104)) {3253return ERR_SKIP;3254}3255}32563257if (!FileAccess::exists(keystore)) {3258if (p_debug) {3259add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));3260} else {3261add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));3262}3263return ERR_FILE_CANT_OPEN;3264}32653266String apk_path = export_path;3267if (apk_path.is_relative_path()) {3268apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);3269}3270apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();32713272Error err;3273#ifdef ANDROID_ENABLED3274err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);3275if (err != OK) {3276add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));3277return err;3278}3279#else3280String target_sdk_version = p_preset->get("gradle_build/target_sdk");3281if (!target_sdk_version.is_valid_int()) {3282target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);3283}32843285String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);3286print_verbose("Starting signing of the APK binary using " + apksigner);3287if (apksigner == "<FAILED>") {3288add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned."));3289return OK;3290}3291if (!FileAccess::exists(apksigner)) {3292add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned."));3293return OK;3294}32953296String output;3297List<String> args;3298args.push_back("sign");3299args.push_back("--verbose");3300args.push_back("--ks");3301args.push_back(keystore);3302args.push_back("--ks-pass");3303args.push_back("pass:" + password);3304args.push_back("--ks-key-alias");3305args.push_back(user);3306args.push_back(apk_path);3307if (OS::get_singleton()->is_stdout_verbose() && p_debug) {3308// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.3309print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));3310} else {3311List<String> redacted_args = List<String>(args);3312redacted_args.find(keystore)->set("<REDACTED>");3313redacted_args.find("pass:" + password)->set("pass:<REDACTED>");3314redacted_args.find(user)->set("<REDACTED>");3315print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));3316}3317int retval;3318err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);3319if (err != OK) {3320add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));3321return err;3322}3323// By design, apksigner does not output credentials in its output unless --verbose is used3324print_line(output);3325if (retval) {3326add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval));3327add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));3328return ERR_CANT_CREATE;3329}3330#endif33313332if (ep.step(TTR("Verifying APK..."), 105)) {3333return ERR_SKIP;3334}33353336#ifdef ANDROID_ENABLED3337err = OS_Android::get_singleton()->verify_apk(apk_path);3338if (err != OK) {3339add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));3340return err;3341}3342#else3343args.clear();3344args.push_back("verify");3345args.push_back("--verbose");3346args.push_back(apk_path);3347if (p_debug) {3348print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));3349}33503351output.clear();3352err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);3353if (err != OK) {3354add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));3355return err;3356}3357print_verbose(output);3358if (retval) {3359add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));3360add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));3361return ERR_CANT_CREATE;3362}3363#endif33643365print_verbose("Successfully completed signing build.");33663367#ifdef ANDROID_ENABLED3368bool prompt_apk_install = EDITOR_GET("export/android/install_exported_apk");3369if (prompt_apk_install) {3370OS_Android::get_singleton()->shell_open(apk_path);3371}3372#endif33733374return OK;3375}33763377void EditorExportPlatformAndroid::_clear_assets_directory(const Ref<EditorExportPreset> &p_preset) {3378Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES);3379String gradle_build_directory = ExportTemplateManager::get_android_build_directory(p_preset);33803381// Clear the APK assets directory3382String apk_assets_directory = gradle_build_directory.path_join(APK_ASSETS_DIRECTORY);3383if (da_res->dir_exists(apk_assets_directory)) {3384print_verbose("Clearing APK assets directory...");3385Ref<DirAccess> da_assets = DirAccess::open(apk_assets_directory);3386ERR_FAIL_COND(da_assets.is_null());33873388da_assets->erase_contents_recursive();3389da_res->remove(apk_assets_directory);3390}33913392// Clear the AAB assets directory3393String aab_assets_directory = gradle_build_directory.path_join(AAB_ASSETS_DIRECTORY);3394if (da_res->dir_exists(aab_assets_directory)) {3395print_verbose("Clearing AAB assets directory...");3396Ref<DirAccess> da_assets = DirAccess::open(aab_assets_directory);3397ERR_FAIL_COND(da_assets.is_null());33983399da_assets->erase_contents_recursive();3400da_res->remove(aab_assets_directory);3401}3402}34033404void EditorExportPlatformAndroid::_remove_copied_libs(String p_gdextension_libs_path) {3405print_verbose("Removing previously installed libraries...");3406Error error;3407String libs_json = FileAccess::get_file_as_string(p_gdextension_libs_path, &error);3408if (error || libs_json.is_empty()) {3409print_verbose("No previously installed libraries found");3410return;3411}34123413JSON json;3414error = json.parse(libs_json);3415ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message());34163417Vector<String> libs = json.get_data();3418Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);3419for (int i = 0; i < libs.size(); i++) {3420print_verbose("Removing previously installed library " + libs[i]);3421da->remove(libs[i]);3422}3423da->remove(p_gdextension_libs_path);3424}34253426String EditorExportPlatformAndroid::join_list(const List<String> &p_parts, const String &p_separator) {3427String ret;3428for (List<String>::ConstIterator itr = p_parts.begin(); itr != p_parts.end(); ++itr) {3429if (itr != p_parts.begin()) {3430ret += p_separator;3431}3432ret += *itr;3433}3434return ret;3435}34363437String EditorExportPlatformAndroid::join_abis(const Vector<EditorExportPlatformAndroid::ABI> &p_parts, const String &p_separator, bool p_use_arch) {3438String ret;3439for (int i = 0; i < p_parts.size(); ++i) {3440if (i > 0) {3441ret += p_separator;3442}3443ret += (p_use_arch) ? p_parts[i].arch : p_parts[i].abi;3444}3445return ret;3446}34473448String EditorExportPlatformAndroid::_get_deprecated_plugins_names(const Ref<EditorExportPreset> &p_preset) const {3449Vector<String> names;34503451#ifndef DISABLE_DEPRECATED3452PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);3453#endif // DISABLE_DEPRECATED34543455String plugins_names = String("|").join(names);3456return plugins_names;3457}34583459String EditorExportPlatformAndroid::_get_plugins_names(const Ref<EditorExportPreset> &p_preset) const {3460Vector<String> names;34613462#ifndef DISABLE_DEPRECATED3463PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset), names);3464#endif // DISABLE_DEPRECATED34653466Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();3467for (int i = 0; i < export_plugins.size(); i++) {3468if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {3469names.push_back(export_plugins[i]->get_name());3470}3471}34723473String plugins_names = String("|").join(names);3474return plugins_names;3475}34763477String EditorExportPlatformAndroid::_resolve_export_plugin_android_library_path(const String &p_android_library_path) const {3478String absolute_path;3479if (!p_android_library_path.is_empty()) {3480if (p_android_library_path.is_absolute_path()) {3481absolute_path = ProjectSettings::get_singleton()->globalize_path(p_android_library_path);3482} else {3483const String export_plugin_absolute_path = String("res://addons/").path_join(p_android_library_path);3484absolute_path = ProjectSettings::get_singleton()->globalize_path(export_plugin_absolute_path);3485}3486}3487return absolute_path;3488}34893490bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExportPreset> &p_preset) {3491bool first_build = last_gradle_build_time == 0;3492bool have_plugins_changed = false;3493String gradle_build_dir = ExportTemplateManager::get_android_build_directory(p_preset);3494bool has_build_dir_changed = last_gradle_build_dir != gradle_build_dir;34953496String plugin_names = _get_plugins_names(p_preset);34973498if (!first_build) {3499have_plugins_changed = plugin_names != last_plugin_names;3500#ifndef DISABLE_DEPRECATED3501if (!have_plugins_changed) {3502Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);3503for (int i = 0; i < enabled_plugins.size(); i++) {3504if (enabled_plugins.get(i).last_updated > last_gradle_build_time) {3505have_plugins_changed = true;3506break;3507}3508}3509}3510#endif // DISABLE_DEPRECATED3511}35123513last_gradle_build_time = OS::get_singleton()->get_unix_time();3514last_gradle_build_dir = gradle_build_dir;3515last_plugin_names = plugin_names;35163517return have_plugins_changed || has_build_dir_changed || first_build;3518}35193520Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {3521int export_format = int(p_preset->get("gradle_build/export_format"));3522bool should_sign = p_preset->get("package/signed");3523return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags);3524}35253526Error EditorExportPlatformAndroid::_generate_sparse_pck_metadata(const Ref<EditorExportPreset> &p_preset, PackData &p_pack_data, Vector<uint8_t> &r_data) {3527Error err;3528Ref<FileAccess> ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export_index", "tmp", false, &err);3529if (err != OK) {3530add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not create temporary file!"));3531return err;3532}3533int64_t pck_start_pos = ftmp->get_position();3534uint64_t file_base_ofs = 0;3535uint64_t dir_base_ofs = 0;3536EditorExportPlatform::_store_header(ftmp, p_preset->get_enc_pck() && p_preset->get_enc_directory(), true, file_base_ofs, dir_base_ofs, p_pack_data.salt);35373538// Write directory.3539uint64_t dir_offset = ftmp->get_position();3540ftmp->seek(dir_base_ofs);3541ftmp->store_64(dir_offset - pck_start_pos);3542ftmp->seek(dir_offset);35433544Vector<uint8_t> key;3545if (p_preset->get_enc_pck() && p_preset->get_enc_directory()) {3546String script_key = _get_script_encryption_key(p_preset);3547key.resize(32);3548if (script_key.length() == 64) {3549for (int i = 0; i < 32; i++) {3550int v = 0;3551if (i * 2 < script_key.length()) {3552char32_t ct = script_key[i * 2];3553if (is_digit(ct)) {3554ct = ct - '0';3555} else if (ct >= 'a' && ct <= 'f') {3556ct = 10 + ct - 'a';3557}3558v |= ct << 4;3559}35603561if (i * 2 + 1 < script_key.length()) {3562char32_t ct = script_key[i * 2 + 1];3563if (is_digit(ct)) {3564ct = ct - '0';3565} else if (ct >= 'a' && ct <= 'f') {3566ct = 10 + ct - 'a';3567}3568v |= ct;3569}3570key.write[i] = v;3571}3572}3573}35743575if (!EditorExportPlatform::_encrypt_and_store_directory(ftmp, p_pack_data, key, p_preset->get_seed(), 0)) {3576add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Can't create encrypted file."));3577return ERR_CANT_CREATE;3578}35793580r_data.resize(ftmp->get_length());3581ftmp->seek(0);3582ftmp->get_buffer(r_data.ptrw(), r_data.size());3583ftmp.unref();35843585return OK;3586}35873588Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags) {3589ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);35903591const String base_dir = p_path.get_base_dir();3592if (!DirAccess::exists(base_dir)) {3593add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));3594return ERR_FILE_BAD_PATH;3595}35963597String src_apk;3598Error err;35993600EditorProgress ep("export", TTR("Exporting for Android"), 105, true);36013602bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build"));3603String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : "";3604bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG);3605bool apk_expansion = p_preset->get("apk_expansion/enable");3606Vector<ABI> enabled_abis = get_enabled_abis(p_preset);36073608print_verbose("Exporting for Android...");3609print_verbose("- debug build: " + bool_to_string(p_debug));3610print_verbose("- export path: " + p_path);3611print_verbose("- export format: " + itos(export_format));3612print_verbose("- sign build: " + bool_to_string(should_sign));3613print_verbose("- gradle build enabled: " + bool_to_string(use_gradle_build));3614print_verbose("- apk expansion enabled: " + bool_to_string(apk_expansion));3615print_verbose("- enabled abis: " + join_abis(enabled_abis, ",", false));3616print_verbose("- export filter: " + itos(p_preset->get_export_filter()));3617print_verbose("- include filter: " + p_preset->get_include_filter());3618print_verbose("- exclude filter: " + p_preset->get_exclude_filter());36193620Ref<Image> main_image;3621Ref<Image> foreground;3622Ref<Image> background;3623Ref<Image> monochrome;36243625load_icon_refs(p_preset, main_image, foreground, background, monochrome);36263627Vector<uint8_t> command_line_flags;3628// Write command line flags into the command_line_flags variable.3629get_command_line_flags(p_preset, p_path, p_flags, command_line_flags);36303631if (export_format == EXPORT_FORMAT_AAB) {3632if (!p_path.ends_with(".aab")) {3633add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension."));3634return ERR_UNCONFIGURED;3635}3636if (apk_expansion) {3637add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle."));3638return ERR_UNCONFIGURED;3639}3640}3641if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) {3642add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension."));3643return ERR_UNCONFIGURED;3644}3645if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) {3646add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!"));3647return ERR_UNCONFIGURED;3648}3649String err_string;3650if (!has_valid_username_and_password(p_preset, err_string)) {3651add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR(err_string));3652return ERR_UNCONFIGURED;3653}36543655if (use_gradle_build) {3656print_verbose("Starting gradle build...");3657//test that installed build version is alright3658{3659print_verbose("Checking build version...");3660String gradle_base_directory = gradle_build_directory.get_base_dir();3661Ref<FileAccess> f = FileAccess::open(gradle_base_directory.path_join(".build_version"), FileAccess::READ);3662if (f.is_null()) {3663add_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."));3664return ERR_UNCONFIGURED;3665}3666String current_version = ExportTemplateManager::get_android_template_identifier(p_preset);3667String installed_version = f->get_line().strip_edges();3668print_verbose("- build version: " + installed_version);3669if (installed_version != current_version) {3670add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR(MISMATCHED_VERSIONS_MESSAGE), installed_version, current_version));3671return ERR_UNCONFIGURED;3672}3673}3674const String assets_directory = get_assets_directory(p_preset, export_format);3675#ifndef ANDROID_ENABLED3676String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");3677if (java_sdk_path.is_empty()) {3678add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));3679return ERR_UNCONFIGURED;3680}3681print_verbose("Java sdk path: " + java_sdk_path);36823683String sdk_path = EDITOR_GET("export/android/android_sdk_path");3684if (sdk_path.is_empty()) {3685add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));3686return ERR_UNCONFIGURED;3687}3688print_verbose("Android sdk path: " + sdk_path);3689#endif36903691// TODO: should we use "package/name" or "application/config/name"?3692String project_name = get_project_name(p_preset, p_preset->get("package/name"));3693err = _create_project_name_strings_files(p_preset, project_name, gradle_build_directory, get_project_setting(p_preset, "application/config/name_localized")); //project name localization.3694if (err != OK) {3695add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res/*.xml files with project name."));3696}3697// Copies the project icon files into the appropriate Gradle project directory.3698_copy_icons_to_gradle_project(p_preset, main_image, foreground, background, monochrome);3699// Write an AndroidManifest.xml file into the Gradle project directory.3700_write_tmp_manifest(p_preset, p_give_internet, p_debug);3701// Modify res/values/themes.xml file.3702_fix_themes_xml(p_preset);37033704//stores all the project files inside the Gradle project directory. Also includes all ABIs3705_clear_assets_directory(p_preset);3706String gdextension_libs_path = gradle_build_directory.path_join(GDEXTENSION_LIBS_PATH);3707_remove_copied_libs(gdextension_libs_path);3708if (!apk_expansion) {3709print_verbose("Exporting project files...");3710CustomExportData user_data;3711user_data.assets_directory = assets_directory;3712user_data.libs_directory = gradle_build_directory.path_join("libs");3713user_data.debug = p_debug;3714if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {3715err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &user_data, copy_gradle_so);3716} else {3717user_data.pd.path = "assets.sparsepck";3718user_data.pd.use_sparse_pck = true;3719if (p_preset->get_enc_directory()) {3720RandomPCG rng = RandomPCG(p_preset->get_seed());3721for (int i = 0; i < 32; i++) {3722user_data.pd.salt += String::chr(1 + rng.rand() % 254);3723}3724}3725err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, nullptr, &user_data, copy_gradle_so);37263727Vector<uint8_t> enc_data;3728err = _generate_sparse_pck_metadata(p_preset, user_data.pd, enc_data);3729if (err != OK) {3730add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));3731return err;3732}37333734err = store_file_at_path(user_data.assets_directory + "/assets.sparsepck", enc_data);3735if (err != OK) {3736add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not write PCK directory!"));3737return err;3738}3739}3740if (err != OK) {3741add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project."));3742return err;3743}3744if (user_data.libs.size() > 0) {3745Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE);3746fa->store_string(JSON::stringify(user_data.libs, "\t"));3747}3748} else {3749print_verbose("Saving apk expansion file...");3750err = save_apk_expansion_file(p_preset, p_debug, p_path);3751if (err != OK) {3752add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));3753return err;3754}3755}37563757print_verbose("Storing command line flags...");3758store_file_at_path(assets_directory + "/_cl_", command_line_flags);37593760#ifndef ANDROID_ENABLED3761print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);3762OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);37633764print_verbose("Updating ANDROID_HOME environment to " + sdk_path);3765OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);3766#endif3767String build_command;37683769#ifdef WINDOWS_ENABLED3770build_command = "gradlew.bat";3771#else3772build_command = "gradlew";3773#endif37743775String build_path = ProjectSettings::get_singleton()->globalize_path(gradle_build_directory);3776build_command = build_path.path_join(build_command);37773778String package_name = get_package_name(p_preset, p_preset->get("package/unique_name"));3779String version_code = itos(p_preset->get("version/code"));3780String version_name = p_preset->get_version("version/name");3781String min_sdk_version = p_preset->get("gradle_build/min_sdk");3782if (!min_sdk_version.is_valid_int()) {3783min_sdk_version = itos(DEFAULT_MIN_SDK_VERSION);3784}3785String target_sdk_version = p_preset->get("gradle_build/target_sdk");3786if (!target_sdk_version.is_valid_int()) {3787target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);3788}3789String enabled_abi_string = join_abis(enabled_abis, "|", false);3790String sign_flag = bool_to_string(should_sign);3791String zipalign_flag = "true";3792String compress_native_libraries_flag = bool_to_string(p_preset->get("gradle_build/compress_native_libraries"));37933794Vector<String> android_libraries;3795Vector<String> android_dependencies;3796Vector<String> android_dependencies_maven_repos;37973798#ifndef DISABLE_DEPRECATED3799Vector<PluginConfigAndroid> enabled_plugins = get_enabled_plugins(p_preset);3800PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_LOCAL, enabled_plugins, android_libraries);3801PluginConfigAndroid::get_plugins_binaries(PluginConfigAndroid::BINARY_TYPE_REMOTE, enabled_plugins, android_dependencies);3802PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos);3803#endif // DISABLE_DEPRECATED38043805bool has_dotnet_project = false;3806Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();3807for (int i = 0; i < export_plugins.size(); i++) {3808if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {3809PackedStringArray export_plugin_android_libraries = export_plugins[i]->get_android_libraries(Ref<EditorExportPlatform>(this), p_debug);3810for (int k = 0; k < export_plugin_android_libraries.size(); k++) {3811const String resolved_android_library_path = _resolve_export_plugin_android_library_path(export_plugin_android_libraries[k]);3812if (!resolved_android_library_path.is_empty()) {3813android_libraries.push_back(resolved_android_library_path);3814}3815}38163817PackedStringArray export_plugin_android_dependencies = export_plugins[i]->get_android_dependencies(Ref<EditorExportPlatform>(this), p_debug);3818android_dependencies.append_array(export_plugin_android_dependencies);38193820PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug);3821android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos);3822}38233824PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug);3825if (features.has("dotnet")) {3826has_dotnet_project = true;3827}3828}38293830bool clean_build_required = _is_clean_build_required(p_preset);3831String combined_android_libraries = String("|").join(android_libraries);3832String combined_android_dependencies = String("|").join(android_dependencies);3833String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);38343835List<String> cmdline;3836cmdline.push_back("validateJavaVersion");3837if (clean_build_required) {3838cmdline.push_back("clean");3839}38403841String edition = has_dotnet_project ? "Mono" : "Standard";3842String build_type = p_debug ? "Debug" : "Release";3843if (export_format == EXPORT_FORMAT_AAB) {3844String bundle_build_command = vformat("bundle%s%s", edition, build_type);3845cmdline.push_back(bundle_build_command);3846} else if (export_format == EXPORT_FORMAT_APK) {3847String apk_build_command = vformat("assemble%s%s", edition, build_type);3848cmdline.push_back(apk_build_command);3849}38503851String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons");38523853#ifndef ANDROID_ENABLED3854cmdline.push_back("-p"); // argument to specify the start directory.3855cmdline.push_back(build_path); // start directory.3856#endif3857cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies3858cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.3859cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.3860cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name.3861cmdline.push_back("-Pexport_version_min_sdk=" + min_sdk_version); // argument to specify the min sdk.3862cmdline.push_back("-Pexport_version_target_sdk=" + target_sdk_version); // argument to specify the target sdk.3863cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs.3864cmdline.push_back("-Pplugins_local_binaries=" + combined_android_libraries); // argument to specify the list of android libraries provided by plugins.3865cmdline.push_back("-Pplugins_remote_binaries=" + combined_android_dependencies); // argument to specify the list of android dependencies provided by plugins.3866cmdline.push_back("-Pplugins_maven_repos=" + combined_android_dependencies_maven_repos); // argument to specify the list of maven repos for android dependencies provided by plugins.3867cmdline.push_back("-Pperform_zipalign=" + zipalign_flag); // argument to specify whether the build should be zipaligned.3868cmdline.push_back("-Pperform_signing=" + sign_flag); // argument to specify whether the build should be signed.3869cmdline.push_back("-Pcompress_native_libraries=" + compress_native_libraries_flag); // argument to specify whether the build should compress native libraries.38703871// NOTE: The release keystore is not included in the verbose logging3872// to avoid accidentally leaking sensitive information when sharing verbose logs for troubleshooting.3873// Any non-sensitive additions to the command line arguments must be done above this section.3874// Sensitive additions must be done below the logging statement.3875print_verbose("Build Android project using gradle command: " + String("\n") + build_command + " " + join_list(cmdline, String(" ")));38763877if (should_sign) {3878if (p_debug) {3879String debug_keystore = _get_keystore_path(p_preset, true);3880String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS);3881String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER);38823883if (debug_keystore.is_empty()) {3884debug_keystore = EDITOR_GET("export/android/debug_keystore");3885debug_password = EDITOR_GET("export/android/debug_keystore_pass");3886debug_user = EDITOR_GET("export/android/debug_keystore_user");3887}3888if (debug_keystore.is_relative_path()) {3889debug_keystore = OS::get_singleton()->get_resource_dir().path_join(debug_keystore).simplify_path();3890}3891if (!FileAccess::exists(debug_keystore)) {3892add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));3893return ERR_FILE_CANT_OPEN;3894}3895#ifdef ANDROID_ENABLED3896if (debug_keystore.begins_with("assets://")) {3897// The Gradle build environment app can't access the Godot3898// editor's assets, so we need to copy this to temp file.3899Error err;3900PackedByteArray keystore_data = FileAccess::get_file_as_bytes(debug_keystore, &err);3901if (err == OK) {3902String temp_dir = build_path + "/.android";3903String temp_filename = temp_dir + "/debug.keystore";39043905DirAccess::make_dir_recursive_absolute(temp_dir);3906Ref<FileAccess> temp_file = FileAccess::open(temp_filename, FileAccess::WRITE);3907if (temp_file.is_valid()) {3908temp_file->store_buffer(keystore_data);3909debug_keystore = temp_filename;3910}3911}3912}3913#endif39143915cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file.3916cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias.3917cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password.3918} else {3919// Pass the release keystore info as well3920String release_keystore = _get_keystore_path(p_preset, false);3921String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);3922String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);3923if (release_keystore.is_relative_path()) {3924release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path();3925}3926if (!FileAccess::exists(release_keystore)) {3927add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find release keystore, unable to export."));3928return ERR_FILE_CANT_OPEN;3929}39303931cmdline.push_back("-Prelease_keystore_file=" + release_keystore); // argument to specify the release keystore file.3932cmdline.push_back("-Prelease_keystore_alias=" + release_username); // argument to specify the release keystore alias.3933cmdline.push_back("-Prelease_keystore_password=" + release_password); // argument to specify the release keystore password.3934}3935}39363937List<String> copy_args;3938String copy_command = "copyAndRenameBinary";3939copy_args.push_back(copy_command);39403941#ifndef ANDROID_ENABLED3942copy_args.push_back("-p"); // argument to specify the start directory.3943copy_args.push_back(build_path); // start directory.3944#endif39453946copy_args.push_back("-Pexport_edition=" + edition.to_lower());39473948copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());39493950String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";3951copy_args.push_back("-Pexport_format=" + export_format_arg);39523953String export_filename = p_path.get_file();3954String export_path = p_path.get_base_dir();3955if (export_path.is_relative_path()) {3956export_path = OS::get_singleton()->get_resource_dir().path_join(export_path);3957}3958export_path = ProjectSettings::get_singleton()->globalize_path(export_path).simplify_path();39593960copy_args.push_back("-Pexport_path=file:" + export_path);3961copy_args.push_back("-Pexport_filename=" + export_filename);39623963#ifdef ANDROID_ENABLED3964String project_path = ProjectSettings::get_singleton()->globalize_path("res://");3965android_editor_gradle_runner->run_gradle(3966project_path,3967build_path.substr(project_path.length()),3968export_path.path_join(export_filename),3969export_format_arg,3970cmdline,3971copy_args);3972#else3973String build_project_output;3974int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);3975if (result != 0) {3976add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);3977return ERR_CANT_CREATE;3978} else {3979print_verbose(build_project_output);3980}39813982print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));3983String copy_binary_output;3984int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, ©_binary_output);3985if (copy_result != 0) {3986add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file:") + "\n\n" + copy_binary_output);3987return ERR_CANT_CREATE;3988} else {3989print_verbose(copy_binary_output);3990}39913992print_verbose("Successfully completed Android gradle build.");3993#endif3994return OK;3995}3996// This is the start of the Legacy build system3997print_verbose("Starting legacy build system...");3998if (p_debug) {3999src_apk = p_preset->get("custom_template/debug");4000} else {4001src_apk = p_preset->get("custom_template/release");4002}4003src_apk = src_apk.strip_edges();4004if (src_apk.is_empty()) {4005if (p_debug) {4006src_apk = find_export_template("android_debug.apk");4007} else {4008src_apk = find_export_template("android_release.apk");4009}4010if (src_apk.is_empty()) {4011add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(p_debug ? TTR("Debug export template not found: \"%s\".") : TTR("Release export template not found: \"%s\"."), src_apk));4012return ERR_FILE_NOT_FOUND;4013}4014}40154016Ref<FileAccess> io_fa;4017zlib_filefunc_def io = zipio_create_io(&io_fa);40184019if (ep.step(TTR("Creating APK..."), 0)) {4020return ERR_SKIP;4021}40224023unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io);4024if (!pkg) {4025add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk));4026return ERR_FILE_NOT_FOUND;4027}40284029int ret = unzGoToFirstFile(pkg);40304031Ref<FileAccess> io2_fa;4032zlib_filefunc_def io2 = zipio_create_io(&io2_fa);40334034String tmp_unaligned_path = EditorPaths::get_singleton()->get_temp_dir().path_join("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk");40354036#define CLEANUP_AND_RETURN(m_err) \4037{ \4038DirAccess::remove_file_or_error(tmp_unaligned_path); \4039return m_err; \4040} \4041((void)0)40424043zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);40444045String cmdline = p_preset->get("command_line/extra_args");40464047String version_name = p_preset->get_version("version/name");4048String package_name = p_preset->get("package/unique_name");40494050String apk_expansion_pkey = p_preset->get("apk_expansion/public_key");40514052Vector<ABI> invalid_abis(enabled_abis);40534054//To temporarily store icon xml data.4055Vector<uint8_t> themed_icon_xml_data;4056int icon_xml_compression_method = -1;40574058while (ret == UNZ_OK) {4059//get filename4060unz_file_info info;4061char fname[16384];4062ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);4063if (ret != UNZ_OK) {4064break;4065}40664067bool skip = false;40684069String file = String::utf8(fname);40704071Vector<uint8_t> data;4072data.resize(info.uncompressed_size);40734074//read4075unzOpenCurrentFile(pkg);4076unzReadCurrentFile(pkg, data.ptrw(), data.size());4077unzCloseCurrentFile(pkg);40784079//write4080if (file == "AndroidManifest.xml") {4081_fix_manifest(p_preset, data, p_give_internet);40824083// Allow editor export plugins to update the prebuilt manifest as needed.4084Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();4085for (int i = 0; i < export_plugins.size(); i++) {4086if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) {4087PackedByteArray export_plugin_data = export_plugins[i]->update_android_prebuilt_manifest(Ref<EditorExportPlatform>(this), data);4088if (!export_plugin_data.is_empty()) {4089data = export_plugin_data;4090}4091}4092}4093}4094if (file == "resources.arsc") {4095_fix_resources(p_preset, data);4096}40974098if (file == THEMED_ICON_XML_PATH) {4099// Store themed_icon.xml data.4100themed_icon_xml_data = data;4101skip = true;4102}41034104if (file == ICON_XML_PATH) {4105if (monochrome.is_valid() && !monochrome->is_empty()) {4106// Defer processing of icon.xml until after themed_icon.xml is read.4107icon_xml_compression_method = info.compression_method;4108skip = true;4109}4110}41114112if ((file.ends_with(".webp") || file.ends_with(".png")) && file.contains("mipmap")) {4113for (int i = 0; i < ICON_DENSITIES_COUNT; ++i) {4114if (main_image.is_valid() && !main_image->is_empty()) {4115if (file == LAUNCHER_ICONS[i].export_path) {4116_process_launcher_icons(file, main_image, LAUNCHER_ICONS[i].dimensions, data);4117}4118}4119if (foreground.is_valid() && !foreground->is_empty()) {4120if (file == LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].export_path) {4121_process_launcher_icons(file, foreground, LAUNCHER_ADAPTIVE_ICON_FOREGROUNDS[i].dimensions, data);4122}4123}4124if (background.is_valid() && !background->is_empty()) {4125if (file == LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].export_path) {4126_process_launcher_icons(file, background, LAUNCHER_ADAPTIVE_ICON_BACKGROUNDS[i].dimensions, data);4127}4128}4129if (monochrome.is_valid() && !monochrome->is_empty()) {4130if (file == LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].export_path) {4131_process_launcher_icons(file, monochrome, LAUNCHER_ADAPTIVE_ICON_MONOCHROMES[i].dimensions, data);4132}4133}4134}4135}41364137if (file.ends_with(".so")) {4138bool enabled = false;4139for (int i = 0; i < enabled_abis.size(); ++i) {4140if (file.begins_with("lib/" + enabled_abis[i].abi + "/")) {4141invalid_abis.erase(enabled_abis[i]);4142enabled = true;4143break;4144}4145}4146if (!enabled) {4147skip = true;4148}4149}41504151if (file.begins_with("META-INF") && should_sign) {4152skip = true;4153}41544155if (!skip) {4156print_line("ADDING: " + file);41574158// Respect decision on compression made by AAPT for the export template4159const bool uncompressed = info.compression_method == 0;41604161zip_fileinfo zipfi = get_zip_fileinfo();41624163zipOpenNewFileInZip(unaligned_apk,4164file.utf8().get_data(),4165&zipfi,4166nullptr,41670,4168nullptr,41690,4170nullptr,4171uncompressed ? 0 : Z_DEFLATED,4172Z_DEFAULT_COMPRESSION);41734174zipWriteInFileInZip(unaligned_apk, data.ptr(), data.size());4175zipCloseFileInZip(unaligned_apk);4176}41774178ret = unzGoToNextFile(pkg);4179}41804181// Process deferred icon.xml and replace it's data with themed_icon.xml.4182if (monochrome.is_valid() && !monochrome->is_empty()) {4183print_line("ADDING: " + ICON_XML_PATH + " (replacing with themed_icon.xml data)");41844185const bool uncompressed = icon_xml_compression_method == 0;4186zip_fileinfo zipfi = get_zip_fileinfo();41874188zipOpenNewFileInZip(unaligned_apk,4189ICON_XML_PATH.utf8().get_data(),4190&zipfi,4191nullptr,41920,4193nullptr,41940,4195nullptr,4196uncompressed ? 0 : Z_DEFLATED,4197Z_DEFAULT_COMPRESSION);41984199zipWriteInFileInZip(unaligned_apk, themed_icon_xml_data.ptr(), themed_icon_xml_data.size());4200zipCloseFileInZip(unaligned_apk);4201}42024203if (!invalid_abis.is_empty()) {4204add_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)));4205CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);4206}42074208if (ep.step(TTR("Adding files..."), 1)) {4209CLEANUP_AND_RETURN(ERR_SKIP);4210}4211err = OK;42124213if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) {4214APKExportData ed;4215ed.ep = &ep;4216ed.apk = unaligned_apk;4217err = export_project_files(p_preset, p_debug, ignore_apk_file, nullptr, &ed, save_apk_so);4218} else {4219if (apk_expansion) {4220err = save_apk_expansion_file(p_preset, p_debug, p_path);4221if (err != OK) {4222add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!"));4223return err;4224}4225} else {4226APKExportData ed;4227ed.ep = &ep;4228ed.apk = unaligned_apk;4229ed.pd.path = "assets.sparsepck";4230ed.pd.use_sparse_pck = true;4231if (p_preset->get_enc_directory()) {4232RandomPCG rng = RandomPCG(p_preset->get_seed());4233for (int i = 0; i < 32; i++) {4234ed.pd.salt += String::chr(1 + rng.rand() % 254);4235}4236}4237err = export_project_files(p_preset, p_debug, save_apk_file, nullptr, &ed, save_apk_so);42384239Vector<uint8_t> enc_data;4240err = _generate_sparse_pck_metadata(p_preset, ed.pd, enc_data);4241if (err != OK) {4242add_message(EXPORT_MESSAGE_ERROR, TTR("Save PCK"), TTR("Could not generate sparse pck metadata!"));4243return err;4244}42454246store_in_apk(&ed, "assets/assets.sparsepck", enc_data, 0);4247}4248}42494250if (err != OK) {4251unzClose(pkg);4252add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files.")));4253CLEANUP_AND_RETURN(ERR_SKIP);4254}42554256zip_fileinfo zipfi = get_zip_fileinfo();4257zipOpenNewFileInZip(unaligned_apk,4258"assets/_cl_",4259&zipfi,4260nullptr,42610,4262nullptr,42630,4264nullptr,42650, // No compress (little size gain and potentially slower startup)4266Z_DEFAULT_COMPRESSION);4267zipWriteInFileInZip(unaligned_apk, command_line_flags.ptr(), command_line_flags.size());4268zipCloseFileInZip(unaligned_apk);4269zipClose(unaligned_apk, nullptr);4270unzClose(pkg);42714272// Let's zip-align (must be done before signing)42734274static const int PAGE_SIZE_KB = 16 * 1024;4275static const int ZIP_ALIGNMENT = 4;42764277// If we're not signing the apk, then the next step should be the last.4278const int next_step = should_sign ? 103 : 105;4279if (ep.step(TTR("Aligning APK..."), next_step)) {4280CLEANUP_AND_RETURN(ERR_SKIP);4281}42824283unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io);4284if (!tmp_unaligned) {4285add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK.")));4286CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND);4287}42884289ret = unzGoToFirstFile(tmp_unaligned);42904291io2 = zipio_create_io(&io2_fa);4292zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2);42934294// Take files from the unaligned APK and write them out to the aligned one4295// in raw mode, i.e. not uncompressing and recompressing, aligning them as needed,4296// following what is done in https://github.com/android/platform_build/blob/master/tools/zipalign/ZipAlign.cpp4297int bias = 0;4298while (ret == UNZ_OK) {4299unz_file_info info;4300memset(&info, 0, sizeof(info));43014302char fname[16384];4303char extra[16384];4304ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0);4305if (ret != UNZ_OK) {4306break;4307}43084309String file = String::utf8(fname);43104311Vector<uint8_t> data;4312data.resize(info.compressed_size);43134314// read4315int method, level;4316unzOpenCurrentFile2(tmp_unaligned, &method, &level, 1); // raw read4317long file_offset = unzGetCurrentFileZStreamPos64(tmp_unaligned);4318unzReadCurrentFile(tmp_unaligned, data.ptrw(), data.size());4319unzCloseCurrentFile(tmp_unaligned);43204321// align4322int padding = 0;4323if (!info.compression_method) {4324// Uncompressed file => Align4325long new_offset = file_offset + bias;4326const char *ext = strrchr(fname, '.');4327if (ext && strcmp(ext, ".so") == 0) {4328padding = (PAGE_SIZE_KB - (new_offset % PAGE_SIZE_KB)) % PAGE_SIZE_KB;4329} else {4330padding = (ZIP_ALIGNMENT - (new_offset % ZIP_ALIGNMENT)) % ZIP_ALIGNMENT;4331}4332}43334334memset(extra + info.size_file_extra, 0, padding);43354336zip_fileinfo fileinfo = get_zip_fileinfo();4337zipOpenNewFileInZip2(final_apk,4338file.utf8().get_data(),4339&fileinfo,4340extra,4341info.size_file_extra + padding,4342nullptr,43430,4344nullptr,4345method,4346level,43471); // raw write4348zipWriteInFileInZip(final_apk, data.ptr(), data.size());4349zipCloseFileInZipRaw(final_apk, info.uncompressed_size, info.crc);43504351bias += padding;43524353ret = unzGoToNextFile(tmp_unaligned);4354}43554356zipClose(final_apk, nullptr);4357unzClose(tmp_unaligned);43584359if (should_sign) {4360// Signing must be done last as any additional modifications to the4361// file will invalidate the signature.4362err = sign_apk(p_preset, p_debug, p_path, ep);4363if (err != OK) {4364// Message is supplied by the subroutine method.4365CLEANUP_AND_RETURN(err);4366}4367}43684369CLEANUP_AND_RETURN(OK);4370}43714372void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features) const {4373r_features->push_back("mobile");4374r_features->push_back("android");4375}43764377void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) {4378}43794380void EditorExportPlatformAndroid::initialize() {4381if (EditorNode::get_singleton()) {4382Ref<Image> img = memnew(Image);4383const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);43844385ImageLoaderSVG::create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false);4386logo = ImageTexture::create_from_image(img);43874388ImageLoaderSVG::create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false);4389run_icon = ImageTexture::create_from_image(img);43904391#ifndef DISABLE_DEPRECATED4392android_plugins_changed.set();4393#endif // DISABLE_DEPRECATED4394#ifndef ANDROID_ENABLED4395devices_changed.set();4396_create_editor_debug_keystore_if_needed();4397_update_preset_status();4398check_for_changes_thread.start(_check_for_changes_poll_thread, this);4399use_scrcpy = EditorSettings::get_singleton()->get_project_metadata("android", "use_scrcpy", false);4400#else // ANDROID_ENABLED4401android_editor_gradle_runner = memnew(AndroidEditorGradleRunner);4402#endif // ANDROID_ENABLED4403}4404}44054406EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {4407#ifndef ANDROID_ENABLED4408quit_request.set();4409if (check_for_changes_thread.is_started()) {4410check_for_changes_thread.wait_to_finish();4411}4412#else4413if (android_editor_gradle_runner) {4414memdelete(android_editor_gradle_runner);4415}4416#endif4417}441844194420