Path: blob/master/platform/android/export/gradle_export_util.cpp
10278 views
/**************************************************************************/1/* gradle_export_util.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 "gradle_export_util.h"3132#include "core/config/project_settings.h"3334int _get_android_orientation_value(DisplayServer::ScreenOrientation screen_orientation) {35switch (screen_orientation) {36case DisplayServer::SCREEN_PORTRAIT:37return 1;38case DisplayServer::SCREEN_REVERSE_LANDSCAPE:39return 8;40case DisplayServer::SCREEN_REVERSE_PORTRAIT:41return 9;42case DisplayServer::SCREEN_SENSOR_LANDSCAPE:43return 11;44case DisplayServer::SCREEN_SENSOR_PORTRAIT:45return 12;46case DisplayServer::SCREEN_SENSOR:47return 13;48case DisplayServer::SCREEN_LANDSCAPE:49default:50return 0;51}52}5354String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation) {55switch (screen_orientation) {56case DisplayServer::SCREEN_PORTRAIT:57return "portrait";58case DisplayServer::SCREEN_REVERSE_LANDSCAPE:59return "reverseLandscape";60case DisplayServer::SCREEN_REVERSE_PORTRAIT:61return "reversePortrait";62case DisplayServer::SCREEN_SENSOR_LANDSCAPE:63return "userLandscape";64case DisplayServer::SCREEN_SENSOR_PORTRAIT:65return "userPortrait";66case DisplayServer::SCREEN_SENSOR:67return "fullUser";68case DisplayServer::SCREEN_LANDSCAPE:69default:70return "landscape";71}72}7374int _get_app_category_value(int category_index) {75switch (category_index) {76case APP_CATEGORY_ACCESSIBILITY:77return 8;78case APP_CATEGORY_AUDIO:79return 1;80case APP_CATEGORY_IMAGE:81return 3;82case APP_CATEGORY_MAPS:83return 6;84case APP_CATEGORY_NEWS:85return 5;86case APP_CATEGORY_PRODUCTIVITY:87return 7;88case APP_CATEGORY_SOCIAL:89return 4;90case APP_CATEGORY_UNDEFINED:91return -1;92case APP_CATEGORY_VIDEO:93return 2;94case APP_CATEGORY_GAME:95default:96return 0;97}98}99100String _get_app_category_label(int category_index) {101switch (category_index) {102case APP_CATEGORY_ACCESSIBILITY:103return "accessibility";104case APP_CATEGORY_AUDIO:105return "audio";106case APP_CATEGORY_IMAGE:107return "image";108case APP_CATEGORY_MAPS:109return "maps";110case APP_CATEGORY_NEWS:111return "news";112case APP_CATEGORY_PRODUCTIVITY:113return "productivity";114case APP_CATEGORY_SOCIAL:115return "social";116case APP_CATEGORY_VIDEO:117return "video";118case APP_CATEGORY_GAME:119default:120return "game";121}122}123124// Utility method used to create a directory.125Error create_directory(const String &p_dir) {126if (!DirAccess::exists(p_dir)) {127Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES);128ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");129Error err = filesystem_da->make_dir_recursive(p_dir);130ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'.");131}132return OK;133}134135// Writes p_data into a file at p_path, creating directories if necessary.136// Note: this will overwrite the file at p_path if it already exists.137Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) {138String dir = p_path.get_base_dir();139Error err = create_directory(dir);140if (err != OK) {141return err;142}143Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);144ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");145fa->store_buffer(p_data.ptr(), p_data.size());146return OK;147}148149// Writes string p_data into a file at p_path, creating directories if necessary.150// Note: this will overwrite the file at p_path if it already exists.151Error store_string_at_path(const String &p_path, const String &p_data) {152String dir = p_path.get_base_dir();153Error err = create_directory(dir);154if (err != OK) {155if (OS::get_singleton()->is_stdout_verbose()) {156print_error("Unable to write data into " + p_path);157}158return err;159}160Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);161ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");162fa->store_string(p_data);163return OK;164}165166// Implementation of EditorExportSaveFunction.167// This method will only be called as an input to export_project_files.168// It is used by the export_project_files method to save all the asset files into the gradle project.169// It's functionality mirrors that of the method save_apk_file.170// This method will be called ONLY when gradle build is enabled.171Error rename_and_store_file_in_gradle_project(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) {172CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata);173174String simplified_path = p_path.simplify_path();175if (simplified_path.begins_with("uid://")) {176simplified_path = ResourceUID::uid_to_path(simplified_path).simplify_path();177print_verbose(vformat(R"(UID referenced exported file name "%s" was replaced with "%s".)", p_path, simplified_path));178}179180Vector<uint8_t> enc_data;181EditorExportPlatform::SavedData sd;182Error err = _store_temp_file(simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, enc_data, sd);183if (err != OK) {184return err;185}186187const String dst_path = export_data->assets_directory + String("/") + simplified_path.trim_prefix("res://");188print_verbose("Saving project files from " + simplified_path + " into " + dst_path);189err = store_file_at_path(dst_path, enc_data);190191export_data->pd.file_ofs.push_back(sd);192return err;193}194195String _android_xml_escape(const String &p_string) {196// Android XML requires strings to be both valid XML (`xml_escape()`) but also197// to escape characters which are valid XML but have special meaning in Android XML.198// https://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling199// Note: Didn't handle U+XXXX unicode chars, could be done if needed.200return p_string201.replace("@", "\\@")202.replace("?", "\\?")203.replace("'", "\\'")204.replace("\"", "\\\"")205.replace("\n", "\\n")206.replace("\t", "\\t")207.xml_escape(false);208}209210// Creates strings.xml files inside the gradle project for different locales.211Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset, const String &p_project_name, const String &p_gradle_build_dir, const Dictionary &p_appnames) {212print_verbose("Creating strings resources for supported locales for project " + p_project_name);213// Stores the string into the default values directory.214String processed_default_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(p_project_name));215store_string_at_path(p_gradle_build_dir.path_join("res/values/godot_project_name_string.xml"), processed_default_xml_string);216217// Searches the Gradle project res/ directory to find all supported locales218Ref<DirAccess> da = DirAccess::open(p_gradle_build_dir.path_join("res"));219if (da.is_null()) {220if (OS::get_singleton()->is_stdout_verbose()) {221print_error("Unable to open Android resources directory.");222}223return ERR_CANT_OPEN;224}225da->list_dir_begin();226while (true) {227String file = da->get_next();228if (file.is_empty()) {229break;230}231if (!file.begins_with("values-")) {232// NOTE: This assumes all directories that start with "values-" are for localization.233continue;234}235String locale = file.replace("values-", "").replace("-r", "_");236String locale_directory = p_gradle_build_dir.path_join("res/" + file + "/godot_project_name_string.xml");237if (p_appnames.has(locale)) {238String locale_project_name = p_appnames[locale];239String processed_xml_string = vformat(GODOT_PROJECT_NAME_XML_STRING, _android_xml_escape(locale_project_name));240print_verbose("Storing project name for locale " + locale + " under " + locale_directory);241store_string_at_path(locale_directory, processed_xml_string);242} else {243// TODO: Once the legacy build system is deprecated we don't need to have xml files for this else branch244store_string_at_path(locale_directory, processed_default_xml_string);245}246}247da->list_dir_end();248return OK;249}250251String bool_to_string(bool v) {252return v ? "true" : "false";253}254255String _get_gles_tag() {256return " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n";257}258259String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) {260String manifest_screen_sizes = " <supports-screens \n tools:node=\"replace\"";261String sizes[] = { "small", "normal", "large", "xlarge" };262constexpr size_t num_sizes = std::size(sizes);263for (size_t i = 0; i < num_sizes; i++) {264String feature_name = vformat("screen/support_%s", sizes[i]);265String feature_support = bool_to_string(p_preset->get(feature_name));266String xml_entry = vformat("\n android:%sScreens=\"%s\"", sizes[i], feature_support);267manifest_screen_sizes += xml_entry;268}269manifest_screen_sizes += " />\n";270return manifest_screen_sizes;271}272273String _get_activity_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug) {274String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(p_export_platform->get_project_setting(p_preset, "display/window/handheld/orientation"))));275String manifest_activity_text = vformat(276" <activity android:name=\".GodotApp\" "277"tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" "278"tools:node=\"mergeOnlyAttributes\" "279"android:excludeFromRecents=\"%s\" "280"android:screenOrientation=\"%s\" "281"android:resizeableActivity=\"%s\">\n",282bool_to_string(p_preset->get("package/exclude_from_recents")),283orientation,284bool_to_string(bool(p_export_platform->get_project_setting(p_preset, "display/window/size/resizable"))));285286manifest_activity_text += " <intent-filter>\n"287" <action android:name=\"android.intent.action.MAIN\" />\n"288" <category android:name=\"android.intent.category.DEFAULT\" />\n";289290bool show_in_app_library = p_preset->get("package/show_in_app_library");291if (show_in_app_library) {292manifest_activity_text += " <category android:name=\"android.intent.category.LAUNCHER\" />\n";293}294295bool uses_leanback_category = p_preset->get("package/show_in_android_tv");296if (uses_leanback_category) {297manifest_activity_text += " <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n";298}299300bool uses_home_category = p_preset->get("package/show_as_launcher_app");301if (uses_home_category) {302manifest_activity_text += " <category android:name=\"android.intent.category.HOME\" />\n";303}304305manifest_activity_text += " </intent-filter>\n";306307Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();308for (int i = 0; i < export_plugins.size(); i++) {309if (export_plugins[i]->supports_platform(p_export_platform)) {310const String contents = export_plugins[i]->get_android_manifest_activity_element_contents(p_export_platform, p_debug);311if (!contents.is_empty()) {312manifest_activity_text += contents;313manifest_activity_text += "\n";314}315}316}317318manifest_activity_text += " </activity>\n";319return manifest_activity_text;320}321322String _get_application_tag(const Ref<EditorExportPlatform> &p_export_platform, const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission, bool p_debug, const Vector<MetadataInfo> &p_metadata) {323int app_category_index = (int)(p_preset->get("package/app_category"));324bool is_game = app_category_index == APP_CATEGORY_GAME;325326String manifest_application_text = vformat(327" <application android:label=\"@string/godot_project_name_string\"\n"328" android:allowBackup=\"%s\"\n"329" android:icon=\"@mipmap/icon\"\n"330" android:isGame=\"%s\"\n"331" android:hasFragileUserData=\"%s\"\n"332" android:requestLegacyExternalStorage=\"%s\"\n",333bool_to_string(p_preset->get("user_data_backup/allow")),334bool_to_string(is_game),335bool_to_string(p_preset->get("package/retain_data_on_uninstall")),336bool_to_string(p_has_read_write_storage_permission));337if (app_category_index != APP_CATEGORY_UNDEFINED) {338manifest_application_text += vformat(" android:appCategory=\"%s\"\n", _get_app_category_label(app_category_index));339manifest_application_text += " tools:replace=\"android:allowBackup,android:appCategory,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";340} else {341manifest_application_text += " tools:remove=\"android:appCategory\"\n";342manifest_application_text += " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n";343}344manifest_application_text += " tools:ignore=\"GoogleAppIndexingWarning\">\n\n";345346for (int i = 0; i < p_metadata.size(); i++) {347manifest_application_text += vformat(" <meta-data tools:node=\"replace\" android:name=\"%s\" android:value=\"%s\" />\n", p_metadata[i].name, p_metadata[i].value);348}349350Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();351for (int i = 0; i < export_plugins.size(); i++) {352if (export_plugins[i]->supports_platform(p_export_platform)) {353const String contents = export_plugins[i]->get_android_manifest_application_element_contents(p_export_platform, p_debug);354if (!contents.is_empty()) {355manifest_application_text += contents;356manifest_application_text += "\n";357}358}359}360361manifest_application_text += _get_activity_tag(p_export_platform, p_preset, p_debug);362manifest_application_text += " </application>\n";363return manifest_application_text;364}365366Error _store_temp_file(const String &p_simplified_path, const Vector<uint8_t> &p_data, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key, uint64_t p_seed, Vector<uint8_t> &r_enc_data, EditorExportPlatform::SavedData &r_sd) {367Error err = OK;368Ref<FileAccess> ftmp = FileAccess::create_temp(FileAccess::WRITE_READ, "export", "tmp", false, &err);369if (err != OK) {370return err;371}372r_sd.path_utf8 = p_simplified_path.trim_prefix("res://").utf8();373r_sd.ofs = 0;374r_sd.size = p_data.size();375err = EditorExportPlatform::_encrypt_and_store_data(ftmp, p_simplified_path, p_data, p_enc_in_filters, p_enc_ex_filters, p_key, p_seed, r_sd.encrypted);376if (err != OK) {377return err;378}379380r_enc_data.resize(ftmp->get_length());381ftmp->seek(0);382ftmp->get_buffer(r_enc_data.ptrw(), r_enc_data.size());383ftmp.unref();384385// Store MD5 of original file.386{387unsigned char hash[16];388CryptoCore::md5(p_data.ptr(), p_data.size(), hash);389r_sd.md5.resize(16);390for (int i = 0; i < 16; i++) {391r_sd.md5.write[i] = hash[i];392}393}394return OK;395}396397398