prepare.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. /**
  2. Licensed to the Apache Software Foundation (ASF) under one
  3. or more contributor license agreements. See the NOTICE file
  4. distributed with this work for additional information
  5. regarding copyright ownership. The ASF licenses this file
  6. to you under the Apache License, Version 2.0 (the
  7. "License"); you may not use this file except in compliance
  8. with the License. You may obtain a copy of the License at
  9. http://www.apache.org/licenses/LICENSE-2.0
  10. Unless required by applicable law or agreed to in writing,
  11. software distributed under the License is distributed on an
  12. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13. KIND, either express or implied. See the License for the
  14. specific language governing permissions and limitations
  15. under the License.
  16. */
  17. var fs = require('fs-extra');
  18. var path = require('path');
  19. const nopt = require('nopt');
  20. const glob = require('fast-glob');
  21. var events = require('cordova-common').events;
  22. var AndroidManifest = require('./AndroidManifest');
  23. var checkReqs = require('./check_reqs');
  24. var xmlHelpers = require('cordova-common').xmlHelpers;
  25. var CordovaError = require('cordova-common').CordovaError;
  26. var ConfigParser = require('cordova-common').ConfigParser;
  27. var FileUpdater = require('cordova-common').FileUpdater;
  28. var PlatformJson = require('cordova-common').PlatformJson;
  29. var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
  30. var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
  31. const utils = require('./utils');
  32. const GradlePropertiesParser = require('./config/GradlePropertiesParser');
  33. function parseArguments (argv) {
  34. return nopt({
  35. // `jvmargs` is a valid option however, we don't actually want to parse it because we want the entire string as is.
  36. // jvmargs: String
  37. }, {}, argv || [], 0);
  38. }
  39. module.exports.prepare = function (cordovaProject, options) {
  40. var self = this;
  41. let args = {};
  42. if (options && options.options) {
  43. args = parseArguments(options.options.argv);
  44. }
  45. var platformJson = PlatformJson.load(this.locations.root, this.platform);
  46. var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
  47. this._config = updateConfigFilesFrom(cordovaProject.projectConfig, munger, this.locations);
  48. // Get the min SDK version from config.xml
  49. const minSdkVersion = this._config.getPreference('android-minSdkVersion', 'android');
  50. const maxSdkVersion = this._config.getPreference('android-maxSdkVersion', 'android');
  51. const targetSdkVersion = this._config.getPreference('android-targetSdkVersion', 'android');
  52. const androidXEnabled = this._config.getPreference('AndroidXEnabled', 'android');
  53. const isGradlePluginKotlinEnabled = this._config.getPreference('GradlePluginKotlinEnabled', 'android');
  54. const gradlePluginKotlinCodeStyle = this._config.getPreference('GradlePluginKotlinCodeStyle', 'android');
  55. const gradlePropertiesUserConfig = {};
  56. if (minSdkVersion) gradlePropertiesUserConfig.cdvMinSdkVersion = minSdkVersion;
  57. if (maxSdkVersion) gradlePropertiesUserConfig.cdvMaxSdkVersion = maxSdkVersion;
  58. if (targetSdkVersion) gradlePropertiesUserConfig.cdvTargetSdkVersion = targetSdkVersion;
  59. if (args.jvmargs) gradlePropertiesUserConfig['org.gradle.jvmargs'] = args.jvmargs;
  60. if (isGradlePluginKotlinEnabled) {
  61. gradlePropertiesUserConfig['kotlin.code.style'] = gradlePluginKotlinCodeStyle || 'official';
  62. }
  63. // Both 'useAndroidX' and 'enableJetifier' are linked together.
  64. if (androidXEnabled) {
  65. gradlePropertiesUserConfig['android.useAndroidX'] = androidXEnabled;
  66. gradlePropertiesUserConfig['android.enableJetifier'] = androidXEnabled;
  67. }
  68. const gradlePropertiesParser = new GradlePropertiesParser(this.locations.root);
  69. gradlePropertiesParser.configure(gradlePropertiesUserConfig);
  70. // Update own www dir with project's www assets and plugins' assets and js-files
  71. return Promise.resolve(updateWww(cordovaProject, this.locations)).then(function () {
  72. // update project according to config.xml changes.
  73. return updateProjectAccordingTo(self._config, self.locations);
  74. }).then(function () {
  75. updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
  76. updateSplashes(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
  77. updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root));
  78. }).then(function () {
  79. events.emit('verbose', 'Prepared android project successfully');
  80. });
  81. };
  82. module.exports.clean = function (options) {
  83. // A cordovaProject isn't passed into the clean() function, because it might have
  84. // been called from the platform shell script rather than the CLI. Check for the
  85. // noPrepare option passed in by the non-CLI clean script. If that's present, or if
  86. // there's no config.xml found at the project root, then don't clean prepared files.
  87. var projectRoot = path.resolve(this.root, '../..');
  88. if ((options && options.noPrepare) || !fs.existsSync(this.locations.configXml) ||
  89. !fs.existsSync(this.locations.configXml)) {
  90. return Promise.resolve();
  91. }
  92. var projectConfig = new ConfigParser(this.locations.configXml);
  93. var self = this;
  94. return Promise.resolve().then(function () {
  95. cleanWww(projectRoot, self.locations);
  96. cleanIcons(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
  97. cleanSplashes(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
  98. cleanFileResources(projectRoot, projectConfig, path.relative(projectRoot, self.locations.root));
  99. });
  100. };
  101. /**
  102. * Updates config files in project based on app's config.xml and config munge,
  103. * generated by plugins.
  104. *
  105. * @param {ConfigParser} sourceConfig A project's configuration that will
  106. * be merged into platform's config.xml
  107. * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
  108. * for this platform.
  109. * @param {Object} locations A map of locations for this platform
  110. *
  111. * @return {ConfigParser} An instance of ConfigParser, that
  112. * represents current project's configuration. When returned, the
  113. * configuration is already dumped to appropriate config.xml file.
  114. */
  115. function updateConfigFilesFrom (sourceConfig, configMunger, locations) {
  116. events.emit('verbose', 'Generating platform-specific config.xml from defaults for android at ' + locations.configXml);
  117. // First cleanup current config and merge project's one into own
  118. // Overwrite platform config.xml with defaults.xml.
  119. fs.copySync(locations.defaultConfigXml, locations.configXml);
  120. // Then apply config changes from global munge to all config files
  121. // in project (including project's config)
  122. configMunger.reapply_global_munge().save_all();
  123. events.emit('verbose', 'Merging project\'s config.xml into platform-specific android config.xml');
  124. // Merge changes from app's config.xml into platform's one
  125. var config = new ConfigParser(locations.configXml);
  126. xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
  127. config.doc.getroot(), 'android', /* clobber= */true);
  128. config.write();
  129. return config;
  130. }
  131. /**
  132. * Logs all file operations via the verbose event stream, indented.
  133. */
  134. function logFileOp (message) {
  135. events.emit('verbose', ' ' + message);
  136. }
  137. /**
  138. * Updates platform 'www' directory by replacing it with contents of
  139. * 'platform_www' and app www. Also copies project's overrides' folder into
  140. * the platform 'www' folder
  141. *
  142. * @param {Object} cordovaProject An object which describes cordova project.
  143. * @param {Object} destinations An object that contains destination
  144. * paths for www files.
  145. */
  146. function updateWww (cordovaProject, destinations) {
  147. var sourceDirs = [
  148. path.relative(cordovaProject.root, cordovaProject.locations.www),
  149. path.relative(cordovaProject.root, destinations.platformWww)
  150. ];
  151. // If project contains 'merges' for our platform, use them as another overrides
  152. var merges_path = path.join(cordovaProject.root, 'merges', 'android');
  153. if (fs.existsSync(merges_path)) {
  154. events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
  155. sourceDirs.push(path.join('merges', 'android'));
  156. }
  157. var targetDir = path.relative(cordovaProject.root, destinations.www);
  158. events.emit(
  159. 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
  160. FileUpdater.mergeAndUpdateDir(
  161. sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
  162. }
  163. /**
  164. * Cleans all files from the platform 'www' directory.
  165. */
  166. function cleanWww (projectRoot, locations) {
  167. var targetDir = path.relative(projectRoot, locations.www);
  168. events.emit('verbose', 'Cleaning ' + targetDir);
  169. // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
  170. FileUpdater.mergeAndUpdateDir(
  171. [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
  172. }
  173. /**
  174. * Updates project structure and AndroidManifest according to project's configuration.
  175. *
  176. * @param {ConfigParser} platformConfig A project's configuration that will
  177. * be used to update project
  178. * @param {Object} locations A map of locations for this platform
  179. */
  180. function updateProjectAccordingTo (platformConfig, locations) {
  181. // Update app name by editing res/values/strings.xml
  182. var strings = xmlHelpers.parseElementtreeSync(locations.strings);
  183. var name = platformConfig.name();
  184. strings.find('string[@name="app_name"]').text = name.replace(/'/g, '\\\'');
  185. var shortName = platformConfig.shortName && platformConfig.shortName();
  186. if (shortName && shortName !== name) {
  187. strings.find('string[@name="launcher_name"]').text = shortName.replace(/'/g, '\\\'');
  188. }
  189. fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8');
  190. events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);
  191. // Java packages cannot support dashes
  192. var androidPkgName = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');
  193. var manifest = new AndroidManifest(locations.manifest);
  194. var manifestId = manifest.getPackageId();
  195. manifest.getActivity()
  196. .setOrientation(platformConfig.getPreference('orientation'))
  197. .setLaunchMode(findAndroidLaunchModePreference(platformConfig));
  198. manifest.setVersionName(platformConfig.version())
  199. .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version()))
  200. .setPackageId(androidPkgName)
  201. .write();
  202. // Java file paths shouldn't be hard coded
  203. const javaDirectory = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'));
  204. const java_files = glob.sync('**/*.java', { cwd: javaDirectory, absolute: true }).filter(f => {
  205. const contents = fs.readFileSync(f, 'utf-8');
  206. return /extends\s+CordovaActivity/.test(contents);
  207. });
  208. if (java_files.length === 0) {
  209. throw new CordovaError('No Java files found that extend CordovaActivity.');
  210. } else if (java_files.length > 1) {
  211. events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]);
  212. }
  213. const destFile = java_files[0];
  214. // var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0]));
  215. // fs.ensureDirSync(path.dirname(destFile));
  216. // events.emit('verbose', java_files[0]);
  217. // events.emit('verbose', destFile);
  218. // console.log(locations);
  219. // fs.copySync(java_files[0], destFile);
  220. utils.replaceFileContents(destFile, /package [\w.]*;/, 'package ' + androidPkgName + ';');
  221. events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile);
  222. var removeOrigPkg = checkReqs.isWindows() || checkReqs.isDarwin()
  223. ? manifestId.toUpperCase() !== androidPkgName.toUpperCase()
  224. : manifestId !== androidPkgName;
  225. if (removeOrigPkg) {
  226. // If package was name changed we need to remove old java with main activity
  227. fs.removeSync(java_files[0]);
  228. // remove any empty directories
  229. var currentDir = path.dirname(java_files[0]);
  230. var sourcesRoot = path.resolve(locations.root, 'src');
  231. while (currentDir !== sourcesRoot) {
  232. if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
  233. fs.rmdirSync(currentDir);
  234. currentDir = path.resolve(currentDir, '..');
  235. } else {
  236. break;
  237. }
  238. }
  239. }
  240. }
  241. // Consturct the default value for versionCode as
  242. // PATCH + MINOR * 100 + MAJOR * 10000
  243. // see http://developer.android.com/tools/publishing/versioning.html
  244. function default_versionCode (version) {
  245. var nums = version.split('-')[0].split('.');
  246. var versionCode = 0;
  247. if (+nums[0]) {
  248. versionCode += +nums[0] * 10000;
  249. }
  250. if (+nums[1]) {
  251. versionCode += +nums[1] * 100;
  252. }
  253. if (+nums[2]) {
  254. versionCode += +nums[2];
  255. }
  256. events.emit('verbose', 'android-versionCode not found in config.xml. Generating a code based on version in config.xml (' + version + '): ' + versionCode);
  257. return versionCode;
  258. }
  259. function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
  260. // Use same extension as source with special case for 9-Patch files
  261. const ext = sourceName.endsWith('.9.png')
  262. ? '.9.png' : path.extname(sourceName).toLowerCase();
  263. const subDir = density ? `${type}-${density}` : type;
  264. return path.join(resourcesDir, subDir, name + ext);
  265. }
  266. function getAdaptiveImageResourcePath (resourcesDir, type, density, name, sourceName) {
  267. if (/\.9\.png$/.test(sourceName)) {
  268. name = name.replace(/\.png$/, '.9.png');
  269. }
  270. var resourcePath = path.join(resourcesDir, (density ? type + '-' + density + '-v26' : type), name);
  271. return resourcePath;
  272. }
  273. function makeSplashCleanupMap (projectRoot, resourcesDir) {
  274. // Build an initial resource map that deletes all existing splash screens
  275. const existingSplashPaths = glob.sync(
  276. `${resourcesDir.replace(/\\/g, '/')}/drawable-*/screen.{png,9.png,webp,jpg,jpeg}`,
  277. { cwd: projectRoot }
  278. );
  279. return makeCleanResourceMap(existingSplashPaths);
  280. }
  281. function updateSplashes (cordovaProject, platformResourcesDir) {
  282. var resources = cordovaProject.projectConfig.getSplashScreens('android');
  283. // if there are "splash" elements in config.xml
  284. if (resources.length === 0) {
  285. events.emit('verbose', 'This app does not have splash screens defined');
  286. return;
  287. }
  288. // Build an initial resource map that deletes all existing splash screens
  289. const resourceMap = makeSplashCleanupMap(cordovaProject.root, platformResourcesDir);
  290. var hadMdpi = false;
  291. resources.forEach(function (resource) {
  292. if (!resource.density) {
  293. return;
  294. }
  295. if (resource.density === 'mdpi') {
  296. hadMdpi = true;
  297. }
  298. var targetPath = getImageResourcePath(
  299. platformResourcesDir, 'drawable', resource.density, 'screen', path.basename(resource.src));
  300. resourceMap[targetPath] = resource.src;
  301. });
  302. // There's no "default" drawable, so assume default == mdpi.
  303. if (!hadMdpi && resources.defaultResource) {
  304. var targetPath = getImageResourcePath(
  305. platformResourcesDir, 'drawable', 'mdpi', 'screen', path.basename(resources.defaultResource.src));
  306. resourceMap[targetPath] = resources.defaultResource.src;
  307. }
  308. events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
  309. FileUpdater.updatePaths(
  310. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  311. }
  312. function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
  313. var resources = projectConfig.getSplashScreens('android');
  314. if (resources.length > 0) {
  315. const resourceMap = makeSplashCleanupMap(projectRoot, platformResourcesDir);
  316. events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);
  317. // No source paths are specified in the map, so updatePaths() will delete the target files.
  318. FileUpdater.updatePaths(
  319. resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  320. }
  321. }
  322. function updateIcons (cordovaProject, platformResourcesDir) {
  323. const icons = cordovaProject.projectConfig.getIcons('android');
  324. // Skip if there are no app defined icons in config.xml
  325. if (icons.length === 0) {
  326. events.emit('verbose', 'This app does not have launcher icons defined');
  327. return;
  328. }
  329. // 1. loop icons determin if there is an error in the setup.
  330. // 2. during initial loop, also setup for legacy support.
  331. const errorMissingAttributes = [];
  332. const errorLegacyIconNeeded = [];
  333. let hasAdaptive = false;
  334. icons.forEach((icon, key) => {
  335. if (
  336. (icon.background && !icon.foreground) ||
  337. (!icon.background && icon.foreground) ||
  338. (!icon.background && !icon.foreground && !icon.src)
  339. ) {
  340. errorMissingAttributes.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width));
  341. }
  342. if (icon.foreground) {
  343. hasAdaptive = true;
  344. if (
  345. !icon.src &&
  346. (
  347. icon.foreground.startsWith('@color') ||
  348. path.extname(path.basename(icon.foreground)) === '.xml'
  349. )
  350. ) {
  351. errorLegacyIconNeeded.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width));
  352. } else if (!icon.src) {
  353. icons[key].src = icon.foreground;
  354. }
  355. }
  356. });
  357. const errorMessage = [];
  358. if (errorMissingAttributes.length > 0) {
  359. errorMessage.push('One of the following attributes are set but missing the other for the density type: ' + errorMissingAttributes.join(', ') + '. Please ensure that all require attributes are defined.');
  360. }
  361. if (errorLegacyIconNeeded.length > 0) {
  362. errorMessage.push('For the following icons with the density of: ' + errorLegacyIconNeeded.join(', ') + ', adaptive foreground with a defined color or vector can not be used as a standard fallback icon for older Android devices. To support older Android environments, please provide a value for the src attribute.');
  363. }
  364. if (errorMessage.length > 0) {
  365. throw new CordovaError(errorMessage.join(' '));
  366. }
  367. let resourceMap = Object.assign(
  368. {},
  369. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
  370. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
  371. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
  372. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
  373. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
  374. mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
  375. );
  376. const preparedIcons = prepareIcons(icons);
  377. if (hasAdaptive) {
  378. resourceMap = updateIconResourceForAdaptive(preparedIcons, resourceMap, platformResourcesDir);
  379. }
  380. resourceMap = updateIconResourceForLegacy(preparedIcons, resourceMap, platformResourcesDir);
  381. events.emit('verbose', 'Updating icons at ' + platformResourcesDir);
  382. FileUpdater.updatePaths(resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  383. }
  384. function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformResourcesDir) {
  385. const android_icons = preparedIcons.android_icons;
  386. const default_icon = preparedIcons.default_icon;
  387. // The source paths for icons and splashes are relative to
  388. // project's config.xml location, so we use it as base path.
  389. let background;
  390. let foreground;
  391. let targetPathBackground;
  392. let targetPathForeground;
  393. for (const density in android_icons) {
  394. let backgroundVal = '@mipmap/ic_launcher_background';
  395. let foregroundVal = '@mipmap/ic_launcher_foreground';
  396. background = android_icons[density].background;
  397. foreground = android_icons[density].foreground;
  398. if (!background || !foreground) {
  399. // This icon isn't an adaptive icon, so skip it
  400. continue;
  401. }
  402. if (background.startsWith('@color')) {
  403. // Colors Use Case
  404. backgroundVal = background; // Example: @color/background_foobar_1
  405. } else if (path.extname(path.basename(background)) === '.xml') {
  406. // Vector Use Case
  407. targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.xml', path.basename(android_icons[density].background));
  408. resourceMap[targetPathBackground] = android_icons[density].background;
  409. } else if (path.extname(path.basename(background)) === '.png') {
  410. // Images Use Case
  411. targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.png', path.basename(android_icons[density].background));
  412. resourceMap[targetPathBackground] = android_icons[density].background;
  413. }
  414. if (foreground.startsWith('@color')) {
  415. // Colors Use Case
  416. foregroundVal = foreground;
  417. } else if (path.extname(path.basename(foreground)) === '.xml') {
  418. // Vector Use Case
  419. targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.xml', path.basename(android_icons[density].foreground));
  420. resourceMap[targetPathForeground] = android_icons[density].foreground;
  421. } else if (path.extname(path.basename(foreground)) === '.png') {
  422. // Images Use Case
  423. targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.png', path.basename(android_icons[density].foreground));
  424. resourceMap[targetPathForeground] = android_icons[density].foreground;
  425. }
  426. // create an XML for DPI and set color
  427. const icLauncherTemplate = `<?xml version="1.0" encoding="utf-8"?>
  428. <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  429. <background android:drawable="` + backgroundVal + `" />
  430. <foreground android:drawable="` + foregroundVal + `" />
  431. </adaptive-icon>`;
  432. const launcherXmlPath = path.join(platformResourcesDir, 'mipmap-' + density + '-v26', 'ic_launcher.xml');
  433. // Remove the XML from the resourceMap so the file does not get removed.
  434. delete resourceMap[launcherXmlPath];
  435. fs.writeFileSync(path.resolve(launcherXmlPath), icLauncherTemplate);
  436. }
  437. // There's no "default" drawable, so assume default == mdpi.
  438. if (default_icon && !android_icons.mdpi) {
  439. let defaultTargetPathBackground;
  440. let defaultTargetPathForeground;
  441. if (background.startsWith('@color')) {
  442. // Colors Use Case
  443. targetPathBackground = default_icon.background;
  444. } else if (path.extname(path.basename(background)) === '.xml') {
  445. // Vector Use Case
  446. defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.xml', path.basename(default_icon.background));
  447. resourceMap[defaultTargetPathBackground] = default_icon.background;
  448. } else if (path.extname(path.basename(background)) === '.png') {
  449. // Images Use Case
  450. defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.png', path.basename(default_icon.background));
  451. resourceMap[defaultTargetPathBackground] = default_icon.background;
  452. }
  453. if (foreground.startsWith('@color')) {
  454. // Colors Use Case
  455. targetPathForeground = default_icon.foreground;
  456. } else if (path.extname(path.basename(foreground)) === '.xml') {
  457. // Vector Use Case
  458. defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.xml', path.basename(default_icon.foreground));
  459. resourceMap[defaultTargetPathForeground] = default_icon.foreground;
  460. } else if (path.extname(path.basename(foreground)) === '.png') {
  461. // Images Use Case
  462. defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.png', path.basename(default_icon.foreground));
  463. resourceMap[defaultTargetPathForeground] = default_icon.foreground;
  464. }
  465. }
  466. return resourceMap;
  467. }
  468. function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResourcesDir) {
  469. const android_icons = preparedIcons.android_icons;
  470. const default_icon = preparedIcons.default_icon;
  471. // The source paths for icons and splashes are relative to
  472. // project's config.xml location, so we use it as base path.
  473. for (var density in android_icons) {
  474. var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher', path.basename(android_icons[density].src));
  475. resourceMap[targetPath] = android_icons[density].src;
  476. }
  477. // There's no "default" drawable, so assume default == mdpi.
  478. if (default_icon && !android_icons.mdpi) {
  479. var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher', path.basename(default_icon.src));
  480. resourceMap[defaultTargetPath] = default_icon.src;
  481. }
  482. return resourceMap;
  483. }
  484. function prepareIcons (icons) {
  485. // http://developer.android.com/design/style/iconography.html
  486. const SIZE_TO_DENSITY_MAP = {
  487. 36: 'ldpi',
  488. 48: 'mdpi',
  489. 72: 'hdpi',
  490. 96: 'xhdpi',
  491. 144: 'xxhdpi',
  492. 192: 'xxxhdpi'
  493. };
  494. const android_icons = {};
  495. let default_icon;
  496. // find the best matching icon for a given density or size
  497. // @output android_icons
  498. var parseIcon = function (icon, icon_size) {
  499. // do I have a platform icon for that density already
  500. var density = icon.density || SIZE_TO_DENSITY_MAP[icon_size];
  501. if (!density) {
  502. // invalid icon defition ( or unsupported size)
  503. return;
  504. }
  505. var previous = android_icons[density];
  506. if (previous && previous.platform) {
  507. return;
  508. }
  509. android_icons[density] = icon;
  510. };
  511. // iterate over all icon elements to find the default icon and call parseIcon
  512. for (var i = 0; i < icons.length; i++) {
  513. var icon = icons[i];
  514. var size = icon.width;
  515. if (!size) {
  516. size = icon.height;
  517. }
  518. if (!size && !icon.density) {
  519. if (default_icon) {
  520. const found = {};
  521. const favor = {};
  522. // populating found icon.
  523. if (icon.background && icon.foreground) {
  524. found.background = icon.background;
  525. found.foreground = icon.foreground;
  526. }
  527. if (icon.src) {
  528. found.src = icon.src;
  529. }
  530. if (default_icon.background && default_icon.foreground) {
  531. favor.background = default_icon.background;
  532. favor.foreground = default_icon.foreground;
  533. }
  534. if (default_icon.src) {
  535. favor.src = default_icon.src;
  536. }
  537. events.emit('verbose', 'Found extra default icon: ' + JSON.stringify(found) + ' and ignoring in favor of ' + JSON.stringify(favor) + '.');
  538. } else {
  539. default_icon = icon;
  540. }
  541. } else {
  542. parseIcon(icon, size);
  543. }
  544. }
  545. return {
  546. android_icons: android_icons,
  547. default_icon: default_icon
  548. };
  549. }
  550. function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
  551. var icons = projectConfig.getIcons('android');
  552. // Skip if there are no app defined icons in config.xml
  553. if (icons.length === 0) {
  554. events.emit('verbose', 'This app does not have launcher icons defined');
  555. return;
  556. }
  557. const resourceMap = Object.assign(
  558. {},
  559. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
  560. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
  561. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
  562. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
  563. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
  564. mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
  565. );
  566. events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir);
  567. // No source paths are specified in the map, so updatePaths() will delete the target files.
  568. FileUpdater.updatePaths(resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  569. }
  570. /**
  571. * Gets a map containing resources of a specified name from all drawable folders in a directory.
  572. */
  573. function mapImageResources (rootDir, subDir, type, resourceName) {
  574. const pathMap = {};
  575. const globOptions = { cwd: path.join(rootDir, subDir), onlyDirectories: true };
  576. glob.sync(type + '-*', globOptions).forEach(drawableFolder => {
  577. const imagePath = path.join(subDir, drawableFolder, resourceName);
  578. pathMap[imagePath] = null;
  579. });
  580. return pathMap;
  581. }
  582. /** Returns resource map that deletes all given paths */
  583. function makeCleanResourceMap (resourcePaths) {
  584. const pathMap = {};
  585. resourcePaths.map(path.normalize)
  586. .forEach(resourcePath => {
  587. pathMap[resourcePath] = null;
  588. });
  589. return pathMap;
  590. }
  591. function updateFileResources (cordovaProject, platformDir) {
  592. var files = cordovaProject.projectConfig.getFileResources('android');
  593. // if there are resource-file elements in config.xml
  594. if (files.length === 0) {
  595. events.emit('verbose', 'This app does not have additional resource files defined');
  596. return;
  597. }
  598. var resourceMap = {};
  599. files.forEach(function (res) {
  600. var targetPath = path.join(platformDir, res.target);
  601. resourceMap[targetPath] = res.src;
  602. });
  603. events.emit('verbose', 'Updating resource files at ' + platformDir);
  604. FileUpdater.updatePaths(
  605. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  606. }
  607. function cleanFileResources (projectRoot, projectConfig, platformDir) {
  608. var files = projectConfig.getFileResources('android', true);
  609. if (files.length > 0) {
  610. events.emit('verbose', 'Cleaning resource files at ' + platformDir);
  611. var resourceMap = {};
  612. files.forEach(function (res) {
  613. var filePath = path.join(platformDir, res.target);
  614. resourceMap[filePath] = null;
  615. });
  616. FileUpdater.updatePaths(
  617. resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  618. }
  619. }
  620. /**
  621. * Gets and validates 'AndroidLaunchMode' prepference from config.xml. Returns
  622. * preference value and warns if it doesn't seems to be valid
  623. *
  624. * @param {ConfigParser} platformConfig A configParser instance for
  625. * platform.
  626. *
  627. * @return {String} Preference's value from config.xml or
  628. * default value, if there is no such preference. The default value is
  629. * 'singleTop'
  630. */
  631. function findAndroidLaunchModePreference (platformConfig) {
  632. var launchMode = platformConfig.getPreference('AndroidLaunchMode');
  633. if (!launchMode) {
  634. // Return a default value
  635. return 'singleTop';
  636. }
  637. var expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
  638. var valid = expectedValues.indexOf(launchMode) >= 0;
  639. if (!valid) {
  640. // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
  641. events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' +
  642. launchMode + '. Expected values are: ' + expectedValues.join(', '));
  643. }
  644. return launchMode;
  645. }